Home > Articles

Kotlin Interoperability with Java

  • Print
  • + Share This

Learn the ins and outs of mixing Kotlin and Java, how their language concepts map to each other, what happens under the hood in the Kotlin compiler to aid interoperability, and how you can write code that further facilitates it.

Save 35% off the list price* of the related book or multi-format eBook (EPUB + MOBI + PDF) with discount code ARTICLE.
* See informit.com/terms

This chapter is from the book

This chapter is from the book

Synergy is better than my way or your way. It’s our way.

Stephen Covey

Interoperability has been one of Kotlin’s priorities from its inception. In this chapter, you’ll learn the ins and outs of mixing Kotlin and Java, how their language concepts map to each other, what happens under the hood in the Kotlin compiler to aid interoperability, and how you can write code that further facilitates it.

Using Java Code from Kotlin

Java code can be called from Kotlin in the way you would expect most of the time. In this section, we’ll explore the details and possible pitfalls, as well as how to write Kotlin-friendly Java code and how to improve your Java code to provide additional information for the Kotlin compiler, especially about nullability.

First, it’s important to appreciate the fact that you can easily use the Java standard library and any third-party library in a natural way. This is demonstrated in Listing 5.1.

Listing 5.1 Using Java Libraries from Kotlin

import com.google.common.math.Stats
import java.util.ArrayList

// Using the standard library
val arrayList = ArrayList<String>()  // Uses java.util.ArrayList
arrayList.addAll(arrayOf("Mercury", "Venus", "Jupiter"))
arrayList[2] = arrayList[0]          // Indexed access operator calls ‘get’ and ‘set’

// Looping as usual, ArrayList provides iterator()
for (item in arrayList) { println(item) }

// Using a third-party library (Google Guava)
val stats = Stats.of(4, 8, 15, 16, 23, 42)
println(stats.sum())  // 108.0

Concepts that differ in Java are mapped to Kotlin automatically. For instance, the indexed access operator for collections is translated to get and set calls under the hood. Also, you can still iterate anything that provides an iterator method using a for loop. This is the easiest and most prevalent use case: simply using Java from Kotlin seamlessly without even thinking about it. With this in mind, let’s now explore special cases to be able to handle mixed-language projects confidently.

Calling Getters and Setters

In Kotlin, you don’t explicitly call getSomething or setSomething but instead use the property syntax that calls the getter and setter under the hood. You want to keep it that way when using Java field accessors for consistency and brevity, and it’s possible by default. Listing 5.2 gives an example.

Listing 5.2 Calling Java Getters and Setters

// Java
public class GettersAndSetters {
  private String readOnly = "Only getter defined";
  private String writeOnly = "Only setter defined";
  private String readWrite = "Both defined";

  public String getReadOnly() { return readOnly; }
  public void setWriteOnly(String writeOnly) { this.writeOnly = writeOnly; }
  public String getReadWrite() { return readWrite; }
  public void setReadWrite(String readWrite) { this.readWrite = readWrite; }
}

// Kotlin
private val gs = GettersAndSetters()
println(gs.readOnly)          // Read-only attribute acts like a val property

gs.readWrite = "I have both"  // Read-write attribute acts like a var property
println(gs.readWrite)

gs.setWriteOnly("No getter")  // Write-only properties not supported in Kotlin

This automatic conversion to a Kotlin property works as long as a Java class provides a parameterless method starting with “get” and optionally a single-parameter method starting with “set”. It also works for Boolean expressions where the getter starts with “is” instead of “get”, but there’s currently no such mechanism if they start with “has” or other prefixes.

Write-only properties are currently not supported in Kotlin, which is why setWriteOnly cannot be called with property syntax as writeOnly.

Handling Nullability

Regarding nullability, there’s a gap in expressiveness between Kotlin and Java that has to be handled: in Kotlin, every variable is either nullable or not, in Java there’s no nullability information expressed in the language itself because every variable may potentially be null (except primitive types). However, handling all data coming from Java as nullable in Kotlin would introduce unnecessary code complexity due to null handling. To handle this problem, Kotlin uses so-called platform types for data coming from Java.

Nullables and Mapped Types

As mentioned in Chapter 2, Diving into Kotlin, primitive types are mapped to the corresponding (non-nullable) Kotlin types. For example, int is mapped to kotlin.Int, boolean to kotlin.Boolean, and vice versa. This makes sense because primitive types cannot be null in Java. However, boxed types must be mapped to nullable types, for instance java.lang.Integer to kotlin.Int?, java.lang.Character to kotlin.Char?, and vice versa.

Generally, interoperability with Java introduces the problems of mapping any objects coming from Java to a Kotlin type. Obviously, it’s not possible to map them all to non-nullable types because that would be unsafe. But because you want to exclude nullability from your code as much as possible, Kotlin doesn’t simply make them all nullable either. Kotlin supports nullability annotations from JSR 305,3 JetBrains, Android, FindBugs,4 and more that define annotations such as @Nullable and @NotNull. This additional information allows a better mapping to appropriate Kotlin types.

Platform Types

Ultimately, the Kotlin team decided to leave the nullability decision to the developer in cases where nullability cannot be inferred. This is represented using platform types such as SomeType!. Here, SomeType! means either SomeType or SomeType?. These are the cases where you can choose to store an object of that platform in a nullable or non-nullable variable. See Table 5.1 for several examples of mapped types.

Table 5.1 Mapped Types and Nullability

Java Declaration

Kotlin Type

Explanation

String name;

kotlin.String!

May or may not be null

@NotNull String name;

kotlin.String

Supposed to not be null

@Nullable String name;

kotlin.String?

Can be null

@NotNull String name = null;

kotlin.String

Source of null pointer exception  even when using Kotlin

Java objects without annotation may or may not be null, so Kotlin will map them to the corresponding platform type, allowing you to treat it as either nullable or non-nullable. If annotations are used, Kotlin uses the additional knowledge about nullability. However, objects marked as @NotNull in Java that still contain null by mistake can cause a null pointer exception in Kotlin.

So platform types denote that the associated type may or may not be nullable, and the developer is able to decide whether the platform type should be handled as nullable or not by using the corresponding type; this is shown in Listing 5.3.

Listing 5.3 Handling Platform Types in Kotlin

// Java
public static String hello() { return "I could be null"; }

// Kotlin
val str = hello()                 // Inferred type: String (by default)
println(str.length)              // 15

val nullable: String? = hello()   // Explicit type: String?
println(nullable?.length)        // 15

Without an explicit type, the compiler infers the non-nullable variant of the platform type so that you can use it without handling null. This is unsafe if you don’t think about whether the method you’re calling does actually never return null. Hence, I recommend stating the type explicitly when calling a Java method. That way, your intention and the fact that you thought about nullability are explicit.

You’ll quickly come across more complex platform types as well. These come from mapped types and generic types that may or may not carry nullability information. Consider the examples in Table 5.2.

Table 5.2 Examples of Platform Types (Combined with Mapped Types)

Java Return Type

Platform Type

Explanation

String

String!

May or may not be null, thus either String or String?

List<String>

(Mutable)List<String!>!

Maybe mutable and maybe nullable list, containing maybe nullable Strings

Map<Int, String>

(Mutable)Map<Int!, String!>!

Maybe mutable and maybe nullable map, containing maybe nullable Ints as keys and maybe nullable Strings as values

Object[]

Array<(out) Any!>!

Maybe nullable array, containing maybe nullable Anys or maybe subtypes of it (because arrays are covariant in Java)

long[]

LongArray!

Maybe nullable array of primitive longs (the whole array may be null)

List<? super Integer>

MutableList <in Int!>!

Maybe nullable and mutable list, containing Ints or parent types of it

The first row demonstrates that unannotated types are translated to platform types by default. Next, a List<T> coming from Java may be used as a mutable list or not, and may itself again be nullable or not (like any other type). Additionally, elements of the list are also potentially nullable. The same holds for other generic types and their type arguments, as shown in the third row. There, the generic type arguments Int and String are mapped to their platform type counterparts. This is necessary because Java allows null as both key and value.

Regarding the fourth row, an array coming from Java is also potentially nullable. On top of that, it may contain subtypes of the declared type argument because arrays are covariant in Java so that an Object[] may contain subtypes of Object. This is why it is translated to a potentially out-projected type. Note also that java.lang.Object is translated to kotlin.Any as defined per the mapped types. A complete list of mapped types is available in the language documentation.5 Additionally, arrays of primitive types are mapped to their corresponding specialized Kotlin class, such as IntArray, DoubleArray, and CharArray. Lastly, a contravariant list coming from Java becomes an in-projected type in Kotlin. As you can see, it even becomes a definitely mutable list because it wouldn’t be usable otherwise as an in-projected list. However, you shouldn’t use a wildcard type as a return type in Java because it makes the return type harder to work with for the caller.

Adding Nullability Annotations

Handling platform types is only required if there’s no nullability information the compiler can use. Even though the Java language itself doesn’t have a concept for this, you can actually attach nullability info via annotations such @NotNull and @Nullable. Such annotations are already fairly widespread, and there are various libraries containing such annotations. The Kotlin compiler can currently process the following nullability annotations.

  • The Android annotations @NonNull and @Nullable from the package android.support.annotations. These can be used out of the box on Android.

  • The JetBrains annotations @NotNull and @Nullable from org.jetbrains.annotations that are used for static analysis in Android Studio and IntelliJ IDEA and can also be used out of the box in both.

  • Annotations from the javax.annotation package.

  • FindBugs annotations from the package edu.umd.cs.findbugs.annotations.

  • The Lombok annotation lombok.NonNull.6

  • Eclipse’s7 nullability annotations from org.eclipse.jdt.annotation.

With these, Kotlin supports the most widely used annotations. Listing 5.4 provides an example for how to use JetBrains’ annotations; the others work the same way. If you’re using others, such as NetBeans8 or Spring Framework annotations, you’ll have to transition to one of the above to get better type inference in Kotlin.

Listing 5.4 Using Nullability Annotations

// Java
public static @Nullable String nullable() { return null; }
public static @NotNull String nonNull() { return "Could be null, but with warning"; }

// Kotlin
val s1 = nullable()  // Inferred type: String?
val s2 = nonNull()   // Inferred type: String

Note that nothing stops you from returning null from the nonNull method in Java, you’ll only receive a warning in AndroidStudio and IntelliJ, depending on which nullability annotations you use. For instance, the JetBrains annotations are used for static analysis in Android Studio and IntelliJ so that such code would yield a warning.

In this way, nullability annotations allow you to get more type information in Kotlin. In other words, they disambiguate the impreciseness of Type! to either Type or Type?. Let’s consider the examples in Table 5.3 and contrast them to the above table of platform types.

Table 5.3 Examples of Inferred Types (When Using Nullability Annotations)

Java Return Type

Inferred Kotlin Type

Explanation

@NonNull String

String

Non-nullable String

@Nullable String

String?

Nullable String

@NonNull List <String>

(Mutable)List <String!>

Non-nullable but maybe mutable list, containing maybe nullable Strings

List<@NonNull String>

(Mutable)List <String>!

Maybe nullable and maybe mutable list, containing non-nullable Strings

@NonNull

List<@NonNull String>

(Mutable)List <String>

Non-nullable but maybe mutable list, containing non-nullable Strings

In short, by resolving the ambiguity to either @NonNull or @Nullable, the return type of the Java method is mapped accordingly when called from Kotlin. Note that, to use annotations on generic types argument as in List<@NonNull String>, you need to use an implementation of JSR 3089 such as the checker framework.10 The purpose of JSR 308 is to allow annotating any type in Java, including generic type parameters, and it was incorporated into Java Standard Edition 8.

Escaping Clashing Java Identifiers

When mapping between languages, there is always a disparity between the keywords defined in the languages. Kotlin has several keywords that don’t exist in Java and are thus valid Java identifiers, such as val, var, object, and fun. All these must be escaped using backticks in Kotlin as shown in Listing 5.5. This is done automatically in IntelliJ and Android Studio.

Listing 5.5 Handling Name Clashes

// Java
class KeywordsAsIdentifiers {
    public int val = 100;
    public Object object = new Object();
    public boolean in(List<Integer> list) { return true; }
    public void fun() { System.out.println("This is fun."); }
}

// Kotlin
val kai = KeywordsAsIdentifiers()
kai.`val`
kai.`object`
kai.`in`(listOf(1, 2, 3))
kai.`fun`()

Even though it looks inconvenient, it’s not something you have to think about explicitly thanks to the IDE. It still does clutter up the code a little but should only appear in rare cases anyway. If you have to escape frequently, you may want to rethink your naming standards in Java—none of the above identifiers convey much meaning when used as identifiers.

Calling Variable-Argument Methods

Calling vararg methods defined in Java works naturally so that you can pass in an arbitrary number of arguments. However, in contrast to Java, you cannot pass an array to a vararg method directly. Instead, you have to use the spread operator by prefixing a * to the array. Listing 5.6 shows both ways to call a variable-argument method.

Listing 5.6 Calling a Variable-Argument Method

// Java
public static List<String> myListOf(String... strings) {  // Vararg method from Java
    return Arrays.asList(strings);                      // Passes in vararg unchanged
}

// Kotlin
val list = myListOf("a", "b", "c")                     // Can pass in any number of args
val values = arrayOf("d", "e", "f")
val list2 = myListOf(*values)                          // Spread operator required

Using Operators

You can call Java methods like operators in Kotlin if they have the right signature. For instance, defining a plus or minus method in a class allows you to add or subtract objects of it. Listing 5.7 provides an example.

Listing 5.7 Using Java Methods as Operators

// Java
public class Box {
    private final int value;

    public Box(int value) { this.value = value; }
    public Box plus(Box other)  { return new Box(this.value + other.value); }
    public Box minus(Box other) { return new Box(this.value - other.value); }
    public int getValue() { return value; }
}

// Kotlin
val value1 = Box(19)
val value2 = Box(37)
val value3 = Box(14)

val result = value1 + value2 - value3  // Uses ‘plus’ and ‘minus’ as operators
println(result.value)  // 42

Although you can write Java code that targets the predefined set of operators, you cannot define other methods that allow infix notation in Kotlin.

Using SAM Types

Interfaces with a single abstract method (“SAM types”) can be called from Kotlin without boilerplate thanks to SAM conversions. Listing 5.8 shows how a lambda expression can be used to implement the single abstract method of a SAM interface.

Listing 5.8 Using SAM Types from Kotlin

// Java
interface Producer<T> {  // SAM interface (single abstract method)
  T produce();
}

// Kotlin
private val creator = Producer { Box(9000) }  // Inferred type: Producer<Box>

In the Java code, this defines a SAM type, with produce being the single abstract method. To create a Producer<T> in Kotlin, you don’t need to use an object expression (or an anonymous inner class as in Java) but can instead use a lambda expression that provides the implementation of the single abstract method. In Listing 5.8, you have to add Producer in front of the lambda expression, otherwise the type of the right-hand side would be () -> Box and not Producer<Box>. This is not necessary when passing in a function argument, as shown in Listing 5.9, because the compiler can then infer the type.

Listing 5.9 SAM Conversion in Function Arguments

// Kotlin
val thread = Thread {  // No need to write Thread(Runnable { … })
  println("I'm the runnable")
}

The Thread constructor accepts a Runnable, which is instantiated here using SAM conversions. The code is equivalent to Thread(Runnable { … }). But because the constructor argument is of type Runnable, you’re not required to add an additional type hint in front of the lambda here. Consequently, you can use Kotlin’s convention to move lambdas out of the parentheses and then omit the parentheses because the lambda is the only parameter.

This mechanism is extremely useful for many built-in SAM types such as Comparator, Runnable, and many listener classes.

Further Interoperability Considerations

You’ve now seen that you can call Java code from Kotlin in a natural way most of the time. Sometimes, as with SAM conversions, it’s even more convenient than from Java itself. The main point to keep in mind is nullability and how it maps to Kotlin. Other considerations to keep in mind can be explained briefly.

  • Methods that throw a checked exception in Java can be called from Kotlin without having to handle the exception because Kotlin decided against checked exceptions.

  • You can retrieve the Java class of an object as SomeClass::class.java or instance.javaClass.

  • You can use reflection on Java classes from Kotlin and use a reference to the Java class as the entry point. For instance, Box::class.java.declaredMethods returns the methods declared in the Box class.

  • Inheritance works naturally across Kotlin and Java; both support only one superclass but any number of implemented interfaces.

Since Kotlin was designed with interoperability in mind, it works seamlessly with Java most of the time. The following section explores what you should keep in mind when calling Kotlin code from Java.

  • + Share This
  • 🔖 Save To Your Account