Home > Articles

  • Print
  • + Share This
This chapter is from the book

This chapter is from the book

Using Kotlin Code from Java

The other way around, Kotlin code can also be called from Java. The best way to understand how to access certain elements, such as extension functions or top-level declarations, is to explore how Kotlin translates to Java bytecode. An additional benefit of this is that you get more insights into the inner workings of Kotlin.

Accessing Properties

Before diving into the details, remember that when I say “field,” I’m usually referring to Java (unless I’m explicitly referring to Kotlin’s backing fields). Contrarily, when talking about “properties,” I’m referring to Kotlin because they don’t directly exist in Java.

As you know, you don’t need to implement property getters and setters manually in Kotlin. They’re created automatically and can be accessed from Java as you would expect, as shown in Listing 5.10.

Listing 5.10 Calling Getters and Setters

// Kotlin
class KotlinClass {
  val fixed: String = "base.KotlinClass"
  var mutable: Boolean = false
}

// Java
KotlinClass kotlinClass = new KotlinClass();
String s = kotlinClass.getFixed();     // Uses getter of ‘val’
kotlinClass.setMutable(true);           // Uses setter of ‘var’
boolean b = kotlinClass.getMutable();  // Uses getter of ‘var’

Notice that Boolean getters also use the prefix get by default instead of is or has. However, if the property name itself starts with is, the property name is used as the getter name—and not just for Boolean expressions; this is irrespective of the property type. Thus, calling the Boolean property isMutable instead would result in the getter of the same name isMutable. Again, there’s currently no such mechanism for properties starting with has but you can always define your own JVM name by annotating the getter or setter with @JvmName, as in Listing 5.11.

Listing 5.11 Custom Method Name Using @JvmName

// Kotlin
class KotlinClass {
  var mutable: Boolean = false
    @JvmName("isMutable") get         // Specifies custom getter name for Java bytecode
}

// Java
boolean b = kotlinClass.isMutable();  // Now getter is accessible as ‘isMutable’

Exposing Properties as Fields

As you’ve learned, properties are compiled to a private field with getter and setter by default (this is also the case for file-level properties). However, you can use @JvmField to expose a property directly as a field in Java, meaning the field inherits the visibility of the Kotlin property and no getter or setter is generated. Listing 5.12 demonstrates the difference.

Listing 5.12 Exposing a Property as a Field using @JvmField

// Kotlin
val prop = "Default: private field + getter/setter" // Here no setter because read-only

@JvmField
val exposed = "Exposed as a field in Java"          // No getter or setter generated

// Decompiled Java code (surrounding class omitted)
@NotNull private static final String prop = "Default: private field + getter/setter";
@NotNull public static final String getProp() { return prop; }

@NotNull public static final String exposed = "Exposed as a field in Java";

As you can see, the property annotated with @JvmField is compiled to a public field that can be accessed directly in Java. This works the exact same way for properties inside a class; by default, they are also accessible via getters and setters but can be exposed as fields.

Note that there are several restrictions for using the @JvmField annotation, and it’s a good exercise to think about why these restrictions exist.

  • The annotated property must have a backing field. Otherwise, there would be no field to expose in the Java bytecode.

  • The property cannot be private because that makes the annotation superfluous. For private properties, no getters or setters are generated anyway because there would be no value to them.

  • The property cannot have the const modifier. Such a property becomes a static final field with the property’s visibility anyway, so @JvmField would have no effect.

  • It cannot have an open or override modifier. This way, field visibilities and existence of getters and setters is consistent between superclass and subclasses. Otherwise, you could accidentally hide the superclass field in Java with a field that has a more restrictive visibility. This can lead to unexpected behavior and is a bad practice.

  • A lateinit property is always exposed so that it can be initialized from anywhere it’s accessible, without assumptions about how it’s initialized. This is useful when an external framework initializes the property. @JvmField would be superfluous here as well.

  • It cannot be a delegated property. Delegation only works with getters and setters that can be routed to the delegate’s getValue and setValue methods, respectively.

Using File-Level Declarations

In Kotlin, you can declare properties and functions on the file level. Java doesn’t support this, so these are compiled to a class that contains the properties and functions. Let’s say you have a Kotlin file sampleName.kt in a package com.example as in Listing 5.13.

Listing 5.13 File-Level Declarations in Kotlin

// sampleName.kt
package com.example

class FileLevelClass                        // Generates class FileLevelClass
object FileLevelObject                      // Generates FileLevelObject as Singleton
fun fileLevelFunction() {}                  // Goes into generated class SampleNameKt
val fileLevelVariable = "Usable from Java"  // Goes into generated class SampleNameKt

Note that classes and objects are also file-level declarations but are simply compiled to a corresponding Java class as you would expect (classes generated for objects implement the singleton pattern). The concepts of file-level properties and functions cannot be mapped to Java directly. So for the code in Listing 5.13, the compiler generates not only the classes com.example.FileLevelClass and com.example.FileLevelObject but also a class com.example.SampleNameKt with a static field for the file-level property and a static method for the file-level function. The fields are private with a public static getter (and setter).

You can explore all classes in the decompiled Java code. The static members can be called from Java as usual after importing SampleNameKt or statically importing the members.

You can also give a shorter and more meaningful name to the generated class than the one based on the file name. This is done in Kotlin using @file:JvmName("<YOUR_NAME>") to annotate the entire file, as shown in Listing 5.14.

Listing 5.14 Using @JvmName on File Level

// sampleName.kt
@file:JvmName("MyUtils")  // Must be the first statement in the file 
// ...

This way, the name of the generated class is MyUtils. You can even compile multiple Kotlin files into a single Java class by adding @file:JvmMultifileClass and @file:JvmName with the same name given to all of them. Be aware that using this increases the chance of name clashes and should only be used if all incorporated file-level declarations are closely related; this is questionable because they come from separate Kotlin files, so use this judiciously.

There are several other annotations that allow you to adjust some parameters of how your Kotlin code is mapped to Java. All of these consistently use the @Jvm prefix, and most will be discussed in this chapter.

Calling Extensions

Extension functions and properties are typically declared on the file level and can then be called as a method on the generated class like other top-level declarations. In contrast to Kotlin, they cannot be called on the extension receiver type directly because there is no such feature in Java. Listing 5.15 provides a brief example.

Listing 5.15 Calling Top-Level Extensions

// Kotlin
@file:JvmName("Notifications")

fun Context.toast(message: String) {  // Top-level function => becomes static method
    Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
}

// Within a Java Activity (Android)
Notifications.toast(this, "Quick info...");

Here, you define an extension function that facilitates showing toast messages on Android and use @JvmName to provide a more descriptive name for the generated class. From Kotlin, you could simply call this extension method as toast("Quick info…") from inside an activity because every activity is itself a context, and Context is the receiver type for this extension. With a static import, you can achieve a similar syntax in Java but the Context must still be passed in as the first argument.

Similarly, you can declare extensions inside a type declaration such as a class or object. These can be called from Java via an instance of that encompassing type. To highlight the difference, Listing 5.16 defines a similar extension but this time inside a Notifier class.

Listing 5.16 Calling Class-Local Extensions

// Kotlin
class Notifier {
    fun Context.longToast(message: String) {  // Becomes member method
        Toast.makeText(this, message, Toast.LENGTH_LONG).show()
    }
}

// Calling from a Kotlin Android activity
with(Notifier()) { longToast("Important notification...") }
Notifier().apply { longToast("Important notification...") }

// Calling from a Java Android Activity
Notifier notifier = new Notifier();
notifier.longToast(this, "Important notification...");  // ‘this’ is the Context arg

To call a class-local extension from outside its containing class, you need an instance of the containing class, irrespective of whether you want to call it from Kotlin or Java. From Java, you can call it as a static method on the instance and must again pass in the context. From Kotlin, you have to get access to the class scope, which you can do via with or apply. Recall that, inside the lambda expression, you can then write code as if you were inside the Notifier class. In particular, you can call longToast.

Accessing Static Members

Several of Kotlin’s language elements compile down to static fields or methods. These can be called directly on their containing class as usual.

Static Fields

Although there’s no static keyword in Kotlin, there are several language elements in Kotlin that will generate static fields in the Java bytecode and can thus be called as such from Java. Apart from the examples already seen (top-level declarations and extensions), static fields may be generated from:

  • Properties in object declarations

  • Properties in companion objects

  • Constant properties

All these are shown in Listing 5.17.

Listing 5.17 Generating Static Fields on the JVM

// Kotlin
const val CONSTANT = 360

object Cache { val obj = "Expensive object here..." }

class Car {
  companion object Factory { val defaultCar = Car() }
}

// Decompiled Java code (simplified)
public final class StaticFieldsKt {
  public static final int CONSTANT = 360;
}

public final class Cache {  // Simplified
  private static final String obj = "Expensive object here..."; // private static field
  public final String getObj() { return obj; }                  // with getter
}

public final class Car {    // Simplified
  private static final Car defaultCar = new Car();              // Static field
  public static final class Factory {
    public final Car getDefaultCar() { return Car.defaultCar; } // Nonstatic method
  }
}

Properties from object declarations and companion objects produce private fields with getters and setters. For object declarations, these are inside the Java class that corresponds to the object, such as Cache. For companion objects, the field itself lives inside the containing class while the getter and setter stay inside the nested class. Static members are accessed as usual from Java, for instance as Cache.INSTANCE.getObj() or Car.Factory.getDefaultCar().

The generated static fields are private by default (except when using const) but they can again be exposed using @JvmField. Alternately, you could use lateinit or const; as you learned, these also expose the field. However, it’s not their main purpose but rather a side effect. Note that, using @JvmField, the static field gets the visibility of the property itself whereas using lateinit, it gets the visibility of the setter.

Static Methods

While top-level functions become static methods in the Java bytecode by default, methods declared in named and companion objects are not static by default. You can change this using the @JvmStatic annotation as shown in Listing 5.18.

Listing 5.18 Using @JvmStatic to Generate Static Methods

// Kotlin
object Cache {
  @JvmStatic fun cache(key: String, obj: Any) { … }  // Becomes a static member
}

class Car {
  companion object Factory {
    @JvmStatic fun produceCar() { … }                  // Now becomes static as well
  }
}

// Inside a Java method
Cache.cache("supercar", new Car());     // Static member is callable directly on class
Cache.INSTANCE.cache("car", new Car());  // Bad practice

Car.produceCar();                        // Static
Car.Factory.produceCar();                // Also possible 
(new Car()).produceCar();                // Bad practice

In named objects (object declarations), using @JvmStatic allows you to call the method directly on the class, as you’re used to for static methods. Also, it’s unfortunately possible to call static methods on instances as well but you should avoid this because it can lead to confusing code and adds no value. Nonstatic methods of named objects can only be called on instances, as usual.

In companion objects, using @JvmStatic allows you to call the method directly on the enclosing class; here, Car. It can still be called explicitly on the nested companion object as well. Nonstatic methods can only be called as instance methods on the companion object; here, Car.Factory. Again, the static method on an instance should be avoided.

You can also use @JvmStatic on named and companion object properties to make their accessors static. But you cannot use @JvmStatic outside of named and companion objects.

Generating Method Overloads

Method overloading can often be avoided in Kotlin using default parameter values, saving many lines of code. When compiling to Java, you can decide whether overloaded methods should be generated to enable optional parameters in Java to some extent, as in Listing 5.19.

Listing 5.19 Generating Overloads with @JvmOverloads

// Kotlin
@JvmOverloads  // Triggers generation of overloaded methods in Java bytecode
fun <T> Array<T>.join(delimiter: String = ", ",
                      prefix: String = "",
                      suffix: String = ""): String {
  return this.joinToString(delimiter, prefix, suffix)
}

// Java
String[] languages = new String[] {"Kotlin", "Scala", "Java", "Groovy"};

// Without @JvmOverloads: you must pass in all parameters
ArrayUtils.join(languages, ";", "{", "}");    // Assumes @file:JvmName("ArrayUtils")

// With @JvmOverloads: overloaded methods
ArrayUtils.join(languages);                   // Skips all optional parameters
ArrayUtils.join(languages, "; ");             // Skips prefix and suffix
ArrayUtils.join(languages, "; ", "Array: ");  // Skips suffix
ArrayUtils.join(languages, "; ", "[", "]");   // Passes in all possible arguments

Using @JvmOverloads, the compiler generates one additional overloaded method for each parameter with default value. This results in a series of methods where each has one fewer parameters, the optional one. Naturally, parameters without default value are never omitted. This increases flexibility when calling the method from Java but still doesn’t allow as many combinations as Kotlin because the order of parameters is fixed and you cannot use named parameters. For instance, you cannot pass in a suffix without passing in a prefix.

Note that you can also use @JvmOverloads on constructors to generate overloads. Also, if all parameters of a constructor have a default value, a parameterless constructor is generated anyway, even without the annotation. This is done to support frameworks that rely on parameterless constructors.

Using Sealed and Data Classes

Both sealed classes and data classes translate to normal classes in Java and can be used as such. Of course, Java does not treat sealed classes specially, so they cannot be used with Java’s switch as they can with Kotlin’s when. For instance, you must use instanceof checks to determine the specific type of an object of the parent sealed class, as shown in Listing 5.20.

Listing 5.20 Working with Sealed Classes

// Kotlin
sealed class Component
data class Composite(val children: List<Component>) : Component()
data class Leaf(val value: Int): Component()

// Java
Component comp = new Composite(asList(new Leaf(1), new Composite(…)));

if (comp instanceof Composite) {       // Cannot use ‘switch’, must use ‘if’
  out.println("It's a Composite");  // No smart-casts
} else if (comp instanceof Leaf) {      // No exhaustiveness inferred
  out.println("It's a Leaf");
}

The sealed class becomes an abstract class, making it impossible to instantiate an object of it. Its child classes can be used as normal classes but carry no special semantics in Java because there is no concept for sealed classes.

Data classes can be used intuitively from Java, but there are two restrictions to keep in mind. First, Java does not support destructuring declarations so that the componentN functions are unnecessary. Second, there are no overloads generated for the copy method so that it has no benefit compared to using the constructor. This is because generating all possible overloads would introduce an exponential number of methods (with respect to the number of parameters). Also, generating only some overloads as is the case for @JvmOverloads, there is no guarantee that this would generate a useful subset of overloads. Hence, no overloads are generated at all for copy. All of this is illustrated in Listing 5.21.

Listing 5.21 Working with Data Classes

// Kotlin
data class Person(val name: String = "", val alive: Boolean = true)

// Java
Person p1 = new Person("Peter", true);
Person p2 = new Person("Marie Curie", false);
Person p3 = p2.copy("Marie Curie", false);  // No advantage over constructor

String name = p1.getName();  // componentN() methods superfluous
out.println(p1);             // Calls toString()
p2.equals(p3);               // true

Visibilities

The available visibilities don’t map exactly between Kotlin and Java, plus you have top-level declarations in Kotlin. So let’s explore how visibilities are mapped to Java. First, some visibilities can be mapped trivially.

  • Private members remain private.

  • Protected members remain protected.

  • Public language elements remain public, whether top-level or member.

Other visibilities cannot be mapped directly but are rather compiled to the closest match:

  • Private top-level declarations also remain private. However, to allow calls to them from the same Kotlin file (which may be a different class in Java), synthetic methods are generated on the JVM. Such methods cannot be called directly but are generated to forward calls that would not be possible otherwise.

  • All of Kotlin’s internal declarations become public because package-private would be too restrictive. Those declared inside a class go through name mangling to avoid accidental calls from Java. For instance, an internal method C.foo will appear as c.foo$production_sources_for_module_yourmodulename() in the bytecode, but you’re not able to actually call them as such. You can use @JvmName to change the name in the Java bytecode if you want to be able to call internal members from Java.

This explains how each visibility maps to Java for both top-level declarations and members.

Getting a KClass

KClass is Kotlin’s representation of classes and provides reflection capabilities. In case you have a Kotlin function accepting a KClass as a parameter and need to call it from Java, you can use the predefined class kotlin.jvm.JvmClassMappingKt as in Listing 5.22. From Kotlin, you can access both KClass and Class more easily.

Listing 5.22 Getting KClass and Class References

// Java
import kotlin.jvm.JvmClassMappingKt;
import kotlin.reflect.KClass;

KClass<A> clazz = JvmClassMappingKt.getKotlinClass(A.class);

// Kotlin
import kotlin.reflect.KClass

private val kclass: KClass<A> = A::class
private val jclass: Class<A> = A::class.java

Handling Signature Clashes

With Kotlin, you may declare methods that have the same JVM signature. This mostly happens due to type erasure of generics types, meaning that type parameter information is not available at runtime on the JVM. Thus, at runtime, Kotlin (like Java) only knows that List<A> and List<B> have type List. Listing 5.23 demonstrates the situation.

Listing 5.23 JVM Name Clash

fun List<Customer>.validate() {}    // JVM signature: validate(java.util.List)
fun List<CreditCard>.validate() {}  // JVM signature: validate(java.util.List)

Here, you wouldn’t be able to call these methods from Java because there’s no way to differentiate between the two at runtime. In other words, they end up with the same bytecode signature. As you may expect, this is easily resolved using @JvmName, as in Listing 5.24.

Listing 5.24 Resolving JVM Name Clashes

fun List<Customer>.validate() { … }

@JvmName("validateCC")  // Resolves the name clash
fun List<CreditCard>.validate() { … }

// Both can be called as validate() from Kotlin (because dispatched at compile-time)
val customers = listOf(Customer())
val ccs       = listOf(CreditCard())
customers.validate()
ccs.validate()

From Java, the methods are available as two static methods, FileNameKt.validate and FileNameKt.validateCC. From Kotlin, you can access both as validate because the compiler dispatches that to the appropriate method internally (at compile time, all necessary type information for this is available).

Using Inline Functions

You can call inline functions from Java just like any other function, but of course they are not actually inlined—there is no such feature in Java. Listing 5.25 demonstrates inlining when used from Kotlin. Be aware that inline functions with reified type parameters are not callable from Java at all because it doesn’t support inlining, and reification without inlining doesn’t work. Thus, you cannot use reified type parameters in methods that should be usable from Java.

Listing 5.25 Calling Inline Functions (from Kotlin)

// Kotlin
inline fun require(predicate: Boolean, message: () -> String) {
  if (!predicate) println(message())
}

fun main(args: Array<String>) {  // Listing uses main function to show decompiled code
  require(someCondition()) { "someCondition must be true" }
}

// Decompiled Java Code (of main function)
public static final void main(@NotNull String[] args) {
  Intrinsics.checkParameterIsNotNull(args, "args");  // Always generated by Kotlin
  boolean predicate$iv = someCondition();
  if (!predicate$iv) {                               // Inlined function call
    String var2 = "someCondition must be true";
    System.out.println(var2);
  }
}

As you can see, the if statement and its body are inlined into the main method and there is no actual call to the require method anymore. Without the inline keyword, or when calling the method from Java, there would be a call to require instead.

Exception Handling

Because there are no checked exceptions in Kotlin, you can call all Kotlin methods from Java without handling exceptions. This is because exceptions are then not declared in the bytecode (there are no throws clauses). To allow exception handling in Java, you can use the @Throws annotation as shown in Listing 5.26.

Listing 5.26 Generating Throws Clauses

// Kotlin
import java.io.*

@Throws(FileNotFoundException::class)  // Generates throws clause in bytecode
fun readInput() = File("input.csv").readText()

// Java
import java.io.FileNotFoundException;
// …
try {                     // Must handle exception
  CsvUtils.readInput();  // Assumes @file:JvmName("CsvUtils")
} catch (FileNotFoundException e) {
  // Handle non-existing file...
}

Without the @Throws annotation, you could call the readInput method from both Kotlin and Java without handling exceptions. With the annotation, you’re free to handle exceptions when calling it from Kotlin, and you must handle all checked exceptions when calling it from Java.

Looking at the decompiled Java code, you can see that all the annotation does is to add a throws FileNotFoundException to the method signature, as demonstrated in Listing 5.27.

Listing 5.27 Difference in Decompiled Java Code

// Without @Throws
public static final String readInput() { ... }

// With @Throws
public static final String readInput() throws FileNotFoundException { ... }

Using Variant Types

Regarding variant types, there’s a disparity between Kotlin and Java because Java only has use-site variance whereas Kotlin also has declaration-site variance. Thus, Kotlin’s declaration-site variance must be mapped to use-site variance. How is this done? Whenever an out-projected type appears as a parameter or variable type, the wildcard type <? extends T> is automatically generated. Conversely, for in-projected types that appear as a parameter, <? super T> is generated. This lets you use the type’s variance in Java. Consider Listing 5.28 as an example.

Listing 5.28 Mapping Declaration-Site Variance to Use Site

// Kotlin
class Stack<out E>(vararg items: E) { … }
fun consumeStack(stack: Stack<Number>) { … }

// Java: you can use the covariance of Stack
consumeStack(new Stack<Number>(4, 8, 15, 16, 23, 42));
consumeStack(new Stack<Integer>(4, 8, 15, 16, 23, 42));

The Java signature for the consumeStack method is void consumeStack(Stack<? extends Number> stack) so that you can call it with a Stack<Number> as well as a Stack<Integer>. You cannot see this signature in the decompiled Java code due to type erasure, but looking at the Kotlin bytecode directly, you can find a comment stating declaration: void consumeStack(Stack<? extends java.lang.Number>). So you’re still able to use the bytecode tool to see what’s happening internally.

The generation of wildcard types only happens when in- or out-projected types are used as parameters. For return types, no such wildcards are generated because this would go against Java coding standards. Still, you can intercept the generation process here as well. To generate wildcards where they normally wouldn’t, you can use @JvmWildcard as in Listing 5.29. To suppress wildcards, you can use @JvmSuppressWildcards, also shown in Listing 5.29.

Listing 5.29 Adjusting Wildcard Generation

// Kotlin
fun consumeStack(stack: Stack<@JvmSuppressWildcards Number>) { … } // No more wildcards

// Normally no wildcards are generated for return types, unless you use @JvmWildcard
fun produceStack(): Stack<@JvmWildcard Number> {
  return Stack(4, 8, 15, 16, 23, 42)
}

// Java
consumeStack(new Stack<Number>(4, 8, 15, 16, 23, 42));   // Matches exactly
consumeStack(new Stack<Integer>(4, 8, 15, 16, 23, 42));  // Error: No longer allowed

Stack<Number> stack = produceStack();           // Error: No longer allowed
Stack<? extends Number> stack = produceStack();  // Inconvenient, thus bad practice

Note that you can also annotate methods or even whole classes with these to control wildcard generation. However, most of the time you’ll be able to call Kotlin methods from Java just fine without intercepting the generation process because declaration-site variance is mapped sensibly by default.

The Nothing Type

Lastly, there’s no counterpart to Kotlin’s Nothing type in Java because even java.lang.Void accepts null as a value. Because it’s still the closest representation of Nothing that’s available in Java, Nothing return types and parameters are mapped to Void. As shown in Listing 5.30, this doesn’t result in the exact same behavior.

Listing 5.30 Using the Nothing Type

// Kotlin
fun fail(message: String): Nothing {          // Indicates non-terminating function
  throw AssertionError(message)
}

fun takeNothing(perpetualMotion: Nothing) {}  // Impossible to call from Kotlin

// Java
NothingKt.takeNothing(null); // Possible in Java (but with warning due to @NonNull)
NothingKt.fail("Cannot pass null to non-null variable");    // Cannot terminate
System.out.println("Never reached but Java doesn't know");  // Dead code

You can actually call takeNothing from Java, an action not possible from Kotlin. However, you’ll at least receive a warning because the signature is void takeNothing(@NonNull Void perpetualMotion). Since null is the only valid value for Void, that’s really the closest representation of Nothing currently available in Java. Similarly, the return type of fail becomes Void so that Java cannot infer unreachable code like Kotlin.

Lastly, using Nothing as a generic type argument generates a raw type in Java to at least provoke unchecked call warnings. For instance, List<Nothing> becomes a raw List in Java.

  • + Share This
  • 🔖 Save To Your Account