The Art of Annotation: Writing Java-friendly Kotlin Code

Hello, Habr!



Today we will touch upon a crucial topic: the interoperability of Java and Kotlin . The authors of the proposed publication reasonably assume that it is not possible to rewrite the code base made in Java to Kotlin. Therefore, it is more correct to ensure the interaction of Java and Kotlin code. Read how to do this with annotations.



I think you opened this article for the following reasons:



  1. Finally decided to try Kotlin.
  2. You liked him, which, however, is not surprising.
  3. Decided to use Kotlin everywhere
  4. Faced with harsh reality: Java cannot be completely abandoned, at least with little blood.


Why?



If Kotlin is so cool, why not use it everywhere and always? Here, offhand, a couple of scenarios in which this is not possible:



  1. When you try to slowly transfer your entire code base to Kotlin, you will notice that there are files that are simply scared to use the Convert Java to Kotlin file



    command. If you have time for refactoring, do it! However, in a real project, time for refactoring is not always found.
  2. Your code will be used by programmers working with both Java and Kotlin. You cannot (or should not) force them all to use a specific language, especially if supporting both languages ​​does not require much effort from you (naturally, I'm talking about annotations).


Here we look at a few annotations that provide interoperability between Java and Kotlin!



Java Annotations



Jvmfield





How it works?



Suppose you define a field inside an object / companion object



in Kotlin:



 object Constants { val PERMISSIONS = listOf("Internet", "Location") }
      
      





If you try to call this function from Java, you have to write:



 Utils.INSTANCE.getPERMISSIONS()
      
      





A lot of code for a simple field! To make the code cleaner, let's remove the excess by adding an annotation.



 object Constants { @JvmField val PERMISSIONS = listOf("Internet", "Location") }
      
      





Now our Java code will look like this:



 Utils.PERMISSIONS;
      
      





The same can be achieved with a modifier, however, such a modifier only works with primitive types or strings.



 //Kotin object Constants { const val KEY = "test" }
      
      





 //Java String key = Constant.KEY;
      
      





When can this annotation not be used?



const



properties marked as and functions cannot be annotated with @JvmField







Jvmstatic





How it works?



Suppose you define a function in object



in Kotlin:



 object Utils { fun doSomething(){ ... } }
      
      





If you try to call this function from Java, you will have to write:



 Utils.INSTANCE.doSomething()
      
      





We have to access the INSTANCE



object whenever we want to call this function. To make the code cleaner, let's better use the @JvmStatic



annotation.



 object Utils { @JvmStatic fun doSomething(){ ... } }
      
      





Now, calling this function from Java, we will only have to write:



 Utils.doSomething();
      
      





So much better, isn't it? The situation is as if the function was originally written in Java as a static method.



Annotations can also be applied to fields:



 object Utils { @JvmStatic var values = listOf("Test 1", "Test 2") }
      
      





Calling this code from Java, you can write:



 Utils.getValues();
      
      





Note: JvmField



provides a member as a field, but with JvmStatic



we provide a get



function.



And since the field is var



, the set



method is also generated:



 Utils.setValues(...);
      
      





If we have a constant inside our object, then we can also declare it as static:



 object Utils { @JvmStatic val KEY = "test" }
      
      





However, in this case, using annotation is not a good idea, since invocation invocation would look like this:



 public void foo(){ String key = Utils.getKEY(); }
      
      





In this case, use the mod modifier or JvmField, as explained above.



When can’t it be used?



A member cannot be annotated with JvmStatic when it is followed by an open



, override



or const



modifier.



In this situation, the code does not compile:







JvmOverloads





How it works?



If you have a class with a constructor (or any other function) with default parameters ...



 class User constructor ( val name: String = "Test", val lastName: String = "Testy", val age: Int = 0 )
      
      





... then you can call such a function from Kotlin in various ways:



 val user1 = User() val user2 = User(name = "Bruno") val user3 = User(name = "Bruno", lastName = "Aybar") val user4 = User(name = "Bruno", lastName = "Aybar", age = 21) val user5 = User(lastName = "Aybar") val user6 = User(lastName = "Aybar", age = 21) val user7 = User(age = 21) val user8 = User(age = 21, name = "Bruno") ...
      
      





However, if you try to call the constructor from Java, you will have only two options: 1) pass all parameters or 2) only if ALL of your parameters have default values, you can not pass any parameters at all.



If we want to create overloads, we can use the JvmOverloads



annotation:



 class User @JvmOverloads constructor ( val name: String = "Test", val lastName: String = "Testy", val age: Int = 0 )
      
      





Now when using Java, we have many opportunities:







However, in Kotlin there are not many options in this case. For example, we will not be able to pass only the last name or only age.



Annotation JvmOverloads



only generate as many overloads as the default parameter function has.





file : JvmName





How does she work?



In Kotlin, where functions are privileged elements, you can write functions that exist outside the class. For example, if you create a new Kotlin file and write the following code, then it compiles without problems:



 //file name: Utils.kt fun doSomething() { ... }
      
      





You can call this code from Java:



 UtilsKt.doSomething();
      
      





Please note: although the file is called Utils, the call uses the name UtilsKt



, which is not ideal. To fix this, let's add a JvmName



annotation on top of the file.



 //  : Utils.kt @file:JvmName("Utils") fun doSomething() { ... }
      
      





Notice how the file:



prefix is ​​used. You probably guessed it: it indicates that the annotation we are using is applied at the file level. If you call the following code from Java:



 Utils.doSomething();
      
      





You can also annotate functions:



 //  : Utils.kt @file:JvmName("Utils") @JvmName("doSomethingElse") fun doSomething() { ... }
      
      





When calling this code from Kotlin, we will still use the original name ( doSomething



), but in Java we use the name specified in the annotation:



 //Java Utils.doSomethingElse(); //Kotlin Utils.doSomething()
      
      





This feature does not seem particularly useful, however, it can be used to resolve signature conflicts. This script is well understood in the official documentation .



Here you can work with methods for accessing properties:



 class User { val likesKotlin: Boolean = true @JvmName("likesKotlin") get() = field }
      
      





See how this call will look in Java with and without annotation:



 //   new User().getLikesKotlin() //   new User().likesKotlin()
      
      





The same can be achieved with the get



prefix.



 class User { @get:JvmName("likesKotlin") val likesKotlin = true }
      
      







I hope you find this annotation overview useful, helping you write code in Kotlin that is easy to use with Java.



All Articles