How is interop achieved?

Kotlin is primarily a JVM language and therefore is compiled to standard JVM bytecode by the Kotlin compiler. This bytecode is what is actually run by the Java Virtual Machine to execute our programs. Because this resultant bytecode is familiar to JVM developers and tools, it can be integrated and manipulated using known techniques. This is largely how Kotlin can work so seamlessly with Java. 

When Kotlin is compiled for the JVM, the underlying bytecode is the same as if you had written Java code. To maintain compatibility with multiple versions of Java, the Kotlin compiler is capable of generating Java 6- and Java 8+-compatible versions of JVM bytecode. This means that tools built to support Java through the examination and manipulation of bytecode can also support Kotlin. This also means that classes and methods declared in Java can be understood by Kotlin and worked with from Kotlin files, classes, and functions.

A great example of this tooling interop is the Kotlin Bytecode tool within IntelliJ. With this tool, it's possible to view the bytecode for a Kotlin source file, and then to decompile that bytecode to Java. Through this process, it's very easy to see how the Kotlin we write is understood at a low level in the same way that our Java code would be.

Let's take a quick look at this bytecode examination to illustrate this point:

  1. To start, within Person.kt, we'll define a simple Person class in Kotlin, which we will use to examine the converted bytecode:
class Person(var firstName:String, var lastName:String)
  1. Next, ensure Person.kt is open, and navigate to Tools | Kotlin | View Kotlin Bytecode. Once this is done, you should see a new tool window open, which displays the bytecode:

  1. Once the Kotlin Bytecode window is open, we can click the DECOMPILE button to view the Java equivalent of our bytecode:
// Person.decompiled.java
public final class
Person {
@NotNull
private String firstName;
@NotNull
private String lastName;

@NotNull
public final String getFirstName() {
return this.firstName;
}

public final void setFirstName(@NotNull String var1) {
Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
this.firstName = var1;
}

@NotNull
public final String getLastName() {
return this.lastName;
}

public final void setLastName(@NotNull String var1) {
Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
this.lastName = var1;
}

public Person(@NotNull String firstName,
@NotNull String lastName) {
Intrinsics.checkParameterIsNotNull(firstName, "firstName");
Intrinsics.checkParameterIsNotNull(lastName, "lastName");
super();
this.firstName = firstName;
this.lastName = lastName;
}
}

This Java class is generated by the decompile tool and represents the Java-equivalent implementation of the Kotlin class we defined previously. You'll likely notice that this class looks very much like what we would implement ourselves if writing our Person class in Java.

This ability to write Kotlin and then decompile the resultant bytecode into human-readable Java demonstrates how well Kotlin is integrated with Java and the IntelliJ tooling. This integration makes it easy to get started and makes it easy to understand what the Kotlin compiler is doing behind the scenes. 

In the next section, we'll look at how to add Kotlin support to an existing Java project to start taking advantage of this great interop experience.