The Hitchhiker's Guide to Kotlin

December 11, 2020

Lately I've been doing a lot of work in a relatively new programming language: Kotlin. From my experience, I've concluded that Kotlin is pretty rad. If you've considered learning Kotlin, or just using it in a personal project, this post might help you with your decision. Below, I hope I'll (attempt) to tell you a little bit about my experience with Kotlin, and describe what I liked about Kotlin and what I didn't like.

Firstly, you should know Kotlin is a JVM (Java Virtual Machine) language, which means that like Java, Scala, and Clojure, Kotlin (usually) compiles to Java bytecode. That means that Kotlin is compatible with libraries written in Java, so if there's a Java library that solves a problem you're dealing with, you can just pull that Java library into your Kotlin project without any need for translation or hoop-jumping. Given the popularity of Java in the software development world for the last 20 years, that's a powerful set of tools to start with for a new language.

Now, of course, you're probably wondering: if I want to use a Java library, why wouldn't I just write Java? A good question, of course; it's not like there's anything inherently wrong with Java! Many of us learned Java syntax in college, the workforce, or even high school for one reason or another. If you know C++, C, or even Javascript syntax, it's not a huge hurdle to learn Java, or at least get to a point of vague competence -- though let's not pretend that the subtleties of Java, like managing dependencies or producing build artifacts (properly) isn't a big hurdle of itself!

Anyway, Java works just fine. But Java is also a language whose syntax was designed literally decades ago. Despite the best intentions of the original language creators, some syntax decisions haven't been popular in the long term, and Java still struggles with the repercussions of those decisions. Kotlin, on the other hand, was designed in the past couple of years. While newer does not necessarily mean better, in terms of syntax it definitely translates to "simpler" in this case. Anyone who's written Java can appreciate that while Java syntax is hardly as opaque as C++ template programming or Perl, it can be, at the very least, a tad bit verbose. For example, consider the following class declaration that describes a dog:

import java.awt.Color;

public class Dog {
    private String name;
    private int age;
    private Color color;

    public Dog(String name, int age, Color color) {
        this.name = name;
        this.age = age;
        this.color = color;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public Color getColor() {
        return color;
    }

    public void setColor(Color color) {
        this.color = color;
    }
}

Nothing unexpected, right? The class is called Dog. It's public, which means that other classes in this package can reference this class (though this class is top-level in a file, so the public modifier is redundant). This class contains values for the dog's name, the dog's age, and the color of the dog. There's a constructor, which accepts parameters matching each of those values and assigns the parameter values to the fields stored within the class. And of course, there are accessors and mutators (getters and setters for the unwashed masses, myself included), which actually allow code outside of this class to access and modify the fields of this class, which are set to private because encapsulation best practices say that code outside of a class should never directly access class fields.

Now let's look at the same class in Kotlin, which functions exactly the same, with accessors, mutators, a constructor, and encapsulation best practices:

import java.awt.Color

data class Dog(var name: String,
               var age: Int,
               var color: Color)

Woof. As you can see, the Kotlin class is a lot, lot, lot shorter. And that's because Kotlin's ethos is all about setting sensible defaults. There's no need to label this class public, because that's implicit. There's no need to define a constructor AND a getter and a setter for every single field; because this is a data class whose sole purpose is to store data, Kotlin "just knows" that we want accessors, mutators, and a constructor. But this is just the beginning of the benefits of Kotlin.

A JIRA ticket comes in, priority 1. "Clients love these Dog thingies", sales says. "But we need to do more with dogs! The clients want to add two dogs together. Can you implement this?" Fortunately, as a seasoned Kotlin developer, you know that you can throw a solution together in just a few simple story points. So you get cracking, and decide to get fancy by overriding the behavior of the + operator for operations involving two Dog objects:

operator fun plus(dog : Dog) : Dog {
    return Dog(dog.name + this.name, dog.age
           + this.age, dog.color + this.color)
}

But you've hit a snag. The String and Int classes define behavior for the + operator. But the Color class doesn't. Worse yet, Color isn't even a Kotlin class -- it's a member of the Java standard library! Fortunately, Kotlin lets you add member functions to any class using something called Kotlin Extensions:

operator fun Color.plus(color: Color) : Color {
    return Color(color.red + this.red, color.blue + this.blue,
        color.green + this.green)
}

Because the red, blue, and green fields stored within the Color class are actually integers, we can just add those together for an (admittedly simple) additive color implementation. And the Color class even handles capping the values at 255 for us!

Thanks to your fancy coding skills, you can add two dogs together like so:

val dog1 : Dog = Dog("henry", 3, Color.RED)
var dog2 = Dog("indiana", 7, Color.GREEN)
val dog3 = dog1 + dog2

What else does Kotlin offer? Well, anybody familiar with Java has inevitably experienced the dreaded NullPointerException at some point. We've all indexed past the end of an array or a string, incorrectly handled a null value when a network call timed out, or any of a million other things that threw a null value right into an object that we forgot to check for a null value with != null. Because writing all those null checks isn't just a pain -- it makes code harder to read! Kotlin makes this issue a lot easier to avoid with explicit nullability. Consider the code snippet above, where we instantiated two Dog objects. Both dog1 and dog2 are non-nullable: they can never point at a null object. If you want to assign a value a nullable type in Kotlin, you have to use the ? operator:

fun maybeDog() : Dog? {
    if (Random.nextInt() % 2 == 0) {
        return Dog("casper", 3, Color.GRAY)
    }
    return null
}

val mightBeDog : Dog? = maybeDog()
println(mightBeDog?.name)
if (mightBeDog != null) {
    println(mightBeDog.name)
}
println(mightBeDog!!.name)

In this example, mightbeDog is of type Dog? -- in other words, it is a nullable Dog type. Whenever you access a nullable value, you need to do so safely: usually, using the safe nullable lookup operator: ?, which you can see when we print out the name of mightBeDog. Basically this operator resolves to "the field you're trying to look up, or null if the base object is null" instead of triggering a NullPointerException. However, you can also use the unsafe nullable lookup operator: !!, which lets you trigger NullPointerException just like you could in Java (see above). Finally, Kotlin includes a feature called smart casting, which basically says "if I check a value in an if statement, assume the implication of that statement in the if statement codeblock". In the above example, in the codeblock corresponding to the mightBeDog != null if check, we don't have to use a safe lookup operator. That's because in that code block, mightBeDog is actually cast to type Dog instead of just Dog?. This also works with types themselves, not just nullability, so if you had an instance of Animal with an if check that checked if the instance was actually subtype Dog, you wouldn't have to perform an if check and cast to type Dog, a common Java-ism.

Did you like this post? Let me know! Coming up: part 2, where I discuss coroutines and a few interesting parts of Kotlin besides mere syntax.