New K2 Compiler in Kotlin. Part 2. Migration Guide

1. Introduction

In this article, we will look at the migration process from the old version of the Kotlin compiler to the new K2 compiler. In another article We have reviewed the K2 compiler in general, and here we will focus only on the migration procedure. From now on, by K2 we will obviously mean the new compiler, and by K1 – the old version of the compiler, since we will also mention it quite often.

2. Migration procedure

K2 is not fully backwards compatible with K1. We need to perform some additional steps to make our code compile on K2. A detailed explanation of the migration is given in official migration guideHere we will simply explain the most important changes that may affect regular users.

3. Initialization of open properties

K2 requires that all open properties with backing fields were initialized at the time of declaration. Previously, the compiler required initialization at the declaration site only for properties open var. For example:

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

This code will no longer compile. This is of course somewhat strange, since the code above is also the following:

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

compile to the same Java bytecode – the initialization of variables occurs inside the constructor in both cases. However, one of these code examples compiles and the other does not. The exception to this rule is properties of the type lateinit open var. They can still have lazy initialization:

open class WithLateinit { 
  open lateinit var point: Instant 
}

K2 will successfully compile the above code snippet.

4. Synthetic setters on Projected types

To understand this change, we need to step back a bit and talk about what limitations generic types can have.

4.1 Limitations of generic types
Let's assume we have the following Java code:

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

This code does not compile. And not without reason: a link like List may point to an object of type List, List>, List etc. Would it be safe to allow an object of any particular type to be added to this list? No, we can't add anything other than null to this list, because in fact we don't know: what exactly are the objects represented in ListIn Kotlin we have star projections, similar to wildcards in java. Therefore, in Kotlin, such code will not compile either:

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

For the same reason. So far everything is clear.

4.2 Kotlin Compiler Bug
Now imagine that 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; 
	} 
}

And if we try to use this Java class in Kotlin like this:

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

The compiler will also throw an error message. The reason is the same: it is not safe to perform such an operation because Box<*> can contain anything. The compiler saves us because the following line would have caused an error. But here's the code:

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 
} 

successfully compiled with the old Kotlin compiler! Although such code compiles, it always produces an error ClassCastException at runtime. The reason is that we used a synthetic setter for the field value via a link like Box<*> to set the value to 12 which is a value of type Int, to the box object, which should actually contain the type String. Therefore, the code containing the reference to Box rightfully expects that the meaning inside Box will be an instance of the class Stringand the Kotlin compiler, as well as javacwill add a bytecode cast instruction when we execute the function getValue(). And this cast naturally fails because the value inside the Box is not Stringand is Int. Kotlin K2 fixes this defect. This applies not only to start projection typebut also to other projected types, such as contravariant types with in-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 is exactly the same problem, but with a contravariant type marked in annotation. The K2 compiler will not compile such code, while K1 will not produce any errors.

5. Constant order of resolution of properties

When using Kotlin, we can extend Java classes and vice versa. And it may happen that both the superclass and the base class will have the same fields. Since Kotlin does not allow us to declare simple fields, but requires us to use properties instead, by “field” we mean a standard Java field as a class member, and a backing field of a property in the case of Kotlin. So, let’s assume that we have 2 classes, one of them is a base Java class:

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

And the second child Kotlin 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 the K1 compiler, we would succeed, but at runtime we would get java.lang.IllegalAccessError. At the same time, the K2 compiler would also compile such code successfully, but there would be no errors during execution. Let's try to understand why.

5.1 Resolution of object properties
Let's first understand what exactly the above code compiles to. The parent class is very simple, in bytecode it would indeed contain two public fields, nothing unusual. But the child class would contain one field – type – and two getters – getType And getStatus. Myself status would not have a backing field because there is no reason to create one – property status is immutable in Kotlin and its value is a string literal. So we have two fields type – one in the parent, the other in the child class. The difference, however, is that in the child class this field would be privateand in the parent type would public field. Thus, in K1 there was a problem with field resolution in cases where we had a hierarchy of extending classes in Java and Kotlin. And this is indeed a somewhat confusing case. For example, AbstractEntitySubclass().type, in the Kotlin world, it can be referred to as on the parent class field typeand on the child class property type. And if a property in a parent class in bytecode can be accessed through a simple bytecode instruction getfieldthen the property reference type in the child class requires calling a synthetically generated getter. Therefore, the compiler must decide which fields we want to access and how – directly or through a getter. So K1 had a drawback – it compiled the above code into a simple bytecode instruction. getfieldbut for the child class. This is of course wrong, since, as we have already said, in the child class the field type is private. It must be accessed via a synthetic getter. Accordingly, the code compiled, but during execution the JVM noticed that we were trying to access something in an illegal way and threw IllegalAccessError.

5.2. Resolution of object properties in K2
K2 solves this problem and establishes a specific property resolution order for such cases. The general rule is that properties of child classes take precedence. This means that properties of more specific classes take precedence over properties of ancestors at the same visibility level. So, when we compile the above code using K2, we get the following:

CONCRETE_TYPE
CONCRETE_STATUS

As you can see, the values ​​from the child class take precedence over the parent fields.

6. Preserving Nullability for Primitive Arrays

Kotlin compilers (both K1 and K2) support nullability annotations in Java code, such as, @Nullable And @NotNull from JetBrains. Therefore, if we have the following Java method:

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

The Kotlin compiler (both K1 and K2) successfully recognizes that the return value of the method fromCharArray – it's a nullable string, not just String. Therefore, the following code will not work in Kotlin:

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

The problem, however, was that K1 could not infer nullability information for primitive arrays with type-qualifying annotations (annotations with a value ElementType.TYPE_USE V @Target). For example, for this function in Java:

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

K1 would not be able to infer nullability information, so it would result in a NullPointerException when running Kotlin code:

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

K2, on the other hand, successfully infers nullability for primitive arrays. So K2 would not compile the above code because the return value toCharArary() In fact CharArray?but not CharArray.

7. Conclusion

K2 has brought many improvements to the Kotlin code compilation processes. In general, to summarize, it can detect many problems at compile time. Interaction with synthetic setters and generics, property resolution, interaction with Java in terms of null handling, and much more have been improved.

Join the Russian-speaking Spring Boot developer community in telegram – Spring AIOto stay up to date with the latest news from the world of Spring Boot development and everything related to it.

Waiting for everybody, join us!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *