[In-depth kotlin] – Interoperate with Java: java calls kotlin

Java calls kotlin

attribute

After compiling a property of the Kotlin class, 3 types of Java objects will be generated:

  • get method, getXxx
  • set method, setXxx
  • private field, the field name exactly matches the kotlin property name.
class Test {
var name: String = ""
}

If the kotlin property name starts with is (Bool type), get method name = property name, set method name is to replace is with set, field name = property name.

@JvmField annotation

When a kotlin property is modified with this annotation, the property becomes an instance variable (public), and the kotlin compiler will not generate get/set methods. Access directly through the field name in java.

Top level object

Kotlin top-level objects will be compiled into static members and static methods. The class name is filename+Kt. Suppose there is a MyClass.kt file:

fun test() {
println("test method")
}
var name:String = "zhangsan"

In java:

public static void main(String[] args) {
  MyClassKt.setName("Joy");
  MyClassKt.test();// print: test method
  System.out.println(MyClassKt.getName());// print: Joy
}

But MyClassKt cannot be instantiated directly (runtime exception). Because there is no constructor without parameters (and no other form of constructor) in the generated bytecode.

Modify the default name

A kotlin source file will compile a java class by default, and the default name of the class is file name+Kt. We can use the @JvmName annotation on the first line of the file to modify the default naming method:

@file: JvmName("HelloWorld")
fun myPrint() {
println("myPrint")
}
var name:String = "zhangsan"

But under the same package, JvmName cannot be repeated. If you want to name multiple files with the same class name, you need to use the JvmMutilfierClass annotation (required for each file):

@file: JvmName("HelloWorld")
@file: JvmMultifileClass
fun myPrint() {
println("myPrint")
}
var name:String = "zhangsan"

The resulting bytecode will be the result of merging all files.

Companion object

Assuming a companion object is defined:

class People {
companion object {
var name = "Joe"
    @JvmField // @JvmField modifies the properties of the companion object, which will turn the properties of the companion object into the public fields of People
var age = 20
}
}

How to access in java:

System.out.println(People.Companion.getName());// Access companion objects through Companion
System.out.println(People.age);// Access the JvmField property of the modified companion object

In kotlin, you can use the method in the companion object:

class MyClass {
companion object {
fun test1(){
println("test1")
}
fun test2(){
println("test2")
}
}
}

Calling methods of companion objects in java:

MyClass. Companion. test1();
MyClass. Companion. test2();

Companion is actually the inner class MyClass$Companion of MyClass. These two methods are located in Companion. This behavior can be changed through annotations:

 @JvmStatic // @JvmStatic will generate static method test2 in MyClass and instance method test2 in Companion inner class
fun test2(){
println("test2")
}

This allows the test2 method to be called in two ways in Java:

MyClass. Companion. test2();
MyClass. test2();

@JvmName annotation

This annotation can be used to resolve naming conflicts in kotlin at the bytecode level.

Naming conflicts for extension functions

Assume that the following extension function is defined:

fun List.myFilter(): List {
return listOf("hello","kotlin")
}
fun List.myFileter(): List {
return listOf(1,2)
}

The above code compiler reports an error saying: Two extension functions have the same JVM method signature: myFilter(Ljava/util/List;) Ljava/util/List;. you mayIt’s strange that after the myFilter method is compiled into bytecode, why does it have an extra parameter of type List? That’s because when the extension function is compiled into bytecode, it will use the extended class as the first hidden parameter of the method.

Secondly, Java’s generic type is not a real generic type, and the generic type information is not stored in the bytecode (type erasure). All generic types do a forced downcast in the bytecode. For example, List is unified into List in the bytecode, but when accessing the elements in it, an additional conversion will be performed String operations.

In this case, the signatures of the two myFilters are obviously identical, and there is a naming conflict between them. If you must let the two coexist, you need to use a JvmName annotation:

fun List.myFilter(): List {
return listOf("hello","kotlin")
}
@JvmName("myFilter2")
fun List.myFilter(): List {
return listOf(1,2)
}

In this way, the second function name will be modified to myFilter2 when the bytecode is compiled, thus eliminating the naming conflict. At the same time, calling in Java will also become:

public static void main(String[] args) {
List list = HelloKotlinKt. myFilter(new ArrayList()); // 1
  List list2 = HelloKitlinKt.myFilter2(new ArrayList()); // 2
}
  1. There is no concept of extension in Java, so myFilter() is called with a static method, because this function is defined at the package level (top-level object) of the HelloKitlin.kt file. In addition, as mentioned before, the first parameter of this function is a hidden parameter, and the parameter type is the extended class, so a List object needs to be passed to it (List is an interface and cannot be instantiated, so ArrayList is used instead).
  2. As for the second extension method, it can only be called with myFilter2, otherwise Java will not recognize it.

It would be much simpler if called directly in Kotlin:

val list = listOf() // 1
println(list. myFilter())
val list2 = listOf() // 2
println(list2. myFilter())
  1. Call myFilter on a List
  2. Call myFilter on a List. At this time, it cannot be called with myFilter2, because the @JvmName annotation is specially used for Java and is invalid for Kotlin.
Name conflict for get/set methods

This annotation is used to modify the default get method name of the attribute when kotlin is compiled into bytecode. For example:

class MyClass {
val a: Int
get() = 20
fun getA()= 30
}

When the attribute a of MyClass is compiled into bytecode, a get method will be generated, the default name is getA(), which obviously conflicts with the existing method naming of MyClass. At this point you can use the JvmName annotation:

class MyClass {
val a: Int
@JvmName("getAProperty")
get() = 20
fun getA() = 30
}

In this way, in the bytecode, the get method of a becomes getAProperty(). Use this name to access the a property in Java:

public static void main(String[] args){
MyClass myClass = new MyClass();
  System.out.println(myClass.getAProperty()); // Output: 20
  System.out.println(myClass.getA()); // output: 30
}

Similarly, the JvmName annotation is invalid for kotlin:

val myClass = MyClass()
println(myClass. getA()) // 30
println(myClass. a) // 20

@JvmOverloads annotation

The concept of a method default value does not exist in Java. Then suppose there is a kotlin method with a default value:

class MyClassconstructor(x:Int, y: String = "abc") {
fun method(a: Int, b: String, c: Int = 2) {
println("a: $a, b: %b, c: $c")
}
}

How to call in Java?

public static void main(String[] args) {
MyClass myClass = new MyClass(1);
	
}

At this point, the compiler reports an error and cannot find the constructor MyClass(Int). Obviously, MyClass does not have a construction method with only one parameter. Although we set a default value for the second construction parameter, there is no concept of a parameter default value in Java. At this point you need to use the JvmOverloads annotation:

class MyClass @JvmOverloads constructor(x:Int, y: String = "abc") {
@JvmOverloads fun method(a:Int, b: String, c: Int = 2) {
println("a: $a, b: %b, c: $c")
}
}

In this way, the kotlin compiler will generate several overloaded methods for our construction method, such as:

MyClass(int x, String y)

MyClass(int x)

In this way, the previous new MyClass(1) will no longer report an error.

@Throw annotation

Java divides exceptions into checked exception and uncheck exception. Among them, checked exception is an exception that must be caught. Usually, these exceptions are thrown by throw statements, and the compiler will force developers to catch.

The unchecked exception is an exception that the programmer cannot predict, and no catch is required. Such as null pointer exception and memory overflow exception.

But in kotlin, there is no concept of checked exception, so kotlin and java have completely different handling methods for exceptions.

If an exception is thrown in kotlin:

class MyClass {
fun method() { // Note that unlike Java, there is no need to add throws FileNotFoundException after the method name
println("method invoked")
throw FileNotFoundException() // In java, this is a checked exception, inherited from IOException -> Exception
}
}

If kotlin calls this method, it does not enforce any catch for this exception. But in java, since the method is not declared as throws, java can still call without catch:

public void main(String[] args){
MyClass myClass = new MyClass();
myClass.method(); // java can't recognize it correctly and a checked exception will be thrown here, so the compiler will pass
}

But this will cause an exception to be thrown upwards, causing the program to crash. The normal way is to wrap the sentence of myClass.method() with try…catch. But in fact this will cause the java compiler to complain that “the method method does not throw an exception”, because the method method is a kotlin method and cannot be recognized as a throws method. To solve this problem, you need to use the @Throws annotation:

 @Throws(FileNotFoundException::class)
fun method() {
println("method invoked")
throw FileNotFoundException()
}

This will generate a throws statement for the method in the bytecode, so that java can normally try…catch:

public void main(String[] args){
MyClass myClass = new MyClass();
try{
mayClass. method();
}catch(FileNotFoundException e){
ex. printStackTrace();
}
System.out.println("....")
}

In this way, the program will not crash, and the following code can still be executed.

Optional can be empty

Because kotlin is stricter than java on type checking, for example, you cannot pass a null to a non-optional object. This poses a challenge for java to call kotlin code. Since there is no non-null check in java, it is very possible to pass a null to a kotlin method for a non-optional parameter.

In this koIn the tlin method, the str parameter is not empty:

class MyClass {
fun method(str: String) {
println("method invoked")
println(str)
}
}

Call it in Java:

public static void main(String[] args){
MyClass myClass = new MyClass();
myClass. method(null);
}

The code compiles successfully, but a crash occurs when running, and an error is reported:

IllegalArgumentException: Parameter specified as non-null is null

This shows that the JVM has checked the parameters for non-nullness before calling the method, and if it finds that null is passed for non-optional parameters, an exception will be thrown. At the same time this method will not be called.

Leave a Reply

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