Recently, a lot of presentations and articles on Kotlin. At the same time, most of them come down to listing the main advantages of the language, without considering how it works inside and in which byte the code is translated.
But in vain, because recently communicating with one of his colleagues, who just read one of the articles on Kotlin with an overview of the main features, proved to me that null-safety is evil and is implemented through exception handling, i.e. executing code:
name?.length
the compiler simply wraps the call in
try-catch , trying to catch a
NullPointerException .
')
Similarly, after another review, another comrade believed that since var is in Kotline, as in JS, then typing is dynamic here and there, and generally “all these
var / val are evil, nothing is clear, it's good that they are in Java not". Say hello, JEP286!
Another unfortunate example of popularization of a language happened recently when, at one of the presentations on Kotlin, the author of the report did not quite correctly describe the work of the language associated with primitives from Java, telling that reference types would always be used in Kotlin. I would like to tell about this in more detail.
The very essence of the problem with
autoboxing / unboxing in Java is well known: there are primitive types, there are reference wrapper classes. When using generic types, we cannot use primitives, since generics themselves are overwritten at runtime (yes, through reflection, we can still pull out this information), and instead of them, the usual Object lives and the ghost to the type that the compiler adds. However, Java allows you to cast from a primitive type to a reference type, i.e. from int to java.lang.Integer and vice versa, which is called autoboxing and unboxing, respectively. In addition to all the obvious problems arising from this, we are now interested in one thing - the fact that during such transformations a new reference object is created, which in general does not have a very good effect on performance (yes, in fact, the object is not always created, but only if will not fall into the cache).
So how does Kotlin behave?
First, it is worth recalling that Kotlin has his own set of types
kotlin.Int ,
kotlin.Long , etc. And at first glance it may seem that the situation is even worse than in Java, because object creation is always happening. However, it is not. Base classes in the standard library Kotlin virtual. This means that the classes themselves exist only at the stage of writing the code, then the compiler translates them to the target classes of the platform, in particular for the
kotlin.Int JVM
is translated to an
int . Those. code on Kotlin:
val tmp = 3.0 println(tmp)
After compiling:
double tmp = 3.0D; System.out.println(tmp);
Null- types Kotlin translates already in the reference, ie
kotlin.Int? -> java.lang.Integer , which is quite logical:
val tmp: Double? = 3.0 println(tmp)
After compiling:
Double tmp = Double.valueOf(3.0D); System.out.println(tmp);
Similarly, for
extension methods and properties . If we specify a non-
null type, the compiler will substitute the primitive as a receiver, if
nullable, then the reference wrapper class.
fun Int.example() { println(this) }
After compiling:
public final void example(int receiver) { System.out.println(receiver); }
In general, the basic idea is clear: the compiler, whenever possible, tries to use java primitives, otherwise the reference classes.
All this is good, but what about the primitive arrays?
Here the situation is similar: for arrays from primitives there are analogs in Kotlin, for example,
IntArray -> int [] , etc. For all other types, the generic class Array -> T [] is used. Moreover, the arrays in Kotlin support all the same “functional” operations as the collections, i.e.
map ,
fold ,
reduce , etc. Again, it can be assumed that there are generic functions under the hood that are called for each of the operations, and as a result, the same boxing at each iteration will work at the code byte level:
val intArr = intArrayOf(1, 2, 3) println(intArr.fold(0, { acc, cur -> acc + cur }))
However, this does not happen, because for each such operation Kotlin has an appropriate method with the desired type. It is clear that there are many similar functions that differ only in the type of array, but to solve this problem, code generation is used inside. In addition, the function itself and the transmitted lambda will be inlined at the point of the call, so all the code above will unfold in a simple loop:
int initial = 0; int accumulator = initial; for(int i = 0; i < receiver.length; ++i) { int element = receiver[i]; accumulator += element; } System.out.println(accumulator);
It is also worth considering that many functions (for example,
map ) for arrays return not a new array, but a list, with the result that autoboxing will still work, as it would be for any code with generalizations in Java.
Many skeptics are very concerned about the performance of "all these new languages." From all of the above, we can conclude (even without resorting to benchmarks, since the resulting code generated by Kotlin and written by hand in Java is almost identical) that the performance in the examples related to autoboxin / unboxing will be at least similar. However, no one cancels the fact that Kotlin, like any other tool or library, needs to be able to use and understand what is happening under the hood.