Calling Kotlin Code from Java

In this codelab, you'll learn how to write or adapt your Kotlin code to make it more seamlessly callable from Java code.

What you'll learn

  • How to make use of @JvmField, @JvmStatic, and other annotations.
  • Limitations with accessing certain Kotlin language features from Java code.

What you must know already

This codelab is written for programmers and assumes basic Java and Kotlin knowledge.

This codelab simulates migrating part of a larger project written with the Java programming language, to incorporate new Kotlin code.

To simplify things, we'll have a single .java file called UseCase.java, which will represent the existing codebase.

We'll imagine we just replaced some functionality originally written in Java with a new version written in Kotlin, and we need to finish integrating it.

Import the project

The project's code can be cloned from the GitHub project here: GitHub

Alternatively, you can download and extract the project from a zip archive found here:

Download Zip

If you're using IntelliJ IDEA, select "Import Project".

If you're using Android Studio, select "Import project (Gradle, Eclipse ADT, etc.)".

Let's open UseCase.java and start working through the errors we see.

The first function with a problem is registerGuest:

public static User registerGuest(String name) {
   User guest = new User(Repository.getNextGuestId(), StringUtils.nameToLogin(name), name);
   Repository.addUser(guest);
   return guest;
}

The errors for both Repository.getNextGuestId() and Repository.addUser(...) are the same: "Non-static cannot be accessed from a static context."

Now let's take a look at one of the Kotlin files. Open the file Repository.kt.

We see that our Repository is a singleton that is declared by using the object keyword. The problem is that Kotlin is generating a static instance inside our class, rather than exposing these as static properties and methods.

For example, Repository.getNextGuestId() could be referenced by using Repository.INSTANCE.getNextGuestId(), but there's a better way.

We can get Kotlin to generate static methods and properties by annotating the public properties and methods of the Repository with @JvmStatic:

object Repository {
   val BACKUP_PATH = "/backup/user.repo"

   private val _users = mutableListOf<User>()
   private var _nextGuestId = 1000

   @JvmStatic
   val users: List<User>
       get() = _users

   @JvmStatic
   val nextGuestId
       get() = _nextGuestId++

   init {
       _users.add(User(100, "josh", "Joshua Calvert", listOf("admin", "staff", "sys")))
       _users.add(User(101, "dahybi", "Dahybi Yadev", listOf("staff", "nodes")))
       _users.add(User(102, "sarha", "Sarha Mitcham", listOf("admin", "staff", "sys")))
       _users.add(User(103, "warlow", groups = listOf("staff", "inactive")))
   }

   @JvmStatic
   fun saveAs(path: String?):Boolean {
       val backupPath = path ?: return false

       val outputFile = File(backupPath)
       if (!outputFile.canWrite()) {
           throw FileNotFoundException("Could not write to file: $backupPath")
       }
       // Write data...
       return true
   }

   @JvmStatic
   fun addUser(user: User) {
       // Ensure the user isn't already in the collection.
       val existingUser = users.find { user.id == it.id }
       existingUser?.let { _users.remove(it) }
       // Add the user.
       _users.add(user)
   }
}

Add the @JvmStatic annotation to your code using your IDE.

If we switch back to UseCase.java, the properties and methods on Repository are no longer causing errors, except for Repository.BACKUP_PATH. We'll come back to that later.

For now, let's fix the next error in the registerGuest() method.

Let's consider the following scenario: we had a StringUtils class with several static functions for string operations. When we converted it to Kotlin, we converted the methods into extension functions. Java doesn't have extension functions, so Kotlin compiles these methods as static functions.

Unfortunately, if we look over at the registerGuest() method inside UseCase.java, we can see that something isn't quite right:

User guest = new User(Repository.getNextGuestId(), StringUtils.nameToLogin(name), name);

The reason is that Kotlin places these "top-level" or package-level functions inside a class whose name is based on the filename. In this case, because the file is named StringUtils.kt, the corresponding class is named StringUtilsKt.

We could change all our references of StringUtils to StringUtilsKt and fix this error, but this isn't ideal because:

  • There may be many places in our code that would need to be updated.
  • The name itself is awkward.

So rather than refactor our Java code, let's update our Kotlin code to use a different name for these methods.

Open StringUtils.Kt, and find the following package declaration:

package com.google.example.javafriendlykotlin

We can tell Kotlin to use a different name for the package-level methods by using the @file:JvmName annotation. Let's use this annotation to name the class StringUtils.

@file:JvmName("StringUtils")

package com.google.example.javafriendlykotlin

Now, if we look back at UseCase.java, we can see that the error for StringUtils.nameToLogin() has been resolved.

Unfortunately, this error was replaced with a new one about the parameters being passed into the constructor for User. Let's continue to the next step and fix this last error in UseCase.registerGuest().

Kotlin supports default values for parameters. We can see how they're used by looking inside the init block of Repository.kt.

Repository.kt:

_users.add(User(102, "sarha", "Sarha Mitcham", listOf("admin", "staff", "sys")))
_users.add(User(103, "warlow", groups = listOf("staff", "inactive")))

We can see that for the user "warlow", we can skip putting in a value for displayName because there's a default value specified for it in User.kt.

User.kt:

data class User(
   val id: Int,
   val username: String,
   val displayName: String = username.toTitleCase(),
   val groups: List<String> = listOf("guest")
)

Unfortunately this doesn't work the same when calling the method from Java.

UseCase.java:

User guest = new User(Repository.getNextGuestId(), StringUtils.nameToLogin(name), name);

Default values aren't supported in the Java programming language. To fix this, let's tell Kotlin to generate overloads for our constructor with the help of the @JvmOverloads annotation.

First, we have to make a slight update to User.kt.

Since the User class only has a single, primary constructor, and the constructor does not include any annotations, the constructor keyword had been omitted. Now that we'd like to annotate it, however, the constructor keyword must be included:

data class User constructor(
    val id: Int,
    val username: String,
    val displayName: String = username.toTitleCase(),
    val groups: List<String> = listOf("guest")
)

With the constructor keyword present, we can add the @JvmOverloads annotation:

data class User @JvmOverloads constructor(
    val id: Int,
    val username: String,
    val displayName: String = username.toTitleCase(),
    val groups: List<String> = listOf("guest")
)

If we switch back to UseCase.java, we can see that there aren't any more errors in the registerGuest function!

Our next step is to fix the broken call to user.hasSystemAccess() in UseCase.getSystemUsers(). Continue on to the next step for that, or continue reading to dig deeper into what @JvmOverloads has done to fix the error.

@JvmOverloads

To better understand what @JvmOverloads does, let's create a test method in UseCase.java:

private void testJvmOverloads() {
   User syrinx = new User(1001, "syrinx");
   User ione = new User(1002, "ione", "Ione Saldana");

   List<String> groups = new ArrayList<>();
   groups.add("staff");
   User beaulieu = new User(1002, "beaulieu", groups);
}

We can construct a User with just two parameters, id and username:

User syrinx = new User(1001, "syrinx");

We can also construct a User by including a third parameter for displayName while still using the default value for groups:

User ione = new User(1002, "ione", "Ione Saldana");

But it's not possible to skip displayName and just provide a value for groups without writing additional code:

So let's delete that line or preface it with ‘//' to comment it out.

In Kotlin, if we want to combine default and non-default parameters, we need to use named parameters.

// This doesn't work...
User(104, "warlow", listOf("staff", "inactive"))
// But using named parameters, it does...
User(104, "warlow", groups = listOf("staff", "inactive"))

The reason is that Kotlin will generate overloads for functions, including constructors, but it will only create one overload per parameter with a default value.

Let's look back at UseCase.java and address our next problem: the call to user.hasSystemAccess() in the method UseCase.getSystemUsers():

public static List<User> getSystemUsers() {
   ArrayList<User> systemUsers = new ArrayList<>();
   for (User user : Repository.getUsers()) {
       if (user.hasSystemAccess()) {     // Now has an error!
           systemUsers.add(user);
       }
   }
   return systemUsers;
}

This is an interesting error! If you use your IDE's autocomplete feature on the class User, you will notice hasSystemAccess() was renamed to getHasSystemAccess().

To fix the problem, we'd like to have Kotlin generate a different name for the val property hasSystemAccess. To do this, we can use the @JvmName annotation. Let's switch back to User.kt and see where we should apply it.

There are two ways we can apply the annotation. The first is to apply it directly to the get() method, like this:

val hasSystemAccess
   @JvmName("hasSystemAccess")
   get() = "sys" in groups

This signals to Kotlin to change the signature of the explicitly-defined getter to the name provided.

Alternatively, it's possible to apply it to the property by using a get: prefix like this:

@get:JvmName("hasSystemAccess")
val hasSystemAccess
   get() = "sys" in groups

The alternate method is particularly useful for properties that are using a default, implicitly-defined getter. For example:

@get:JvmName("isActive")
val active: Boolean

This allows the getter's name to be changed without having to explicitly define a getter.

Despite this distinction, you can use whichever feels better to you. Both will cause Kotlin to create a getter with the name hasSystemAccess().

If we switch back to UseCase.java, we can verify that getSystemUsers() is now error free!

The next error is in formatUser(), but if you'd like to read more about Kotlin getter naming convention, continue reading here before moving on to the next step.

Getter and Setter Naming

When we're writing Kotlin, it's easy to forget that writing code such as:

val myString = "Logged in as ${user.displayName}")

Is actually calling a function to get the value of displayName. We can verify this by going to Tools > Kotlin > Show Kotlin Bytecode in the menu and then clicking the Decompile button:

String myString = "Logged in as " + user.getDisplayName();

When we want to access these from Java, we need to explicitly write out the name of the getter.

In most cases, the Java name of getters for Kotlin properties is simply get + the property name, as we've seen with User.getHasSystemAccess() and User.getDisplayName(). The one exception is properties whose names begin with "is". In this case, the Java name for the getter is the name of the Kotlin property.

For example, a property on User such as:

val isAdmin get() = //...

Would be accessed from Java with:

boolean userIsAnAdmin = user.isAdmin();

By using the @JvmName annotation, Kotlin generates bytecode that has the specified name, rather than the default one, for the item being annotated.

This works the same for setters, whose generated names are always set + property name. For example, take the following class:

class Color {
   var red = 0f
   var green = 0f
   var blue = 0f
}

Let's imagine we'd like to change the setter name from setRed() to updateRed(), while leaving the getters alone. We could use the @set:JvmName version to do this:

class Color {
   @set:JvmName("updateRed")
   var red = 0f
   @set:JvmName("updateGreen")
   var green = 0f
   @set:JvmName("updateBlue")
   var blue = 0f
}

From Java, we'd then be able to write:

color.updateRed(0.8f);

UseCase.formatUser() uses direct field access to get the values of the properties of a User object.

In Kotlin, properties are normally exposed via getters and setters. This includes val properties.

It's possible to change this behavior by using the @JvmField annotation. When this is applied to a property in a class, Kotlin will skip generating getter (and setter for var properties) methods, and the backing field can be accessed directly.

Since User objects are immutable, we'd like to expose each of their properties as fields, and so we'll annotate each of them with @JvmField:

data class User @JvmOverloads constructor(
   @JvmField val id: Int,
   @JvmField val username: String,
   @JvmField val displayName: String = username.toTitleCase(),
   @JvmField val groups: List<String> = listOf("guest")
) {
   @get:JvmName("hasSystemAccess")
   val hasSystemAccess
       get() = "sys" in groups
}

If we look back at UseCase.formatUser() now we can see that the errors have been fixed!

@JvmField or const

With that, there's another similar looking error in the UseCase.java file:

Repository.saveAs(Repository.BACKUP_PATH);

If we use autocomplete here, we can see that there is a Repository.getBACKUP_PATH(), and so it may be tempting to change the annotation on BACKUP_PATH from @JvmStatic to @JvmField.

Let's try this. Switch back to Repository.kt, and update the annotation:

object Repository {
   @JvmField
   val BACKUP_PATH = "/backup/user.repo"

If we look at UseCase.java now, we'll see the error went away, but there's also a note on BACKUP_PATH:

In Kotlin, the only types that can be const are primitives, such as int, float, and String. In this case, because BACKUP_PATH is a string, we can get better performance by using const val rather than a val annotated with @JvmField, while retaining the ability to access the value as a field.

Let's change that now in Repository.kt:

object Repository {
   const val BACKUP_PATH = "/backup/user.repo"

If we look back at UseCase.java we can see there's only one error left.

The final error says Exception: 'java.io.IOException' is never thrown in the corresponding try block.

If we look at the code for Repository.saveAs in Repository.kt, we see that it does throw an exception, though. What's going on?

Java has the concept of a "checked exception". These are exceptions that could be recovered from, such as the user mistyping a file name, or the network being temporarily unavailable. After a checked exception is caught, the developer could then provide feedback to the user on how to fix the problem.

Since checked exceptions are checked at compile time, you declare them in the method's signature:

public void openFile(File file) throws FileNotFoundException {
   // ...
}

Kotlin, on the other hand, does not have checked exceptions, and that's what's causing the problem here.

The solution is to ask Kotlin to add the IOException that's potentially thrown to the signature of Repository.saveAs(), so that the JVM bytecode includes it as a checked exception.

We do this with the Kotlin @Throws annotation, which helps with Java/Kotlin interoperability. In Kotlin, exceptions behave similar to Java, but unlike Java, Kotlin only has unchecked exceptions. So if you want to inform your Java code that a Kotlin function throws an exception, you need to use the @Throws annotation to the Kotlin function signature Switch to the Repository.kt file and update saveAs() to include the new annotation:

@JvmStatic
@Throws(IOException::class)
fun saveAs(path: String?) {
   val outputFile = File(path)
   if (!outputFile.canWrite()) {
       throw FileNotFoundException("Could not write to file: $path")
   }
   // Write data...
}

With the @Throws annotation in place, we can see that all of the compiler errors in UseCase.java are fixed! Hooray!

You might wonder if you'll have to use try and catch blocks when calling saveAs() from Kotlin now.

Nope! Remember, Kotlin doesn't have checked exceptions, and adding @Throws to a method doesn't change that:

fun saveFromKotlin(path: String) {
   Repository.saveAs(path)
}

It's still useful to catch exceptions when they can be handled, but Kotlin doesn't force you to handle them.

In this codelab, we covered the basics for how to write Kotlin code that also supports writing idiomatic Java code.

We talked about how we can use annotations to change the way Kotlin generates its JVM bytecode, such as:

  • @JvmStatic to generate static members and methods.
  • @JvmOverloads to generate overloaded methods for functions that have default values.
  • @JvmName to change the name of getters and setters.
  • @JvmField to expose a property directly as a field, rather than via getters and setters.
  • @Throws to declare checked exceptions.

The final contents of our files are:

User.kt

data class User @JvmOverloads constructor(
   @JvmField val id: Int,
   @JvmField val username: String,
   @JvmField val displayName: String = username.toTitleCase(),
   @JvmField val groups: List<String> = listOf("guest")
) {
   val hasSystemAccess
       @JvmName("hasSystemAccess")
       get() = "sys" in groups
}

Repository.kt

object Repository {
   const val BACKUP_PATH = "/backup/user.repo"

   private val _users = mutableListOf<User>()
   private var _nextGuestId = 1000

   @JvmStatic
   val users: List<User>
       get() = _users

   @JvmStatic
   val nextGuestId
       get() = _nextGuestId++

   init {
       _users.add(User(100, "josh", "Joshua Calvert", listOf("admin", "staff", "sys")))
       _users.add(User(101, "dahybi", "Dahybi Yadev", listOf("staff", "nodes")))
       _users.add(User(102, "sarha", "Sarha Mitcham", listOf("admin", "staff", "sys")))
       _users.add(User(103, "warlow", groups = listOf("staff", "inactive")))
   }

   @JvmStatic
   @Throws(IOException::class)
   fun saveAs(path: String?):Boolean {
       val backupPath = path ?: return false

       val outputFile = File(backupPath)
       if (!outputFile.canWrite()) {
           throw FileNotFoundException("Could not write to file: $backupPath")
       }
       // Write data...
       return true
   }

   @JvmStatic
   fun addUser(user: User) {
       // Ensure the user isn't already in the collection.
       val existingUser = users.find { user.id == it.id }
       existingUser?.let { _users.remove(it) }
       // Add the user.
       _users.add(user)
   }
}

StringUtils.kt

@file:JvmName("StringUtils")

package com.google.example.javafriendlykotlin

fun String.toTitleCase(): String {
   if (isNullOrBlank()) {
       return this
   }

   return split(" ").map { word ->
       word.foldIndexed("") { index, working, char ->
           val nextChar = if (index == 0) char.toUpperCase() else char.toLowerCase()
           "$working$nextChar"
       }
   }.reduceIndexed { index, working, word ->
       if (index > 0) "$working $word" else word
   }
}

fun String.nameToLogin(): String {
   if (isNullOrBlank()) {
       return this
   }
   var working = ""
   toCharArray().forEach { char ->
       if (char.isLetterOrDigit()) {
           working += char.toLowerCase()
       } else if (char.isWhitespace() and !working.endsWith(".")) {
           working += "."
       }
   }
   return working
}