Chapters

Hide chapters

Kotlin Apprentice

Second Edition · Android 10 · Kotlin 1.3 · IDEA

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

Section III: Building Your Own Types

Section 3: 8 chapters
Show chapters Hide chapters

Section IV: Intermediate Topics

Section 4: 9 chapters
Show chapters Hide chapters

19. Kotlin/Java Interoperability
Written by Ellen Shapiro

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

Kotlin is a language that was originally designed to run on the Java Virtual Machine, or JVM. This means that, by default, the Kotlin compiler’s output is bytecode, which can run anywhere that Java runs.

Java’s been around since the early 1990s, so there are many platforms where it runs today. Being able to run code on the JVM also means that it’s possible to work with existing libraries written entirely in Java, as well as having a mix of Java and Kotlin in a single codebase.

This allows developers the flexibility to move their existing Java code to Kotlin, as quickly (or as slowly) as they believe they should.

Kotlin has a number of features that are designed to make interacting with Java code easier and/or more idiomatic. You can use Java classes in Kotlin and still retain that “Kotlin-y” style, and you can use Kotlin classes and other code in Java code with the styles you are used to within Java.

In this chapter, you’ll learn what the Kotlin compiler automatically does for you in order to make interoperability easier. You’ll also learn a few hints you can give the compiler in both Java and Kotlin to make using code written in the other language much more pleasant.

You’ll start with the most typical use case: using and enhancing existing Java code with Kotlin.

Mixing Java and Kotlin code

There are several things that the Kotlin compiler will automatically do for you in order to make using code written in Java feel more at home when called from Kotlin, as well as interacting with code written in Kotlin from Java. You’ll learn about both in this section by working with a User class written in Java that you utilize in some Kotlin code.

Getters and setters

To start, open up the starter project for this chapter and go to the User.java file.

val user = User()

user.firstName = "Bob"
user.lastName = "Barker"
user.city = "Los Angeles"
user.country = "United States"

println("User info:\n$user")
User info:
Bob Barker
Los Angeles, United States

Adding a Kotlin class as a Java property

In IntelliJ IDEA’s menu, select File ▸ New ▸ Kotlin File/Class. Name the file you’re creating Address:

enum class AddressType {
  Billing,
  Shipping,
  Gift
}
data class Address(
    val streetLine1: String,
    val streetLine2: String?,
    val city: String,
    val stateOrProvince: String,
    val postalCode: String,
    var addressType: AddressType,
    val country: String = "United States"
) {
  // TODO
}
fun forPostalLabel(): String {
  var printedAddress = streetLine1
  streetLine2?.let { printedAddress += "\n$it" }
  printedAddress += "\n$city, $stateOrProvince $postalCode"
  printedAddress += "\n${country.toUpperCase()}"
  return printedAddress
}
val billingAddress = Address("123 Fake Street",
    "4th floor",
    "Los Angeles",
    "CA",
    "90291",
    AddressType.Billing)
  
println("Billing Address:\n$billingAddress\n")
Billing Address: 
Address(streetLine1=123 Fake Street, streetLine2=4th floor, city=Los Angeles, stateOrProvince=CA, postalCode=90291, addressType=Billing, country=United States)
override fun toString(): String {
    return forPostalLabel()
}
Billing Address: 
123 Fake Street
4th floor
Los Angeles, CA 90291
UNITED STATES
@Override
public String toString() {
  return firstName + " " + lastName;
}

Adding extension functions to a Java class

Go to File ▸ New ▸ Kotlin File/Class. Name your new file UserExtensions:

val User.fullName: String
  get() = "$firstName $lastName"
@Override
public String toString() {
  return UserExtensionsKt.getFullName(this);
}
@file:JvmName("UserExtensions")
return UserExtensions.getFullName(this);
private List<Address> addresses = new ArrayList<>();
public List<Address> getAddresses() {
  return addresses;
}

public void setAddresses(List<Address> addresses) {
  this.addresses = addresses;
}
return UserExtensions.getFullName(this) + " - Addresses: " + addresses.size();
User info:
Bob Barker - Addresses: 0
fun User.addressOfType(type: AddressType): Address? {
  return addresses.firstOrNull { it.addressType == type }
}
fun User.addOrUpdateAddress(address: Address) {
  val existingOfType = addressOfType(address.addressType)

  if (existingOfType != null) {
    addresses.remove(existingOfType)
  }

  addresses.add(address)
}
user.addOrUpdateAddress(billingAddress)
println("User info after adding address:\n$user")
User info after adding address:
Bob Barker - Addresses: 1
val shippingAddress = Address("987 Unreal Drive",
    null,
    "Burbank",
    "CA",
    "91523",
    AddressType.Shipping)
  
user.addOrUpdateAddress(shippingAddress)

println("User info after adding addresses:\n$user")
User info after adding addresses:
Bob Barker - Addresses: 2
User info after adding addresses:
Bob Barker - Addresses: 1

Free functions

Free functions in Kotlin are functions that don’t extend any existing class and are not tied to a class themselves. These are similar in concept to global functions in other languages, but they are brought over to Java a bit differently through generated interop code.

// 1
fun labelFor(user: User, type: AddressType): String {
    // 2 
    val address = user.addressOfType(type)
    if (address != null) {
        // 3
        var label = "-----\n"
        label += "${user.fullName}\n${address.forPostalLabel()}\n"
        label += "-----\n"
        return label
    } else {
        return "\n!! ${user.fullName} does not have a $type address set up !!\n"
    }
}

// 4
fun printLabelFor(user: User, type: AddressType) {
    println(labelFor(user, type))
}
println("Shipping Label:")
printLabelFor(user, AddressType.Shipping)
Shipping Label:
-----
Bob Barker
987 Unreal Drive
Burbank, CA 91523
UNITED STATES
-----
public String allAddresses() {
  StringBuilder builder = new StringBuilder();
  for (Address address : addresses) {
    builder.append(address.getAddressType().name() + " address:\n");
    builder.append(LabelPrinterKt.labelFor(this, address.getAddressType()));
  }

  return builder.toString();
}
@file:JvmName("LabelPrinter")
builder.append(LabelPrinter.labelFor(this, address.getAddressType()));
return UserExtensions.getFullName(this) + " - Addresses: " + addresses.size() + "\n" + allAddresses();
User info after adding addresses:
Bob Barker - Addresses: 2
Billing address:
-----
Bob Barker
123 Fake Street
4th floor
Los Angeles, CA 90291
UNITED STATES
-----
Shipping address:
-----
Bob Barker
987 Unreal Drive
Burbank, CA 91523
UNITED STATES
-----

Java nullability annotations

Though Java 8 introduced Optional to make null values safer to work with, annotations are the way to go when handling nullability between Kotlin and Java.

val anotherUser = User()
println("Another User has ${anotherUser.addresses.count()} addresses")
Another User has 0 addresses
anotherUser.addresses = null

@Nullable
public List<Address> getAddresses() { ... }

println("Another User has ${anotherUser.addresses?.count()} addresses")

Another User has null addresses
@NotNull
public List<Address> getAddresses() { ... }

@NotNull
public String getFirstName() { ... }
...
@NotNull
public String getLastName() { ... }
println("Another User first name: ${anotherUser.firstName}")

@Nullable
public String getFirstName() { ... }
...
@Nullable
public String getLastName() { ... }
Another User first name: null
println("Another User first name: ${anotherUser.firstName ?: "(not set)"}")
Another User first name: (not set)

Making your Kotlin Code Java-friendly

In the starter project, there’s also a main.java file with a JavaApplication class. Open this file and replace the System.out.println() command with some new code:

// 1
User user = new User();
// 2
user.setFirstName("Testy");
user.setLastName("McTesterson");

// 3
Address address = new Address(
  "345 Nonexistent Avenue NW",
  null,
  "Washington",
  "DC",
  "20016",
  AddressType.Shipping
);

// 4
UserExtensions.addOrUpdateAddress(user, address);
LabelPrinter.printLabelFor(user, AddressType.Shipping);

data class Address @JvmOverloads constructor (
-----
Testy McTesterson
345 Nonexistent Avenue NW
Washington, DC 20016
UNITED STATES
-----

address.setAddressType(AddressType.Billing);
!! Testy McTesterson does not have a Shipping address set up !!
-----
Testy McTesterson
345 Nonexistent Avenue NW
Washington, DC 20016
UNITED STATES
-----
fun printLabelFor(user: User, type: AddressType = AddressType.Shipping) {
printLabelFor(user)
Shipping Label:
-----
Bob Barker
987 Unreal Drive
Burbank, CA 91523
UNITED STATES
-----
LabelPrinter.printLabelFor(user);

@JvmOverloads
fun printLabelFor(user: User, ....
-----
Testy McTesterson
345 Nonexistent Avenue NW
Washington, DC 20016
UNITED STATES
-----

Accessing nested Kotlin objects

In Kotlin, you can create objects that are not necessarily classes within a class. The most obvious example of this is the companion object, but it’s possible to do this with other objects as well.

object JSONKeys {
  val streetLine1 = "street_1"
  val streetLine2 = "street_2"
  val city = "city"
  val stateOrProvince = "state"
  val postalCode = "zip"
  val addressType = "type"
  val country = "country"
}
Address.JSONKeys keys = Address.JSONKeys;

Address.JSONKeys keys = Address.JSONKeys.INSTANCE;
HashMap<String, Object> addressJSON = new HashMap<>();
addressJSON.put(keys.getStreetLine1(), address.getStreetLine1());
data class Address @JvmOverloads constructor (
    @JvmField val streetLine1: String,
    @JvmField val streetLine2: String?,
    @JvmField val city: String,
    @JvmField val stateOrProvince: String,
    @JvmField val postalCode: String,
    @JvmField var addressType: AddressType,
    @JvmField val country: String = "United States") {
object JSONKeys {
    const val streetLine1 = "street_1"
    const val streetLine2 = "street_2"
    const val city = "city"
    const val stateOrProvince = "state"
    const val postalCode = "zip"
    const addressType = "type"
    const val country = "country"
}
addressJSON.put(keys.streetLine1, address.streetLine1);
addressJSON.put(keys.streetLine2, address.streetLine2);
addressJSON.put(keys.city, address.city);
addressJSON.put(keys.stateOrProvince, address.stateOrProvince);
addressJSON.put(keys.postalCode, address.postalCode);
addressJSON.put(keys.country, address.country);
addressJSON.put(keys.addressType, address.addressType.name());

System.out.println("Address JSON:\n" + addressJSON);
builder.append(address.addressType.name() + " address:\n");
builder.append(LabelPrinter.labelFor(this, address.addressType));
Address JSON: 
{zip=20016, country=United States, street_1=345 Nonexistent Avenue NW, city=Washington, street_2=null, state=DC, type=Shipping}

“Static” values and functions from Kotlin

In Java, a static member in a class means that it can be accessed without an instance of the class. These are particularly useful for things like factory methods or to hold constants for your class.

companion object {
  val sampleFirstLine = "123 Fake Street"
}
println("Sample First Line: ${Address.sampleFirstLine}")
Sample First Line: 123 Fake Street
System.out.println("Sample first line of address: " + Address.sampleFirstLine);

const val sampleFirstLine = "123 Fake Street"
Sample first line of address: 123 Fake Street
fun canadianSample(type: AddressType): Address {
  return Address(sampleFirstLine,
    "4th floor",
    "Vancouver",
    "BC",
    "A3G 4B2",
    type,
    "Canada")
}
println("Sample Canadian Address:\n${Address.canadianSample(AddressType.Billing)}")
Sample Canadian Address:
123 Fake Street
4th floor
Vancouver, BC A3G 4B2
CANADA
Address canadian = Address.canadianSample(AddressType.Shipping);
System.out.println(canadian);

Address canadian = Address.Companion.canadianSample(AddressType.Shipping);
System.out.println(canadian);
@JvmStatic 
fun canadianSample(type: AddressType): Address { ... }
123 Fake Street
4th floor
Vancouver, BC A3G 4B2
CANADA

Challenge

For this chapter’s challenge, you’ll create an insecure way to store credit card information and access it from Java:

Key points

  • Kotlin was designed from the beginning to be compatible with the JVM, and Kotlin bytecode can run anywhere that Java bytecode runs.
  • You can intermix Kotlin and Java code within one project.
  • It’s possible to add Kotlin extension functions to classes written in Java, and also to call Kotlin free functions from Java code.
  • Annotations like @JvmOverloads and @JvmStatic help you integrate your Java and Kotlin code.

Where to go from here?

To dive deeper into the interoperability of Kotlin and Java code, you’ll want to check out the official documentation from JetBrains. If you’re an Android developer, you’ll also want to check out the interop guide created by Google:

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now