Kotlin
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Null Safety
-
Section 4: Object-Oriented Kotlin
-
Section 5: Functional Kotlin
-
Section 6: Coroutines
-
Section 7: Collections Deep Dive
-
Section 8: Type System Deep Dive
-
Section 9: Interop and Tooling
-
Section 10: Kotlin DSLs and Patterns
-
Section 11: Testing and Quality
-
Section 12: Server-Side Kotlin
-
Section 13: Practical Projects
-
Section 14: Interview Practice
-
Section 15: More Practice Exercises
-
Section 16: More Standard Library
-
Section 17: Multiplatform Kotlin
-
Section 18: kotlin.collections In Depth
-
Section 19: kotlin.text In Depth
-
Section 20: kotlin.ranges and kotlin.sequences
-
Section 21: kotlin.io and File Handling
-
Section 22: kotlinx.coroutines Deep Dive
-
Section 23: kotlin.reflect
-
Section 24: Android Development with Kotlin Overview
-
Section 25: Kotlin Multiplatform Deep Dive
-
Section 26: Kotlin for Backend Deep Dive
-
Section 27: Kotlin Design Patterns
-
Section 28: Advanced Language Features
-
Section 29: More Practice Exercises
-
Section 30: More Interview Practice
-
Section 31: Kotlin Type System Deep Dive
-
Section 32: Kotlin Null Safety Advanced
-
Section 33: Kotlin Testing Deep Dive
-
Section 34: Kotlin Build Tooling Deep Dive
-
Section 35: Kotlin Serialization
-
Section 36: Kotlin Performance Considerations
-
Section 37: Kotlin Native Overview
-
Section 38: Kotlin for Data and Scripting
-
Section 39: More Coroutines Practice
-
Section 40: More Android-Adjacent Patterns
-
Section 41: More Practical Projects
-
Section 42: More Design and Architecture Practice
-
Section 43: Kotlin Language Evolution
-
Section 44: More Interview and Review
-
Section 45: Kotlin Delegation Patterns Deep Dive
-
Section 46: Kotlin Annotations Deep Dive
-
Section 47: Kotlin for Gradle Plugin Development
-
Section 48: Kotlin Concurrency Beyond Coroutines
-
Section 49: Kotlin Compiler Internals
-
Section 50: Real-World Kotlin Case Studies
-
Section 51: Final Practice Projects
-
Section 52: Kotlin for Server-Side Reactive Programming
-
Section 53: More Practice and Drills
-
Section 54: Kotlin Security Practices
150: Nullability Annotations for Java Interop
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?).
There are no comments for now.