Writing Java Friendly Kotlin Code

    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 hidden 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 conference Mobius Sergey Ryabov told, how to write such code on Kotlin, which will be comfortable to access 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 appeared quite often.

    Often, when you add a Kotlin project, you can see how this ...

    compile'rxbinding:x.y.x'		
    compile 'rxbinding-appcompat-v7:x.y.x' 
    compile 'rxbinding-design:x.y.x'
    compile 'autodispose:x.y.z'
    compile 'autodispose-android:x.y.z'
    compile 'autodispose-android-archcomponents:x.y.z'

    ... turns into this:

    compile'rxbinding:x.y.x'
    compile 'rxbinding-kotlin:x.y.x'
    compile 'rxbinding-appcompat-v7:x.y.x' 
    compile 'rxbinding-appcompat-v7-kotlin:x.y.x' 
    compile 'rxbinding-design:x.y.x'
    compile 'rxbinding-design-kotlin:x.y.x'
    compile 'autodispose:x.y.z'
    compile 'autodispose-kotlin:x.y.z'
    compile 'autodispose-android:x.y.z'
    compile 'autodispose-android-kotlin:x.y.z'
    compile 'autodispose-android-archcomponents:x.y.z' 
    compile 'autodispose-android-archcomponents-kotlin:x.y.z'

    The specifics of the last couple of years: the most popular libraries acquire “wrappers” so that they can be used from Kotlin more idiomatically.

    If you wrote on Kotlin, then 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, and will not need to support all these wrappers, everyone will be happy and satisfied.

    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 keywords in Kotlin, synchronized, strictfp, transient? 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 plugin.

    Let's imagine for example that we want to write a similar library, only for analytics. Also abstracted.

    object Analytics {
        funsend(event: Event) {}
        funaddPlugins(plugs: List<Plugin>) {}
        fungetPlugins(): List<Plugin> {} 
    }
    interfacePlugin{
        funinit()funsend(event: Event)funclose() 
    }
    dataclassEvent(
        val name: String,
        val context: Map<String, Any> = emptyMap() 
    )
    

    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.

    A 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 see how to brush this whole thing.

    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.

    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-runfinal List<EmptyPlugin> pluginsToSet = Arrays.asList(new LoggerPlugin("ALog"), new SegmentPlugin());
        // ...
    }
    

    Immediately, the INSTANCE cheese, 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 vicissitudes 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:

    publicfinalclassEvent{
        @NotNullprivatefinal String name;
        @NotNullprivatefinal Map context;
        @NotNullpublicfinal String getName(){ returnthis.name; }
        @NotNullpublicfinal Map getContext(){ returnthis.context; }
        publicEvent(@NotNull String name, @NotNull Map context){
          Intrinsics.checkParameterIsNotNull(name, "name");
          Intrinsics.checkParameterIsNotNull(context, "context");
          super();
          this.name = name;
          this.context = context;
        }
        // $FF: Synthetic methodpublicEvent(String var1, Map var2, int var3, DefaultConstructorMarker var4){
          if ((var3 & 2) != 0) {
          var2 = MapsKt.emptyMap();
          }
        // ...
    }
    

    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:

    @JvmOverloadspublicEvent(@NotNull String name){
        this.name, (Map)null, 2, (DefaultConstructorMarker)null);
    }
    

    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 the color purple :)



    Let's check, due to what it happens. Look at the baytkod again.

    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 we have methods, the instance methods are obvious, then, for example, with perperty, 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 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.

    publicstaticvoiduseAnalytics(){
        Analytics.send(new Event("only_name_event"));
        final Map<String, Object> props = new HashMap<>();
        props.put(USER_ID, 1235);
        props.put("my_custom_attr", true);
        Analytics.send(new Event("custom_event", props));
        boolean hasPlugins = Analytics.getHasPlugins();
        Analytics.addPlugin(Analytics.INSTANCE.getEMPTY_PLUGIN()); // dry-run// ...
    }
    

    Next step: we see this getHasPlugins getter with two prefixes at once. Of course, I am not a great connoisseur of the English language, 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 the rules of JavaBeans. 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 through 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 repeat this often today). The @JvmName annotation allows you to specify any name we want (naturally, supported by Java). Add it:

    @JvmStaticval hasPlugins
        @JvmName("hasPlugin")get() = plugins.isNotEmpty()
    

    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 an annotation directly on the getter. What is the reason? With the fact that there is a lot of things under the profile, it is not clear to what @JvmName applies. If you transfer the annotation to val hasPlugins itself, the compiler will not understand what to apply it to.

    However, in Kotlin there is also the opportunity to specify the place of application of the annotation directly in it. You can specify the goal of the getter, the entire file, a parameter, a delegate, a field, a property, a receiver extension function, a setter, and a setter parameter. In our case, the getter is interesting. And if you do like this, there will be the same effect as when we annotated get:

    @get:JvmName("hasPlugins") @JvmStaticval hasPlugins
        get() = plugins.isNotEmpty()
    

    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 a small introduction first.

    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, a completely normal 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. This means that the old solutions will not work, we are looking for new ones.

    And then for this abstract @JvmField, which says: "I do not want getters here, I do not 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 selected 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 there is no getter anymore and access to it is recorded only. 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 by any relation, and they are called invariant.



    So, in Java, when it was designed before version 1.5 (where the generics appeared), by default, arrays were made covariant. 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:

    // Java
    List<Dog> dogs = new ArrayList<>();
    List<Animal> animals = dogs;
    

    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:

    List<Dog> dogs = new ArrayList<>();
    List<? extends Animal> animals = dogs;
    

    As you can see, this variation is indicated at the place of use, where we appropriate the 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 specify 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, but in Kotlin it is read-only, not implying. Therefore, in principle, we can immediately say that we can 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:

    @JvmStaticfunaddPlugins(plugs: List<Plugin>) {
        plugs.forEach { addPlugin(it) }
    }
    А мы передаём ему, как видно, List<EmptyPlugin>, ну, просто закастили всё это дело:
    <source lang="java">
    final List<EmptyPlugin> pluginsToSet = 
        Arrays.asList(new LoggerPlugin("Alog"), new SegmentPlugin());
    

    Due to the fact that the List in Kotlin is covariant, we can easily transfer here the sheet of the heirs of the plugin. 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 annotation is called @JvmSuppressWildcards, that is, by default we assume that here is a type with a wildcard, a covariant type.

    @JvmStaticfunaddPlugins(plugs: List<@JvmSuppressWildcardsPlugin>) {
      plugs.forEach { addPlugin(it) }
    }
    

    Talking SuppressWildcards, we suppress all these questions, and our signature actually changes. Even more, I will show how everything looks in bytecode:



    Delete the annotation from the code for now. 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 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 no longer compile due to the fact that we 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. It is obvious to assume that when this sheet is returned from getPlugins, it will also be with a question. What does it mean? This means that we cannot write to it, because the type is covariant and not contravariant. Let's take a look at what's happening in Java.

    final List<Plugin> plugins = Analytics.getPlugins();
    displayPlugins(plugins);
    Analytics.getPlugins().add(new EmptyPlugin());
    

    Nobody is indignant that in the last line we write something down, which means that someone is wrong here. If we look at the baytkod, then we will be convinced of the loyalty of our suspicions. We did not hang any annotations, and the type for some reason without question.

    Surprise is based on this. Kotlin postulates itself as a pragmatic language, so when all this was designed, we collected statistics on how wildcards are generally used in Java. It turned out that, at the entrance, variation is most often allowed, that is, the types are made covariant. Well, it is useful wherever we want a List, to be able to put a list of any heir from Plugin there. But where we return, on the contrary, we want pure types: as there is a Plugin sheet, so it will be returned.

    And here we personally see an example of this. This seems a bit counterintuitive, but it generates fewer strange annotations in your code, because it is the most frequent usecase, and if you don’t use any jokes, everything will work out of the box.

    But in this case, we see that this situation is not for us, because we do not want to write something down there. And neither do we want it to be made from Java. Here in Kotlin, the List is a read-only type, and we cannot write anything there, but a client of our library came from Java and stuffed everything there — who would like it? Therefore, we are going to import this method to return a List with a wildcard. And we can make it clear how. Adding the @JvmWildcard annotation we say: generate us a type with a question, everything is quite simple. Now let's see what happens in java in this place. Java says “what are you doing?”:



    We can even lead here to the correct type List <? extends Plugin>, but it still says “what are you doing?” And, in principle, this situation suits us so far. But there is a script kiddie who says, “I saw the source code, it's the same open source, I know that there is an ArrayList, and I’ll pick you up.” And everything will work, because there really is an ArrayList and he knows what can be written there.

    ((ArrayList<Plugin>) Analytics.getPlugins()).add(new EmptyPlugin());
    

    Therefore, of course, cool annotations hang, but you still need to use defensive-copy, which is long known. Sorian, nowhere without him, if you want the script kiddies not to poach you.

    @JvmStaticfungetPlugins(): List<@JvmWildcard Plugin> = 
        plugin.toImmutableList()
    

    I will only add that the @JvmSuppressWildcard annotation can be hung both on the parameter, then only he will know about it, and on the function, and on the whole class, then its coverage area is expanded.

    It seems to be all right, with our analytics, we figured out. And now the other side with which we can approach: the plugin.

    We want to implement a plugin on the Java side. As good guys, we will report his exception:

    @Overridepublicvoidsend(@NotNull Event event)throws IOException
    

    Here you can see everything:

    interfacePlugin{
        funinit()/** @throws IOException if sending failed */funsend(event: Event)// ...
    }
    

    In Kotlin, there is no checked exception. And we say in the documentation: you can throw here. Well, we throw, throw, throw. And Java does not like for some reason. Says: "but there is no Throws for some reason in your signature, monsieur":



    And how can I add something here, right there, Kotlin? Well, you know the answer ...

    There is the @Throws annotation that does exactly that. It changes the throws part in the method signature. We say that we can throw IOExsection here:

    openclassEmptyPlugin : Plugin {@Throws(IOException::class)overridefunsend(event: Event) {}
        // ...
    }
    

    And add this case at the same time to the interface:

    interfacePlugin{
        funinit()/** @throws IOException if sending failed */@Throws(IOException::class)funsend(event: Event)// ...
    }
    

    And now what? Now our plugin, written in Java, where we have information about the exception, is happy with everything. Everything works, compiles. In principle, with annotations on this more or less everything, but there are two more nuances of how to use @JvmName. One interesting.

    We all added these annotations to make Java beautiful. And here ...

    package util
    fun List<Int>.printReversedSum() {
        println(this.foldRight(0) { it, acc -> it + acc })
    }
    @JvmName("printReversedConcatenation")fun List<String>.printReversedSum() {
        println(this.foldRight(StringBuilder()) { it, acc -> acc.append(it) })
    }
    

    Suppose, in Java, we do not care here, we remove the annotation. Opachki, now IDE shows an error in both functions. What do you think, what is the reason? Yes, without annotations they are generated with the same name, but it says here that one is on the List, the other is on the List. True, type erasure. We can even check out this case:



    You already know how I realized that all top-level functions are generated in a static context. And without this annotation, we will try to generate printReversedSum from List, and below another from List too. Because the Kotlin compiler knows about generics, but the Java bytecode does not know. Therefore, this is the only case when the annotations from the kotlin.jvm package are needed not for good and convenient Java, but for your Kotlin to assemble. We set a new name - once we work with strings, we use concatenation - and everything works well, now everything is compiled.

    And the second yuzkeys. He is connected with this. We have an extension function reverse.

    inlinefun String.reverse() =
      StringBuilder(this).reverse().toString()
    inlinefun<reified T>reversedClassName() = T::class.java.simpleName.reverse()
    inlinefun<T> Iterable<T>.forEachReversed(action: (T) -> Unit) {
      for (element inthis.reversed()) action(element)
    }
    

    This reverse is compiled into a static class method called ReverserKt.

    privatestaticvoiduseUtils(){
        System.out.println(ReverserKt.reverse("Test"));
        SumsKt.printReversedSum(asList(1, 2, 3, 4, 5));
        SumsKt.printReversedConcatenation(asList("1", "2", "3", "4", "5"));
    }
    

    This, I think, is not news to you. The nuance is that dudes using our library in Java may be suspicious of something wrong. We have leaked details of the implementation of our library on the side of the user and want to cover their tracks. How can we do this? As is already clear, the @JvmName annotation, which I am talking about now, but there is one nuance.

    To begin with, we will give it the name we want, do not burn, and it is important to say that we apply this annotation on the file, we need to rename the file.

    @file:Suppress("NOTHING_TO_INLINE")
    @file:JvmName("ReverserUtils")
    

    Now the Java compiler does not like ReverserKt, but this is expected, we replace it with ReverserUtils and everyone is happy. And such a “use case 2.1” is a frequent case when you want to collect the methods of several of your top-level files under one class, under one facade. For example, you do not want the methods of the above sums.kt to be called from SumsKt, but you want this to be all about reversing and twitching from ReverserUtils. Then we add this wonderful @JvmName annotation there, we write “ReverserUtils”, in principle, everything is OK, you can even try to compile it, but no.

    Although the environment does not warn in advance, but when trying to compile, they will tell us that "you want to generate two classes in one package with one name, ata-ta." What should be done? Add the last annotation @JvmMultifileClass in this package, which says that the contents of several files will turn into one class, that is, there will be one facade for this.

    We add in both cases "@file: JvmMultifileClass", and you can replace SumsKt with ReverserUtils, everyone is happy - trust me. With annotations finished!

    We talked with you about this package, about all the annotations. In principle, even from their names it is clear what each is used for. There are tricky cases when you need, for example, to use @JvmName even simply in Kotlin.

    Kotlin-specific


    But most likely this is not all that you would like to know. It is also important to note how to work with Kotlin-specific things.

    For example, inline functions. They are inline in Kotlin and it would seem, and will they be generally available from Java in bytecode? It turns out that everything will be fine, and the methods are actually available for Java. Although if you are writing, for example, a Kotlin-only project, this does not quite affect your dex count limit. Because in Kotlin they are not needed, but in reality they will be in bytecode.

    Further it is necessary to note Reified type parameters. Such parameters are specific to Kotlin, they are available only for inline functions, and allow you to recover such hacks that are not available in Java with reflection. Since this is a Kotlin-only thing, it is available only for Kotlin, and in Java you cannot use functions with reified, unfortunately.

    java.lang.Class. If we want to reflect a little, and our library is also for Java, then it should be supported. Let's see an example. We have such a “own Retrofit”, quickly written on the knee (I don’t understand what the guys wrote for so long):

    classRetrofitprivateconstructor(
        val baseUrl: String,
        val client: Client
    ) {
        fun<T : Any>create(service: Class<T>): T {...}
        fun<T : Any>create(service: KClass<T>): T {
          return create(service.java)
        }
    }
    

    There is a method that works with the Java class, there is a method that works with Kotlin's KClass, you do not need to do two different implementations, you can use extension-property, which take out the KClass Class, from the Class get the KClass (it is called Kotlin, in principle obviously).

    It will all work, but it is a little non-idiomatic. In the Kotlin-code, you do not pass KClass, you write using Reified-types, so it’s better to redo the method like this:

    inlinefun<reified T : Any>create(): T {
        return create(T::class.java.java)

    Everything is awesome. Now let's go to Kotlin and see how this thing is used. There val api = retrofit.create (Api :: class) turned into val api = retrofit.create <Api> () , no explicit :: class gets out. This is a typical use of Reified-functions, and everything will be super-duper.

    Unit. If your function returns a Unit, then it compiles perfectly into a function that returns void in Java, and vice versa. You can work with this interchangeably. But all this ends in the place where your lambda units begin to return units. If someone has worked with Scala, then Scala has a car and a small cart of interfaces that return some values, and the same car with a cart of interfaces that do not return anything, that is, 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.

    inlinefun<T> Iterable<T>.forEachReversed(action: (T) -> Unit) {
        for (element inthis.reversed()) action(element)
    }
    

    Using it from Kotlin: everything is good, we even use method reference, if we can, and it reads well, the eyes are not sore.

    privatefunuseMisc() {
        listOf(1, 2, 3, 4).forEachReversed(::println)
    println(reversedClassName<String>())
    }
    

    What happens in java? In Java, this canoe happens:

    privatestaticvoiduseMisc(){
        final List<Integer> list = asList(1, 2, 3, 4);
        ReverserUtils.forEachReversed(list, integer -> {
    	System.out.println(integer);
    	return Unit.INSTANCE;
      });
    

    Due to the fact that we have to return something here. It's like a Void with a capital letter, we can not just take it and score on it. We cannot use the reference method here, which return void, unfortunately. And this is probably the first thing, which really makes the eyes ache after all our manipulations with annotations. Unfortunately, you will have to return the Unit instance from here. You can null, no one needs it anyway. I mean, nobody needs the return value.

    Let's go further: Typealiases is also a rather specific thing, it's just aliases or synonyms, they are available only from Kotlin, and in Java, unfortunately, you will use what is under these aliases. Either it is a footcloth of triply nested generics, or some nested classes. Java programmers are used to living with it.

    And now interesting: visibility. Or rather, internal visibility. You probably know that there is no package private in Kotlin, if you write without any modifiers, it will be public. But there is an internal. Internal is such a tricky thing that we even look at it now. In Retrofit, we have the validate internal method.

    internalfunvalidate(): Retrofit {
        println("!!!!!! internal fun validate() was called !!!!!!")
        returnthis
    }
    

    It cannot be called from Kotlin, and this is understandable. What happens with java? Can we call validate? Perhaps it is not a secret to you that internal becomes public. If you do not believe me, believe Kotlin bytecode viewer.



    This is really public, but with such a scary signature that hints to a person that it was probably not quite so intended that such a bastard crawls into the public API. If someone has 80 character formatting done, then this method may not even fit into one line.

    In Java, we now have this:

    final Api api = retrofit
        .validate$production_sources_for_module_library_main()
        .create(Api.class);
    api.sendMessage("Hello from Java");
    }
    

    Let's try to compile this case. So, at least it won't compile, not bad. On this one could stop, but let me explain this to you. What if I do like this?

    final Api api = retrofit
        .validate$library()
        .create(Api.class);
    api.sendMessage("Hello from Java");
    }
    

    Then compiles. And the question is "Why is that?" What can I say ... MAGIC!

    Therefore, it is very important if you stuff something critical into internal, this is bad, because it will leak into your public API. And if the script kiddie is armed with the Kotlin Bytecode Viewer, it will be bad. Do not use anything very important in methods with internal visibility.

    If you want a little more joy, I recommend two things. To make it more comfortable to work with bytecode and read it, I recommend the report from Zhenya Vartanov, there is a free video , despite the fact that this is from the SkillsMatter event. Very cool.

    And quite an old seriesfrom three articles from Christoph Beils about what different Kotlin-features are turning into. Everything is cool there, something is irrelevant now, but on the whole is very intelligible. Everything is also with Kotlin bytecode viewer and all that.

    Thank!

    If you like the report, pay attention: on December 8-9, a new Mobius will take place in Moscow , and there will also be many interesting things there. Already known information about the program - on the website , and tickets can be purchased there.

    Also popular now: