1. Introduction

In this article, we’ll examine the migration procedure from an old Kotlin compiler to the K2 Kotlin compiler. We’ll focus on the migration procedure only. Henceforth, we might refer to a new compiler as K2 and an old compiler as K1 (as it is also commonly referred to).

2. Migration Procedure

The K2 Compiler itself is not fully backward compatible with K1. We need to perform some extra steps to make our code compile on the K2 compiler. The detailed explanation for migration is described in the official migration guide. Here, we’ll just explain the most important changes that may impact ordinary users.

3. Open Properties Initialization

The K2 compiler demands, that all open properties with backing fields must be initialized immediately. In the past, the compiler required only open var properties to be initialized immediately. For instance, this code will not compile:

open class BaseEntity {
    open val points: Int
    open var pages: Long?

    init {
        points = 1
        pages = 12
    }
}

It’s, of course, a little bit strange since the code above and this one below compile to the same Java bytecode:

open class BaseEntity {
    open val points: Int = 1
    open var pages: Long? = 12 
}

The initialization of variables happens inside the constructor in both cases. Still, one of the samples of the code compiles, and another doesn’t. The exception to this rule is lateinit open var properties. They still may have deferred initialization:

open class WithLateinit {
    open lateinit var point: Instant
}

The K2 compiler will successfully compile the snippet above.

4. Synthetic Setters on Projected Types

To understand this change, we need to rewind a bit and consider what constraints generic types can have.

4.1. Generic Types Constraint

Let’s say we have the following code in Java:

public void add(List<?> list, Object element) {
    list.add(element);
}

This code does not compile. *And for a good reason – reference of type List<?> can point to an object of type List, List<List>, List, etc*. Therefore, would it be safe to allow adding an object of any particular type to this list? No, we cannot add anything apart from null into this list since we don’t actually know what exactly objects does List<?> represent.

In Kotlin, we have a star projection, which is similar to wildcards in Java. So in Kotlin, this would not compile as well for the exact same reasons:

fun execute(list: MutableList<*>, element: Any) {
    list.add(element)
}

With that in mind, we’re ready to understand the change K2 introduces regarding synthetic setters for generic types.

4.2. Bug in Kotlin Compiler

Now, imagine we have the following Java class:

public class Box<E> {
    private E value;

    public E getValue() {
        return value;
    }

    public void setValue(E value) {
        this.value = value;
    }
}

If we try to use this Java class in Kotlin like this, the compiler will also throw an error:

fun explicitSetter() {
    val box = Box<String>()
    val tmpBox : Box<*> = box
    tmpBox.setValue(12) // Compile Error! Thats unsafe!
    val myValue : String? = box.value
}

The reason is the same: it’s unsafe to perform this kind of operation since Box<*> may contain anything. The compiler saves us because the next line would’ve resulted in an error. But apparently, this code used to compile successfully with an old Kotlin compiler:

fun syntheticSetter() {
    val box = Box<String>()
    val tmpBox : Box<*> = box
    tmpBox.value = 12 // That compiles!
    val foo : String? = box.value // And here we fail with ClassCastException
}

Although this code compiles, it would always produce ClassCastException in runtime.

The reason is that we used the synthetic setter on the value field in the Box via reference of type Box<*> to set 12, which is an Int, to a box, that actually is supposed to contain String.

Therefore, the code that has reference to a Box, of course, rightfully expects that the value within the Box is an instance of a String, and the Kotlin compiler, just like javac, adds a cast bytecode instruction whenever we execute the getValue() function. And that cast, of course, fails since the value within the Box is not a String, but an Int.

So, the Kotlin K2 compiler fixed this bug from now on. It not only applies to a start projection type but to other projected types, like types annotated with in variance annotation:

fun syntheticSetter_inVariance() {
    val box = Box<String>()
    val tmpBox : Box<in String> = box
    tmpBox.value = 12 // Wow, thats a trap again!
    val foo : String? = box.value // Blast! ClassCastException
}

Here, it is exactly the same problem, but with in variance annotation. The K2 compiler would not compile such code, while K1 would not complain.

5. Properties Consistent Resolution Order

When using Kotlin, we can extend Java classes and vice versa. It might happen that both superclass and base class have the same fields. Since Kotlin doesn’t allow us to declare plain fields, but properties instead, by field, we should understand here a standard Java field as a class member and Kotlin backing field of the property.

So, let’s assume we have two classes. The first one is Java base class:

public class AbstractEntity {
    public String type = "ABSTRACT_TYPE";
    public String status = "ABSTRACT_STATUS";
}

And another one is Kotlin child class:

class AbstractEntitySubclass(val type: String) : AbstractEntity() {
    val status: String
        get() = "CONCRETE_STATUS"
}

fun main() {
    val sublcass = AbstractEntitySubclass("CONCRETE_TYPE")
    println(sublcass.type)
    println(sublcass.status)
}

If we tried to compile this code with K1, we’d succeed, but in runtime, we’d get a java.lang.IllegalAccessError. Meanwhile, K2 would compile this code successfully as well, but there will be no problem at runtime. Let’s understand why.

5.1. Properties Resolution

Let’s first understand what exactly the code above compiles. The parent class is very simple. In bytecode, it would indeed contain two public fields—nothing fancy. But the child class would contain one field—type—and two getters—getType and getStatus. The status itself would not have a backing field because there is no reason to create one—the status property in Kotlin is immutable, and its value is a string literal.

So, we have two type fields—one for the parent and another for the child. The difference, however, is that the field for the child would be private, while the parent’s type would be public.

So, K1 had problems when dealing with properties resolution in cases where we have Java and Kotlin classes extension hierarchy. And it is indeed a little bit of a confusing case. For instance, the AbstractEntitySubclass().type, in the world of Kotlin, can equally refer to the parent’s type field or child’s type property. And if the parent’s property can be accessed via a simple bytecode getfield instruction, the child’s type property needs to be accessed via a synthetically generated getter.

Therefore, the compiler needs to decide what actual fields we want to access and how – directly or via getter. So, K1 had a problem – it solved the problem that we described above with simple getfield bytecode instruction, but on the child’s class. This is, of course, wrong because, as we said, the child’s type field is private. It needs to be accessed with a synthetic getter. Therefore, code compiled, but at runtime JVM would notice that we’re trying to access something illegally and would throw an IllegalAccessError.

5.2. K2 Properties Resolution

K2 solves this problem and establishes a concrete resolution order of properties for such cases. The overall rule is this – child properties take precedence. It means that, if possible, considering access checks, the property of a more concrete type in the chain of extension wins. So, when compiling the code above with K2, we’ll get the following:

CONCRETE_TYPE
CONCRETE_STATUS

As we see, the child’s values are favored.

6. Retaining Nullability for Primitive Arrays

The Kotlin compiler (both K1 and K2) supports nullability annotations in Java code, such as JetBrains’s @Nullable and @NotNull for instance. Therefore, let’s assume we have the following method in Java:

public static @Nullable String fromCharArray(char[] s) {
    if (s == null || s.length == 0) return null;

    return new String(s);
}

Kotlin would successfully infer that the return value of the fromCharArray method is a nullable string, not just String. Therefore, in Kotlin, this wouldn’t work:

val resultString : String = fromCharArray(null) // Corret type should be String?

The problem, however, was that K1 was unable to infer the nullability information for primitive arrays with annotations on a type use. For instance, for this Java function, an old Kotlin compiler would not be able to infer the nullability information:

public static char @Nullable [] toCharArray(String s) {
    if (s == null) return null;

    return s.toCharArray();
}

Therefore, it would’ve been resulted in NPE at runtime in Kotlin code:

val array : CharArray = toCharArray(null) // That compiles fine in K1
println(array[0]) // NPE

The K2 compiler, on the other hand, successfully infers the nullability information for primitive arrays. So, the K2 compiler wouldn’t compile the code above, as the return value of toCharArary() is actually CharArray?, not CharArray.

6. Conclusion

The K2 compiler brought many improvements to the Kotlin compilation process. In general, it can catch many problems at compile time. This includes better work with synthetic setters and generics, better property resolution, better interoperability with Java in terms of null handling, and much more.

As always, the source code in the article is available over on GitHub.