Skip to Content
Course content

48: Calling Java from Kotlin and Vice Versa

Click on the "Edit" button in the top corner of the screen to edit your slide content.

In the real world, you're rarely starting a project from a completely blank slate. More often than not, you're dropping Kotlin into a massive existing Java codebase or using a library that was written a decade ago in Java. The good news is that Kotlin was designed specifically to be "interoperable," which is a fancy way of saying they play very well together. They both compile to JVM bytecode, so as far as the virtual machine is concerned, they're speaking the same language.

To see this in action, let's imagine we're working on a retail app. We have a legacy Java class that handles loyalty point calculations—it's old, it's tested, and nobody wants to rewrite it. But we're building the new Customer Dashboard in Kotlin.

Dealing with the Legacy: Our Java Loyalty Engine

First, let's look at our Java class. I've kept it simple, but notice the classic Java patterns: private fields with public getters and setters.

// LoyaltyCalculator.java
public class LoyaltyCalculator {
    private double multiplier = 1.5;

    public double getMultiplier() {
        return multiplier;
    }

    public void setMultiplier(double multiplier) {
        this.multiplier = multiplier;
    }

    public int calculatePoints(int spendAmount) {
        return (int) (spendAmount * multiplier);
    }
}

Bringing Java into the Kotlin Fold

Now, let's use this in Kotlin. One of the best parts about this interop is that Kotlin treats Java getters and setters as properties. You don't have to call getMultiplier(); you just access multiplier.

// DashboardManager.kt
fun main() {
    val calculator = LoyaltyCalculator()
    
    // I can treat the Java getter/setter as a property!
    calculator.multiplier = 2.0 
    
    val points = calculator.calculatePoints(100)
    println("Customer earned $points points!")
}

It feels native, right? But there's a catch when we start moving in the other direction—calling Kotlin from Java.

The "Companion" Hiccup

I wanted to create a utility class in Kotlin to format the points for the UI, and I thought I'd use a companion object since that's where we usually put "static-like" methods in Kotlin. Here's what I wrote:

// PointsFormatter.kt
class PointsFormatter {
    companion object {
        fun format(points: Int): String {
            return "$points Loyalty Points"
        }
    }
}

Then, I tried to call it from a Java class, expecting it to work like a static method:

// LegacyReport.java
public class LegacyReport {
    public void printReport(int points) {
        // ERROR: Cannot resolve method 'format' in PointsFormatter
        String text = PointsFormatter.format(points); 
        System.out.println(text);
    }
}

I hit a wall here. I forgot that Kotlin doesn't actually have static methods. The companion object is actually a singleton instance inside the class. To fix this, I have two choices: I could call PointsFormatter.Companion.format(points) in Java, but that looks ugly and leaks Kotlin's implementation details into Java. The professional way to handle this is using the @JvmStatic annotation.

// PointsFormatter.kt (Corrected)
class PointsFormatter {
    companion object {
        @JvmStatic 
        fun format(points: Int): String {
            return "$points Loyalty Points"
        }
    }
}

Adding @JvmStatic tells the Kotlin compiler to generate a real static method in the bytecode. Now, the Java code PointsFormatter.format(points) works perfectly.

Handling Nullability Across the Border

One last thing to keep in mind: Java doesn't have Kotlin's strict null safety. When you call a Kotlin function from Java, Java can pass null into a parameter that you've marked as non-nullable. This can lead to a NullPointerException the moment the code enters the Kotlin function.

If you're calling Kotlin from a Java codebase you don't trust, you can use @NotNull or @Nullable annotations (from JetBrains or JSR-305). This helps the Kotlin compiler understand the Java intent and helps the Java IDE warn the developer before they pass a null value.




📋 Practical Task

Exercise: Bridging the Currency Converter

You are integrating a legacy Java CurrencyConverter into a new Kotlin FinanceApp. However, the bridge is currently broken due to nullability and visibility issues.

Your Task:

  1. Fix the Java call: In FinanceApp.kt, you are trying to call converter.getExchangeRate(). Change this to use Kotlin's property syntax instead of the explicit getter.
  2. Fix the Kotlin utility: The CurrencyUtils.kt file has a companion object method called formatCurrency(). The Java class LegacyAudit.java is failing to compile because it tries to call CurrencyUtils.formatCurrency(amount) as a static method. Add the necessary annotation to the Kotlin function to make this work.
  3. Null Safety: The Java method converter.setBaseCurrency(String currency) is being called with a null value in LegacyAudit.java, but the Kotlin side expects a non-nullable String. Update the Kotlin function signature to allow a nullable String? and add a null check that returns "Unknown Currency" if the input is null.

Starter Code:

// CurrencyConverter.java
public class CurrencyConverter {
    private double rate = 1.2;
    public double getExchangeRate() { return rate; }
    public void setBaseCurrency(String currency) { /* logic */ }
}

// CurrencyUtils.kt
class CurrencyUtils {
    companion object {
        fun formatCurrency(double amount) = "$$amount"
    }
}

// FinanceApp.kt
fun main() {
    val converter = CurrencyConverter()
    println(converter.getExchangeRate()) // FIX THIS
}

// LegacyAudit.java
public class LegacyAudit {
    public void audit(double amount) {
        System.out.println(CurrencyUtils.formatCurrency(amount)); // FAILS TO COMPILE
    }
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.