From the outside, it may seem that Kotlin has simplified Android development, without at all bringing new challenges: the language is Java-compatible, so even a large Java project can be gradually transferred to it without scoring a head, right? But if you look deeper, there is a double bottom in each box and a secret door in the dressing table. Programming languages ​​are too complex projects to combine without cunning nuances.
Of course, this does not mean “everything is bad and you don’t need to use Kotlin together with Java”, but it means that you should know about the nuances and take them into account. At our Mobius conference, Sergei Ryabov told how to write such code on Kotlin that you’ll be comfortable using from Java. And the audience liked the report so much that we not only decided to post a video, but also made a text version for Habr:
I have been writing on Kotlin for more than three years, now only on it, but at first I dragged Kotlin into existing Java projects. Therefore, the question "how to connect Java and Kotlin together" in my way arose quite often. ')
Often, when you add a Kotlin project, you can see how this ...
The specifics of the last couple of years: the most popular libraries acquire “wrappers” in order to be able to use them from Kotlin more idiomatically.
If you wrote on Kotlin, you know that there are cool extension-functions, inline-functions, lambda-expressions that are available from Java 6. And this is cool, it attracts us to Kotlin, but the question arises. One of the biggest, most publicized features of the language is interoperability with Java. If we take into account all the listed features, then why not just write libraries on Kotlin? They will all work out of the box with Java perfectly, and you will not need to support all these wrappers, everyone will be happy and contented.
But, of course, in practice, not everything is as rosy as in advertising brochures, there is always a “small sign”, there are sharp edges at the junction of Kotlin and Java, and today we will talk about this a little.
Sharp edges
Let's start with the differences. For example, do you know that there are no volatile, synchronized, strictfp, transient keywords in Kotlin? They are replaced by annotations of the same name that are in the kotlin.jvm package. So, about the contents of this package and go most of the conversation.
There is Timber - such a library-abstraction over loggers from the notorious Zheka Vartanov . It allows you to use it everywhere in your application, and everything where you want to send logs (to logcat, or to your server for analysis, or to crash reporting, and so on) turns into plugins.
Let's imagine for example that we want to write a similar library, just for analytics. Also abstracted.
We take the same construction pattern, we have one entry point - this is Analytics. We can send events there, add plugins and watch what we have already added there.
Plugin is a plugin interface that abstracts a particular analytic API.
And, actually, the Event class that contains the key and our attributes that we send. Here the report is not about whether it is worth using singltons, so let's not breed holivar, but we will watch how this whole thing is combed.
Now a little dive. Here is an example of using our library in Kotlin:
privatefunuseAnalytics() { Analytics.send(Event("only_name_event")) val props = mapOf( USER_ID to 1235, "my_custom_attr" to true ) Analytics.send(Event("custom_event", props)) val hasPlugins = Analytics.hasPlugins Analytics.addPlugin(EMPTY_PLUGIN) // dry-run Analytics.addPlugins(listOf(LoggerPlugin("ALog"), SegmentPlugin))) val plugins = Analytics.getPlugins() // ... }
In principle, it looks as expected. One entry point, methods are called a la statics. Event without parameters, event with attributes. We check if we have plug-ins, push an empty plug-in there in order to just do some kind of “dry run”. Or add a few other plugins, display them, and so on. In general, standard user cases, I hope everything is clear for now.
Now let's see what happens in Java when we do the same:
privatestaticvoiduseAnalytics(){ Analytics.INSTANCE.send(new Event("only_name_event", Collections.emptyMap())); final Map<String, Object> props = new HashMap<>(); props.put(USER_ID, 1235); props.put("my_custom_attr", true); Analytics.INSTANCE.send(new Event("custom_event", props)); boolean hasPlugins = Analytics.INSTANCE.getHasPlugins(); Analytics.INSTANCE.addPlugin(Analytics.INSTANCE.getEMPTY_PLUGIN()); // dry-run final List<EmptyPlugin> pluginsToSet = Arrays.asList(new LoggerPlugin("ALog"), new SegmentPlugin()); // ... }
Immediately, the fuss with INSTANCE, which is stretched upward, the presence of explicit values ​​for the default parameter with attributes, some getters with strong names, immediately catches the eye. Since we, in general, have gathered here to turn it into something similar to the previous file with Kotlin, let's go through each moment that we don’t like and try to adapt it somehow.
Let's start with the Event. We remove the Colletions.emptyMap () parameter from the second line, and a compiler error pops up. What is the reason?
data class Event( valname: String, valcontext: Map<String, Any> = emptyMap() )
Our constructor has a default parameter to which we pass the value. We come from Java to Kotlin, it is logical to assume that the presence of the default parameter generates two constructors: one complete with two parameters, and one partial, which can only be given a name. Obviously, the compiler does not think so. Let's see why he thinks we're wrong.
Our main tool for analyzing all the twists and turns of how Kotlin turns into a JVM bytecode - Kotlin Bytecode Viewer. In Android Studio and IntelliJ IDEA, it is located in the Tools - Kotlin - Show Kotlin Bytecode menu. You can simply press Cmd + Shift + A and enter Kotlin Bytecode in the search bar.
Here, surprisingly, we see the baytkod of what turns our Kotlin-class. I do not expect from you excellent knowledge of baytkod, and, most importantly, IDE developers also do not expect. Therefore, they made a button Decompile.
After pressing it, we see such a pretty good Java code:
We see our fields, getters, the expected constructor with two parameters name and context, everything happens fine. And below we see the second constructor, and here it is with an unexpected signature: not with one parameter, but for some reason with four.
Here you can be embarrassed, but you can climb a little deeper and delve. When we begin to understand, we will understand that DefaultConstructorMarker is a private class from the Kotlin standard library added here so that there are no conflicts with us written by designers, since we cannot set the parameters of the DefaultConstructorMarker type with our hands. And int var3 is interesting in all - the bit mask of what default values ​​we should use. In this case, if the bitmask matches the two, we know that var2 is not set, our attributes are not set, and we use the default value.
How can we fix the situation? To do this, there is a wonderful annotation @JvmOverloads from the package, which I have already mentioned. We have to hang it on the designer.
data class Event @JvmOverloads constructor( val name: String, val context: Map<String, Any> = emptyMap() )
And what will she do? Refer to the same tool. Now we can see both our complete constructor and the constructor with the DefaultConstructorMarker, and, miracle, the constructor with one parameter, which is now available from Java:
And, as you can see, it delegates all the work with default parameters to that our constructor with bit masks. Thus, we do not produce information about the fact that for the default value we need to shove there, we simply delegate everything to one constructor. Nice We check what we got from the Java side: the compiler is happy and not outraged.
Let's see what we don't like next. We do not like this INSTANCE, which in IDEA is purple in color. I do not like purple color :)
Let's check, due to what it turns out. Look at the baytkod again.
Let us select, for example, the function init and make sure that init is really generated not static.
That is, whatever one may say, we need to work with the instance of this class and call these methods on it. But we can generate the generation of all these methods as static. There is a wonderful annotation for this @JvmStatic. Let's add it to the functions init and send and check what the compiler thinks about it now.
We see that the static keyword has been added to the public final init (), and we have saved ourselves from working with INSTANCE. See for yourself in the Java code.
The compiler now tells us that we are calling a static method from the context INSTANCE. This can be corrected: press Alt + Enter, select "Cleanup Code", and voila, INSTANCE disappears, everything looks approximately the same as it was in Kotlin:
Analytics.send(new Event("only_name_event"));
Now we have a scheme for working with static methods. Add this annotation wherever it matters to us:
And a comment: if the methods we have are obviously methods of the instance, then, for example, with prop, not everything is so obvious. The fields themselves (for example, plugins) are generated as static. But the getters and setters work as instance methods. Therefore, you also need to add this annotation for the perpetrators to get the setters and getters as static. For example, we see the isInited variable, we add the @JvmStatic annotation to it, and now we see in the Kotlin Bytecode Viewer that the isInited () method has become static, everything is fine.
Now let's go to the Java code, “behind-the-clean-up-it” it, and everything looks like in Kotlin, except for the semicolons and the word new - well, you will not get rid of them.
Next step: we see this dubbed getHasPlugins getter with two prefixes at once. Of course, I am not a great expert in English, but it seems to me that something else was meant here. Why it happens?
As they know, who tightly communicated with Kotlin, for the names of getters and setters are generated according to JavaBeans rules. This means that, in general, getters will be with get prefixes, setters with set prefixes. But there is one exception: if you have a boolean field and its name has the prefix is, then the getter will be prefixed with is. This can be seen on the example of the aforementioned isInited field.
Unfortunately, not always Boolean fields should be called with is. isPlugins would not quite satisfy what we want to semantically show by name. How can we be?
And it is easy for us to have our own abstract for this (as you already understood, I will often repeat this today). The @JvmName annotation allows you to specify any name we want (naturally, supported by Java). Add it:
Let's check out what we got in Java: the getHasPlugins method is no longer there, but hasPlugins is quite there. This solved our problem, again, with one annotation. Now let's solve all the annotations!
As you can see, here we hung the annotation directly on the getter. What is the reason? With the fact that there is a lot of things under the profile, and it is not clear to what @JvmName applies. If you transfer the annotation to val hasPlugins itself, the compiler does not understand what to apply it to.
However, Kotlin has the ability to specify the location of the application of the annotation directly in it. You can specify the goal of the getter, the entire file, the parameter, the delegate, the field, the property, the receiver extension functions, the setter, and the parameter of the setter. In our case, the getter is interesting. And if you do this, it will be the same effect as when we annotated get:
Accordingly, if you do not have a custom getter, then you can hang directly on your property, and everything will be OK.
The next point that confuses us a bit is “Analytics.INSTANCE.getEMPTY_PLUGIN ()”. It's not even English, but simply: WHY? The answer is about the same, but at first a small introduction.
In order to make a field constant, you have two options. If you define a constant as a primitive type or as a String, and it is also inside an object, then you can use the keyword const, and then no getter setters and other things will be generated. This will be a regular constant — private final static — and it will be inline, that is, an absolutely regular Java thing.
But if you want to make a constant from an object that is different from the string, then you will not be able to use the word const for this. Here we have val EMPTY_PLUGIN = EmptyPlugin (), according to it, that terrible getter was obviously generated. We can rename the @JvmName annotation, remove this get prefix, but still it will remain a method with parentheses. So, the old solutions will not work, we are looking for new ones.
And here for this is the abstract @JvmField, which says: “I don’t want getters here, I don’t want setters, make me a field”. Put it in front of val EMPTY_PLUGIN and check that this is all true.
Kotlin Bytecode Viewer shows highlighted the piece on which you are now standing in the file. We are now standing at EMPTY_PLUGIN, and you see that there is some kind of initialization written in the constructor. The fact is that the getter is no more and access to it is only recorded. And if you click decompile, we see that “public static final EmptyPlugin EMPTY_PLUGIN” has appeared, this is exactly what we wanted. Nice We check that everyone is happy, in particular, the compiler. The most important person you need to appease is the compiler.
Generics
Let's tear off a bit from the code and look at generics. This is quite a hot topic. Or slippery, someone that no longer like. Java has its difficulties, but Kotlin is different. First of all, we are concerned about the variation. What it is?
Variance is a way to transfer information about a type hierarchy from basic types to derivatives, for example, to containers or generics. Here we have the classes Animal and Dog with a quite obvious connection: Dog is a subtype, Animal is a supertype, the arrow comes from the subtype.
And what connection will their derivatives have? Let's look at some cases.
The first is Iterator. To determine what is a supertype, and what is a subtype, we will be guided by the Barbara Liskov substitution rule. It can be formulated as follows: “the subtype should require no more, and provide no less.”
In our situation, the only thing Iterator does is give us typed objects, for example, Animal. If we take Iterator somewhere, we can easily put Iterator into it, and we get Animal from the next () method, because the dog is also Animal. We provide not less, but more, because the dog is a subtype.
I repeat: we only read from this type, therefore the dependence between type and subtype is preserved here. And such types are called covariant.
Another case: Action. Action is a function that returns nothing, takes one parameter, and we only write to Action, that is, it takes a dog or an animal from us.
Thus, here we no longer provide, but demand, and we must demand no more. This means that our dependence is changing. "No more" we have Animal (Animal less than a dog). And such types are called contravariant.
There is also a third case - for example, ArrayList, from which we both read and write. Therefore, in this case we are breaking one of the rules, we demand more for the recording (dog, not animal). Such types are not related in any way by relation, and they are called invariant.
So, in Java, when it was designed before version 1.5 (where the generics appeared), arrays were made covariant by default. This means that you can assign an array of objects to an array of objects, then transfer it somewhere to a method where you need an array of objects, and try to stuff an object there, although this is an array of strings. Everything will fall for you.
Having learned from bitter experience that it is impossible to do this, when designing generics, they decided “we will make collections invariant, we will not do anything with them”.
And in the end it turns out that in such a seemingly obvious thing everything should be ok, but in fact not ok:
But we need to somehow determine that we can still: if we only read from this sheet, why not make it possible to transfer a sheet of dogs here? Therefore, it is possible with the help of a wildcard to describe what kind of variation this type will have:
As you can see, this variation is indicated at the place of use, where we assign dogs. Therefore, it is called use-site variance.
What negative side does it have? The negative side is that you must, wherever you use your API, specify these terrible wildcards, and this is all very fruitful in the code. But for some reason, in Kotlin such a thing works out of the box, and there is no need to indicate anything:
val dogs: List<Dog> = ArrayList() val animals: List<Animal> = dogs
What is the reason? With the fact that the sheets are actually different. List in Java implies writing, and in Kotlin it is read-only, not implying. Therefore, in principle, we can immediately say that we only read from here, so we can be covariant. And it is specified in the type declaration with the out keyword, replacing the wildcard:
interfaceList<out E> : Collection<E>
This is called a declaration-site variance. Thus, we indicated everything in one place, and where we use it, we no longer touch on this topic. And this is nishtyak.
Back to code
Let's go back to our depths. Here we have the addPlugins method, it takes a List:
Due to the fact that the List in Kotlin is covariant, we can transfer here a sheet of the heirs of the plugin without any problems. Everything works, the compiler does not mind. But due to the fact that we have a Declaration-site variance, where we have specified everything, we cannot then control the connection with Java at the use stage. And what will happen if we really want the Plugin list there, do not want any heirs there? There are no modifiers for this, but is that? That's right, there is a summary. And the abstract is called @JvmSuppressWildcards, that is, by default we consider that here is a type with a wildcard, a covariant type.
Speaking of SuppressWildcards, we suppress all these questions, and our signature actually changes. Moreover, I will show how everything looks in bytecode:
I will delete while from the code the summary. Here is our method. You probably know that there is a type erasure. And in your bytecode there is no information about what kind of questions there were, well, in general, generics. But the compiler follows this and signs it in the comments to the bytecode: which is the type with us.
Now we will insert the annotation again and see that this is our type without questions.
Now our previous code will stop compiling precisely because we have cut off the Wildcards. You can see for yourself.
We deconstructed covariant types. Now the opposite situation.
We think that we have a list with a question. , getPlugins, . What does it mean? , , , . , Java.
final List<Plugin> plugins = Analytics.getPlugins(); displayPlugins(plugins); Analytics.getPlugins().add(new EmptyPlugin());
inlinefun<reified T : Any>create(): T { return create(T::class.java.java)
. Kotlin , . val api = retrofit.create(Api::class)val api = retrofit.create<Api>() , ::class . Reified-, -.
Unit. Unit, , void Java, . . , . - Scala, Scala , - , , , void.
And in Kotlin this is not. Kotlin has only 22 interfaces that accept a different set of parameters and return something. Thus, the lambda that returns Unit will return not void, but Unit. And it imposes its limitations. What does the lambda that returns Unit look like? Here, look at it in this code snippet. Let's get acquainted.