Why Kotlin over Java

Nils de Groot's photo
Nils de Groot
·Nov 7, 2022·

12 min read

Table of contents

For most projects related to server-side software development, Cloudflight prefers to use Kotlin over Java. This has a multitude of reasons, some of which I'll describe in this blog post.

A plain old Java object according to Kotlin

When transferring data from one place to another with Java, it's common to use a plain old java object (POJO). Usually, this is an object with some properties and accessor methods.

Kotlin provides means to reduce the amount of code used in these objects. In this example, we'll take a POJO, and convert it into a Kotlin class.

Plain old Java objects

Let's start with a UserDto, for now, it will only contain a few fields, and accessor methods for those fields. It could look as follows:

public class UserDto {
    private Integer id;

    private String name;

    private String email;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }
}

If you are using IntelliJ IDEA and would copy this code into a Kotlin file, we'll get a notification asking if the Java code should be converted to Kotlin code. After clicking yes, we get the following code.

class UserDto {
    var id: Int? = null
    var name: String? = null
    var email: String? = null
}

The piece of Kotlin code is 25 lines shorter compared to the Java code. All the accessor methods can be omitted by the syntax of Kotlin.

Constructors

To be able to instantiate a fully initialized copy of the earlier defined Java object we would need to add a constructor. If we would make one for all our arguments, it would look as follows:

public class UserDto {
    public UserDto(
        id: Integer,
        name: String,
        email: String
    ) {
        this.id = id;
        this.name = name;
        this.email = email;
    }

    ...
}

Adding the constructor in Java would add more code. if we add more properties to the class, we would have to update the constructor as well.

Meanwhile, on the Kotlin side, we can update our class as follows to add a constructor.

class UserDto(
    var id: Int? = null,
    var name: String? = null,
    var email: String? = null
)

By replacing the braces with parentheses, we added a constructor to the class, without adding a line of code. Both constructors are called the same way in both Java and Kotlin code.

Equals, Hashcode, ToString

Commonly, Java classes use some boilerplate methods as well. This would add more code to our UserDto class.

public class UserDto {
    ...

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        UserDto userDto = (UserDto) o;
        return id.equals(userDto.id) && name.equals(userDto.name) && email.equals(userDto.email);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, name, email);
    }

    @Override
    public String toString() {
        return "UserDto{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", email='" + email + '\'' +
                '}';
    }
}

Similar to the constructor, when updating our class, we would need to update these methods as well.

Kotlin again provides a solution for this with only one line change. Kotlin introduces data classes. Data classes define a handful of extra methods for us (equals, hashCode, toString and copy). If we would use a data class to give us these methods, our class will look as follows.

data class UserDto(
    var id: Int? = null,
    var name: String? = null,
    var email: String? = null
)

But all of this code can be generated by any modern IDE. Why would I use Kotlin for this?

The resulting Java version has a whopping 63 lines of code, and 63 lines of maintenance. Meanwhile, the Kotlin version has only 5 lines. Leading to a codebase that's easier to navigate, has fewer surprises and is easier to maintain.

I took a quick look at a relatively small project and found 35 similar classes to the freshly refactored class. I'd estimate that using Kotlin in this project would save 4000 lines of code and just POJO's.

Safer and more expressive code

The following section will refactor a function from a Java-looking function, into a Kotlin function, using features Kotlin provides us to write safe and more expressive code.

First, we define a basic tree. A tree is either a branch with multiple Tree items or a leaf with one value in it. Here is the Java version:

interface Tree<T> {

}

class Branch<T> implements Tree<T> {
    public Branch(List<Tree<T>> nodes) {
        this.nodes = nodes;
    }

    List<Tree<T>> nodes;

    public List<Tree<T>> getNodes() {
        return nodes;
    }
}

class Leaf<T> implements Tree<T> {
    public Leaf(T value) {
        this.value = value;
    }

    T value;

    public T getValue() {
        return value;
    }
}

In Kotlin it would look as follows:

interface Tree<T>

class Branch<T>(val nodes: List<Tree<T>>) : Tree<T>

class Leaf<T>(val value: T) : Tree<T>

Next, we write a function to refactor into Kotlin. For this example, I made a function to sum all numbers in the tree. In Java you could make it like this:

class TreeUtil {
    static Integer sumTree(Tree<Integer> tree) {
        Integer count;

        if (tree instanceof Branch) {
            var branch = (Branch<Integer>) tree;
            count = 0;
            for (var node : branch.getNodes()) {
                count += sumTree(node);
            }
        } else if (tree instanceof Leaf) {
            var leaf = (Leaf<Integer>) tree;
            count = leaf.getValue();
        } else {
            throw new IllegalArgumentException("Unknown variant");
        }

        return count;
    }
}

Directly translating this to Kotlin gives me the following:

fun sumTree(tree: Tree<Int>): Int {
    var count: Int

    if (tree is Branch) {
        val branch = tree as Branch

        count = 0
        for (node in branch.nodes) {
            count += sumTree(node)
        }
    } else if (tree is Leaf) {
        val leaf = tree as Leaf
        count = leaf.value
    } else {
        throw IllegalArgumentException("Unknown variant")
    }

    return count
}

Now let's apply some of Kotlin's features to make this function more readable and expressive.

Expressions

A common pattern in Java is to define a variable set to null and update it during an if statement. In Kotlin, if statements are expressions, this allows us to return values from the if statement instead of mutating it during the if statement. Allowing us to refactor the method as follows.

fun sumTree(tree: Tree<Int>): Int = if (tree is Branch) {
    val branch = tree as Branch
    var count = 0
    for (node in branch.nodes) {
        count += sumTree(node)
    }

    count
} else if (tree is Leaf) {
    val leaf = tree as Leaf
    leaf.value
} else {
    throw IllegalArgumentException("Unknown variant")
}

Instead of returning a value, we can return the if statement as a whole, making for less noise code.

Smart casting

Type casting an object in Java requires two steps. First, a check needs to be made if the type matches the expected one, then the value needs to be cast into another type. Kotlin introduces smart casting here. After checking the type, the compiler already knows the actual type, so here we could omit the cast. When using smart casting, our code would look like this.

fun sumTree(tree: Tree<Int>): Int = if (tree is Branch) {
    var count = 0
    for (node in tree.nodes) {
        count += sumTree(node)
    }

    count
} else if (tree is Leaf) {
    tree.value
} else {
    throw IllegalArgumentException("Unknown variant")
}

After calling the type check, tree becomes either a Branch or a Leaf implicitly, allowing us to remove the unsafe typecast and directly access the properties from the tree variable.

Pattern matching

Kotlin allows us to pattern match for a type, then we could check the type without adding cases to an if statement, refactoring our code to the following.

fun sumTree(tree: Tree<Int>): Int = when (tree) {
    is Branch -> {
        var count = 0
        for (node in tree.nodes) {
            count += sumTree(node)
        }

        count
    }
    is Leaf -> tree.value
    else -> throw IllegalArgumentException("Unknown variant")
}

The when statement in Kotlin is similar to the switch in Java, but it provides some superpowers compared to switch, primarily that we can define a condition for each branch instead of only checking equality.

Sealed classes

Kotlin introduces sealed classes, all implementors of a sealed class must be in the same module as the sealed class itself. We can make our tree a sealed class in the following manner:

sealed interface Tree<T>

Now the compiler knows all possible types a tree could be, allowing us to safely remove the else clause from the when statement.

fun sumTree(tree: Tree<Int>): Int = when (tree) {
    is Branch -> {
        var count = 0
        for (node in tree.nodes) {
            count += sumTree(node)
        }

        count
    }
    is Leaf -> tree.value
}

If we would remove the sealed modifier, the program will not compile because the pattern match is not exhaustive. Because only two versions of Tree are possible, we only need to check for those types.

Functional abstraction

To clean up the last part of the Java code, we could rewrite the iteration of all nodes in the branch into something more readable.

fun sumTree(tree: Tree<Int>): Int = when (tree) {
    is Branch -> tree.nodes.sumOf { node -> sumTree(node) }
    is Leaf -> tree.value
}

The sumOf method is a standard library method turning iterable into an int. If we would write it ourselves it could look like this:

fun <T> Iterable<T>.sumOf(operation: (T) -> Int): Int {
    var sum = 0
    for (item in this) {
        sum += operation(item)
    }

    return sum
}

A few things are happening here:

  • We are defining an extension method, even though Iterable is not in our class, we can write functions for it. This is not possible in Java at all.
  • The parameter operation is a function that expects type T and returns a Int.

Using the tools that Kotlin provides, the Java-like function is rewritten into 4 lines of expressive Kotlin code.

Other features

Next, some other features of Kotlin.

Compile-time null safety

One of the primary selling points for Kotlin is compile-time null safety. For a variable to be null, it needs to be defined as nullable. Nullable variables need to be handled or checked. This is one of the primary defects in Java systems.

In Kotlin, a parameter can be assigned as nullable as follows:

fun sumTwoNumbers(n1: Int?, n2: Int?): Int {
    return n1 + n2 // Will not compile because n1 or n2 could be null
}

This example will not compile. Both n1 and n2 could be null. To make it compile, we need to add some checks or have different behavior for null values. Using smart-casting, we can safely add the values.

fun sumTwoNumbers(n1: Int?, n2: Int?): Int {
    if (n1 == null || n2 == null) throw Exception("Invalid value passed")
    return n1 + n2
}

Interoperability with Java

Kotlin was designed to be fully cross-compatible with JVM Java. This allows us to define logic or structures in either Kotlin or Java code, and call them from both Kotlin and Java code. I'll take the earlier created UserDto as an example.

data class UserDto(var id: Int, var name: String, var email: String)

With Gradle or Maven properly configured, we can call this code the Java side in an idiomatically correct manner. It would look at follows:

void main() {
    var user = new UserDto(1, "Cloudflight", "info@cloudflight.io");

    System.out.println(user.toString()); // UserDto(id=1, name=Cloudflight, email=info@cloudflight.io)
    System.out.println(user.getId()); // 1
}

Preferred immutability

Java includes a final keyword, which disallows a variable from being reassigned. A small example is shown below.

void main() {
    var someMutableValue = "The first value";
    someMutableValue = "The value is updated"

    final var someImmutableValue = "The first value"
    someImmutableValue = "Will not compile now"
}

The final keyword does add noise to the code. Kotlin has a much more subtile approach to this.

fun main() {
    var someMutableValue = "The first value";
    someMutableValue = "The value is updated"

    val someImmutableValue = "The first value"
    someImmutableValue = "Will not compile now"
}

Kotlin has two keywords for defining variables, val and var. val is used for immutable variables, while var is for mutable variables.

This pattern can also be found in the Kotlin standard library.

Due to historical reasons, most collections in Java have a method named add, which adds an item to the collection. This includes immutable lists. These usually throw an UnsupportedOperationException at runtime when called.

void main() {
    var list = new ArrayList();
    list.add(1);
    list.add(2);

    var immutableList = List.of();
    immutableList.add(1); // Throws an UnsupportedOperationException at runtime
}

In Kotlin, collections are immutable by default. To create a mutable list mutableListOf should be called, and to create an immutable list, listOf is called. An immutable list has no to add items to a collection, this could prevent runtime defects.

fun main() {
    var list = mutableListOf()
    list.add(1)
    list.add(2)

    var immutableList = listOf()
    immutableList.add(1) // Will not compile here
}

Internal visibility

Kotlin introduces a new visibility keyword, internal. This allows code to be visible in the following cases:

  • The internal code can be called within the same Maven project.
  • The internal code can be called within the same Gradle source set.
  • The internal code can be called by code compiled with the same kotlinc invocation.

This helps with modularizing codebases when working with separate modules.

Clarifications of some risks

There are a few points commonly made on why switching to Kotlin is a bad idea. This section explains why these points are not as strong as commonly thought of.

Is it possible to find Kotlin developers?

According to the StackOverflow developer survey 2022, 9.2% of developers work with Kotlin compared to 33.27% who work with Java. The survey also finds that at least 10% of these developers want to work with Kotlin instead of Java.

Turning a Java developer into a Kotlin developer is easy. Writing Kotlin compared to writing Java is very similar. Both are usually run in the same environment as well. Both for development and execution. Together with the better code safety of Kotlin, a Java developer can quickly write good Kotlin code.

Is Kotlin future-proof?

At I/O 2019, Google announced that Kotlin is going to be the preferred programming language for all Android apps. This shows that Google is certain Kotlin will stay and Kotlin stays for a long time to come.

According to Google, 80% of the top 1000 android apps use Kotlin as a programming language.

Conclusion

So, a few of the reasons why Cloudflight (and other companies) prefer to use Kotlin over Java for server-side app development.

Sources

 
Share this