Kotlin

Java to Kotlin: what to expect

·17 min read

After a decade of Java, I started picking up Kotlin — alongside Python, Go, and TypeScript. The feeling was what people describe as pain relief. Things that took 30 lines in Java took 3. Things that required entire libraries in Java were built into the language.

But not everything is rosy. There are gotchas — things that look the same but behave differently, things the documentation doesn't warn you about. This is what I wish I'd read before diving in.

What makes life easier right away

These are the features that made me go "wait, that's it?" within the first week.

Null safety — finally built in

In Java, null is a runtime bomb. In Kotlin, it's a compile-time error. The type system distinguishes between things that can be null and things that can't.

Kotlin
// Java — null is always lurking
String name = user.getName();
if (name != null) {
    int length = name.length(); // Still might be null?
}

// Kotlin — the compiler has your back
val name: String = user.name       // Can't be null, guaranteed
val nickname: String? = user.nick  // Can be null, marked with ?
val length = nickname?.length ?: 0 // Safe call + default

String and String? are different types. The compiler won't let you call methods on a nullable without checking first. This alone eliminates an entire category of NullPointerException bugs.

Data classes — goodbye boilerplate

The Java POJO: constructor, getters, setters, equals, hashCode, toString. About 30 lines for a simple object. In Kotlin? One line.

Kotlin
// Java: 30 lines for a POJO
public class User {
    private String name;
    private int age;
    public User(String name, int age) { this.name = name; this.age = age; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }
    @Override public boolean equals(Object o) { /* 10 lines */ }
    @Override public int hashCode() { /* 5 lines */ }
    @Override public String toString() { /* 3 lines */ }
}

// Kotlin: 1 line. All of the above, auto-generated.
data class User(val name: String, val age: Int)

when instead of switch

Java's switch is limited — integers, strings, enums. Kotlin's when is an expression that returns a value, supports ranges, type checks, and the compiler warns you if you miss a branch.

Kotlin
// Java switch — doesn't return a value
String result;
switch (status) {
    case "ACTIVE": result = "ok"; break;
    case "INACTIVE": result = "paused"; break;
    default: result = "unknown";
}

// Kotlin when — it's an expression!
val result = when (status) {
    "ACTIVE" -> "ok"
    "INACTIVE" -> "paused"
    in "PENDING".."REVIEW" -> "in progress"  // Ranges!
    else -> "unknown"
}

// Smart casts inside when
when (animal) {
    is Dog -> animal.bark()    // Smart cast to Dog
    is Cat -> animal.meow()    // Smart cast to Cat
}

Watch out for these gotchas

Now the part they don't put in the migration guide. These are the things that caused real bugs in production.

Gotcha 1: Companion object is not static

In Java, static means the method belongs to the class. In Kotlin, there's no static. Instead, there's companion object — a real singleton object with its own lifecycle. It's closer to an inner class instance than a static method.

Kotlin
class User(val name: String) {
    companion object {
        fun create(name: String): User = User(name)
    }
}

// Looks like a static call...
User.create("Alice")

// But it's actually a singleton object method:
// User.Companion.create("Alice")
// The Companion object has its own init, lifecycle, etc.

Gotcha 2: Collections are read-only, not immutable

This one catches everyone. listOf(1, 2, 3) in Kotlin is read-only, not truly immutable. Under the hood, it might still be a java.util.ArrayList. At the Java-Kotlin boundary, a Java method can mutate your "immutable" list.

Kotlin
// Kotlin: read-only view
val list = listOf(1, 2, 3)         // Can't add/remove... in Kotlin
val mutable = mutableListOf(1, 2, 3) // Can add/remove

// But under the hood:
// listOf() might return java.util.Arrays$ArrayList
// A Java method can still cast and mutate it!

// Safe approach at Java boundaries:
val safeList = listOf(1, 2, 3).toList() // Defensive copy

Gotcha 3: Coroutines are not threads

The biggest conceptual shift. A coroutine is not a thread. It's a lightweight unit of work that can suspend without blocking the thread. Using Thread.sleep() inside a coroutine blocks the entire thread — defeating the purpose.

Kotlin
// ❌ Bad — blocks the thread, defeats the purpose
suspend fun fetchUser(): User {
    Thread.sleep(5000)  // The thread is BLOCKED for 5 seconds
    return userRepository.find()
}

// ✅ Good — suspends coroutine, thread is FREE
suspend fun fetchUser(): User {
    delay(5000)  // The coroutine suspends, thread serves other coroutines
    return userRepository.find()
}

// With delay: one thread can handle 100,000+ coroutines
// With Thread.sleep: one thread handles ONE coroutine
Rendering diagram...

Practical tips for getting started with Kotlin:

  • Start with tests. Write new tests in Kotlin, don't touch existing Java.
  • One file at a time. IntelliJ converts Java to Kotlin in one click.
  • Don't use everything at once. Start with val/var, data classes, null safety. Coroutines and DSL later.
  • Hybrid projects work. Full Java-Kotlin interoperability. You can run both for years.
  • Listen to the compiler. Kotlin's compiler is stricter and gives better hints than Java's.

You don't need to rewrite the project. Kotlin was designed for seamless interoperability with Java — start writing new code in Kotlin and let both languages coexist.

— Summary
Pavel Ponomarev

About Me

I'm Pavel — a software engineer with 10+ years of experience building robust systems in Java, Kotlin, Python, Go, and TypeScript. Currently focused on AI agents, LLM workflows, and writing about what I learn along the way.

Stay in the loop

Get notified when I publish new articles about software craftsmanship, AI agents, and engineering empathy.

© 2026 Result Crafter Blog. Building software that works.