Skip to Content
Course content

150: Nullability Annotations for Java Interop

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

If you've been working in a mixed codebase, you've probably noticed something frustrating: Kotlin's famous null safety seems to just... disappear when you call Java code. You'll see those weird types with exclamation marks—like User!—and you'll wonder why the compiler isn't forcing you to use ?. or !!. This is the "Platform Type" problem, and today we're going to fix it using annotations.

Wait, why isn't Kotlin forcing me to use a safe call on this Java method?

When Kotlin encounters a Java method, it has no idea if the return value is intended to be nullable or not, because Java's type system doesn't track that. Instead of forcing every single Java call to be nullable (which would be a nightmare for productivity), Kotlin treats them as platform types. Essentially, Kotlin says, "I don't know, so I'll let you decide."

Here is the danger. If you have a Java class like this:

public class UserProfileProvider {
    public String getBio(int userId) {
        // This might return null if the user hasn't written a bio
        return database.findBio(userId);
    }
}

In Kotlin, getBio returns String!. If you call it like this: val bio: String = provider.getBio(123), Kotlin lets it slide. But if that method actually returns null at runtime, your app crashes with a NullPointerException. You've effectively bypassed the biggest benefit of Kotlin.

How do I actually tell Kotlin a Java value can't be null?

The fix is to use nullability annotations in your Java code. Kotlin recognizes several different sets of annotations (like JetBrains, AndroidX, and JSR-305), but the logic is the same: you explicitly mark your parameters and return types.

Let's rewrite that provider. I'll use the JetBrains annotations here, but you'll see the same pattern everywhere:

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

public class UserProfileProvider {
    @Nullable
    public String getBio(@NotNull int userId) {
        return database.findBio(userId);
    }

    @NotNull
    public String getUserName(@NotNull int userId) {
        return database.findName(userId);
    }
}

Now, the magic happens on the Kotlin side. When you call these methods, the ! disappears. getBio is now seen as String?, and getUserName is seen as String. If you try to assign getBio to a non-nullable String variable, the Kotlin compiler will stop you right there. I always recommend doing this for any Java library you control; it saves hours of debugging.

Do these annotations actually change how Java works?

Short answer: No. These annotations are "metadata." They don't add any runtime checks to the Java bytecode itself. If a Java developer ignores the @NotNull annotation and passes a null anyway, the Java compiler won't stop them.

However, they are incredibly powerful for tool-assisted development:

  • Kotlin Compiler: Uses them to decide whether to force a null check.
  • IntelliJ IDEA: Will highlight the Java code in yellow and warn you, "Passing 'null' to parameter annotated as @NotNull."

I think of them as a contract. In pure Java, the contract is usually hidden in a Javadoc comment that nobody reads. With these annotations, the contract is baked into the code where the compiler can actually see it.




📋 Practical Task

Fixing the Legacy Payment Gateway Interop

You are integrating a legacy Java class PaymentGateway into a new Kotlin billing module. Currently, the Kotlin compiler is treating everything as a platform type, and the app is crashing because the getTransactionStatus method frequently returns null when a transaction is still pending.

Your Task: Add the correct @NotNull and @Nullable annotations to the PaymentGateway class so that the Kotlin compiler can enforce null safety. Ensure that transactionId is always required, getTransactionStatus can be null, and getGatewayVersion is always provided.

// Java Class
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

public class PaymentGateway {
    // TODO: Add annotation to return type
    public String getTransactionStatus(String transactionId) { 
        // TODO: Add annotation to parameter
        return statusMap.get(transactionId); 
    }

    // TODO: Add annotation to return type
    public String getGatewayVersion() {
        return "v2.4.1";
    }
}

Once you've annotated the Java class, explain how the Kotlin call val status: String = gateway.getTransactionStatus("TXN_123") would change (would it compile or fail?).

Rating
0 0

There are no comments for now.

to be the first to leave a comment.