Skip to Content
Course content

70: Comparing Kotlin Idioms to Java Equivalents

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

If you've spent a few years in the Java ecosystem, you probably have "Java brain." There's nothing wrong with that—Java taught us a lot about structure and types—but when you first move to Kotlin, it's incredibly easy to write "Java with Kotlin syntax." I see it all the time in code reviews: a developer uses val and var, but they're still writing nested null checks and manual for-loops. It works, and the compiler is happy, but you're missing out on the actual power of the language.

Fighting the Nulls with Java Habits

In Java, we're conditioned to be terrified of NullPointerException, so we write defensive guards. If I were writing this in Java, I'd probably check if a user object exists, then check if their profile exists, and then finally grab the email. In naive Kotlin, you might see something like this:

if (user != null) {
    val profile = user.profile
    if (profile != null) {
        val email = profile.email
        if (email != null) {
            sendEmail(email)
        }
    }
}

This is what I call "The Pyramid of Doom." It's visually noisy and forces the reader to keep track of three levels of indentation just to find the actual business logic. In idiomatic Kotlin, we treat nullability as a first-class citizen using the safe-call operator and let. I'd rewrite that whole block as a single chain:

user?.profile?.email?.let { email -> 
    sendEmail(email) 
}

The trade-off here is a shift in mindset. Instead of "guarding" the code, you're "streaming" the data. If any link in that chain is null, the whole expression simply evaluates to null and the let block never executes. It's cleaner, and more importantly, it's harder to accidentally forget a null check because the type system forces your hand.

The Verbosity of Manual Loops

Another habit that lingers is the imperative approach to collections. I often see developers create a mutable list, loop through a source collection, check a condition, and manually add items to that new list. It's a pattern we've used for decades.

val activeUsers = mutableListOf<User>()
for (user in users) {
    if (user.isActive && user.age > 18) {
        activeUsers.add(user)
    }
}

While this is performant, it's purely procedural. It tells the computer how to do it, rather than what you want. Kotlin's standard library is designed to handle this functionally. I'd suggest using filter instead. It eliminates the need for a mutable temporary list and makes the intent immediately clear to anyone glancing at the code:

val activeUsers = users.filter { it.isActive && it.age > 18 }

Now, you might worry about the overhead of creating intermediate collections. In most business applications, that cost is negligible compared to the gain in readability. If you're working with a massive dataset where performance is critical, you can just slap .asSequence() onto the front of the chain to make it lazy, giving you the best of both worlds.

Cleaning up Object Configuration

Finally, let's talk about object setup. In Java, we often have a series of setter calls after instantiating an object. Even in Kotlin, I see people doing this:

val request = ApiRequest()
request.url = "https://api.example.com"
request.timeout = 30
request.retryCount = 3
service.execute(request)

It's not "wrong," but it's repetitive. You're typing request over and over again. This is where the scope function apply becomes your best friend. It changes the scope of this to the object being configured, allowing you to group all the initialization logic together.

val request = ApiRequest().apply {
    url = "https://api.example.com"
    timeout = 30
    retryCount = 3
}
service.execute(request)

I prefer this because it visually encapsulates the configuration. When I see an apply block, I know exactly where the object setup begins and ends, and the resulting request variable is cleanly assigned the fully configured object in one expression.




📋 Practical Task

Refactoring the Legacy Order Processor

You've inherited a piece of code written by a developer who recently switched from Java to Kotlin. The code works, but it's written in a very imperative, non-idiomatic style. Your task is to refactor the processOrders function to use Kotlin idioms.

Requirements:

  • Replace the nested null checks with a safe-call chain and let.
  • Replace the for loop and mutable list with a filter and map chain.
  • Use apply to configure the OrderResult object.
data class Order(val id: String, val status: String?, val amount: Double?)
data class OrderResult(var orderId: String = "", var finalAmount: Double = 0.0, var processed: Boolean = false)

fun processOrders(orders: List<Order>): List<OrderResult> {
    val results = mutableListOf<OrderResult>()
    
    for (order in orders) {
        if (order.status != null) {
            if (order.status == "COMPLETED") {
                if (order.amount != null) {
                    val result = OrderResult()
                    result.orderId = order.id
                    result.finalAmount = order.amount
                    result.processed = true
                    results.add(result)
                }
            }
        }
    }
    return results
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.