Why Kotlin Won Me Over After Java

java dev.to

I still remember the exact feeling of sitting down to write my first "real" program and having absolutely no idea which language deserved my time. There's a particular kind of paralysis that hits when you're new to programming and every forum, every YouTube video, and every well-meaning senior seems to be pushing a different answer. For a while, I resisted picking based on hype. It felt important, somehow, that I choose something because it actually fit the way I thought, not because it was what everyone else was learning that semester.

And at that moment, the overwhelming answer from almost everyone around me was the same: learn JavaScript. It made sense on paper — it's everywhere. React on the frontend, Node on the backend, a new framework announced seemingly every month, entire career paths built around a single language. So I gave it an honest, sustained attempt. I worked through tutorials, built a couple of small toy projects, tried to convince myself I liked it. But something about the syntax never settled with me the way I wanted it to. Variables that could quietly change type, values that behaved unexpectedly depending on context, a flexibility that felt less like freedom and more like uncertainty. I kept finding myself wanting more structure underneath my feet, something that told me clearly, at every step, what was actually happening and why.

That's roughly where I was mentally when college introduced me to Java, and I still remember being a little surprised by how much I liked it. There was nothing flashy about the introduction — a fairly standard third-year course, mostly console programs and simple class hierarchies. But something about the discipline of it appealed to me immediately. Classes, objects, interfaces, inheritance, access modifiers — the entire object-oriented model made intuitive sense in a way JavaScript's looser style never had. I could read someone else's Java code and reason about it. I could predict what a method would do before running it. For someone who was still building confidence as a programmer, that predictability mattered enormously. It gave me something solid to stand on.

But the honeymoon period with Java had a natural expiration date, and it arrived quietly. It wasn't that the concepts became difficult. Object-oriented thinking still made sense to me, and I wasn't struggling to grasp anything new. What started wearing on me instead was the sheer amount of scaffolding required to say something simple. I'd sit down with a clear, small idea in my head, a class to hold a user's name and age, nothing more and by the time I'd written the constructor, the getters, the setters, an equals() override, and a toString(), I'd produced thirty or forty lines of code to express something that took about five seconds to think through. I remember genuinely asking myself, more than once, why am I writing this much just to say something this simple? It wasn't frustration with Java as a language exactly. It was frustration with how much of my typing had nothing to do with my actual problem.

That question sat with me for a while before I stumbled onto Kotlin. I'd heard the basics before actually trying it — built by JetBrains, running on the JVM so it could sit comfortably alongside existing Java code, and increasingly the language Google was pushing hard for Android development. Given how attached I already was to Java's structure, I went in expecting something unfamiliar, maybe even something that would force me to relearn habits I'd only just built. What I found instead was almost the opposite. It felt like someone had taken the Java I already understood, sat down with it, and asked one deceptively simple question: what if we removed everything that didn't need to be there? That single question, more than any individual feature, is what actually hooked me — and it's the thread that runs through everything else I want to talk about here.

The first thing that stood out: doing more with less

The gap became obvious almost immediately. A basic data-holding class in Java takes real estate:

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 int getAge() { return age; }
}
Enter fullscreen mode Exit fullscreen mode

In Kotlin, the same idea collapses into one line of intent:

data class User(
    val name: String,
    val age: Int
)
Enter fullscreen mode Exit fullscreen mode

The constructor, accessors, equals(), hashCode(), and toString() are all generated for me. What struck me wasn't just the line count — it was that the Kotlin version reads like a statement of what the class is for, rather than a checklist of boilerplate I have to maintain by hand. That became my first real glimpse of Kotlin's underlying philosophy: the language should help me say what I mean, not force me to keep re-describing the mechanics behind it.

Null safety rewired how I think about my code

The feature that changed my habits the most was null safety. In Java, any reference can silently be null, and you usually find out the hard way — at runtime, via a NullPointerException you didn't see coming.

String name = getName();
if (name != null) {
    System.out.println(name.length());
}
Enter fullscreen mode Exit fullscreen mode

Kotlin folds nullability directly into its type system:

val name: String? = getName()
println(name?.length)
Enter fullscreen mode Exit fullscreen mode

And when I know a value can never be absent, I can say so outright:

val name: String = "Trishit"
Enter fullscreen mode Exit fullscreen mode

The distinction between String and String? isn't cosmetic — it forces the question earlier in the process. Instead of writing code and then remembering to guard against null, I started asking upfront: can this even be null? That's a better question to be asking, and it quietly improved the design of everything I wrote afterward.

val and var made intent visible at a glance

A smaller feature I initially underrated: the explicit split between val and var.

val name = "Trishit"
var age = 21
Enter fullscreen mode Exit fullscreen mode

Java gets you there with final, but it reads as an afterthought:

final String name = "Trishit";
int age = 21;
Enter fullscreen mode Exit fullscreen mode

In Kotlin, immutability isn't bolted on — it's the default posture of the language. Seeing val user = getUser() tells me instantly that user isn't going anywhere. It nudges you toward more intentional state management without making immutable code feel like extra effort.

Functions got noticeably lighter

Java methods are readable enough, but Kotlin trims the ceremony further:

public int square(int number) {
    return number * number;
}
Enter fullscreen mode Exit fullscreen mode
fun square(number: Int): Int {
    return number * number
}
Enter fullscreen mode Exit fullscreen mode

And for something this simple, Kotlin lets you collapse it into a single expression:

fun square(number: Int) = number * number
Enter fullscreen mode Exit fullscreen mode

That single-expression form appealed to me because it mirrors how I actually think about small transformations — input in, output out, nothing else to track.

String templates: a small convenience that adds up

System.out.println("Hello, " + name + ". You are " + age + " years old.");
Enter fullscreen mode Exit fullscreen mode
println("Hello, $name. You are $age years old.")
Enter fullscreen mode Exit fullscreen mode

On its own, this is a minor convenience. But Kotlin is full of small conveniences like this, and they compound. Over time I noticed I was spending less mental energy fighting syntax and more of it actually thinking about the problem I was solving.

Extension functions felt like a small superpower

Extension functions were one of the features that genuinely surprised me. Rather than writing a free-floating utility function:

fun isValidEmail(value: String): Boolean {
    // ...
}
Enter fullscreen mode Exit fullscreen mode

I could attach the behavior directly to the type:

fun String.isValidEmail(): Boolean {
    // ...
}
Enter fullscreen mode Exit fullscreen mode

and call it naturally:

email.isValidEmail()
Enter fullscreen mode Exit fullscreen mode

I never had to touch the original String class — I just extended what it could do, from my own code. This turned out to be especially handy when working with Android APIs and collection types that I couldn't otherwise modify.

Collections stopped feeling like a chore

This was one of the biggest practical wins for everyday code. Java's Stream API gets the job done, but it reads like ceremony:

List<String> names = users.stream()
        .filter(user -> user.getAge() >= 18)
        .map(User::getName)
        .collect(Collectors.toList());
Enter fullscreen mode Exit fullscreen mode

Kotlin's equivalent reads almost like plain English:

val names = users
    .filter { it.age >= 18 }
    .map { it.name }
Enter fullscreen mode Exit fullscreen mode

Filter the users, then map them to names — that's literally what the code says. And Kotlin's standard library goes much further than filter and map:

val adults = users.filter { it.age >= 18 }
val names = users.map { it.name }
val oldest = users.maxByOrNull { it.age }
val grouped = users.groupBy { it.age }
val counts = users.groupingBy { it.name }.eachCount()
Enter fullscreen mode Exit fullscreen mode

Once these became second nature, a huge amount of everyday data-shuffling got dramatically easier to express and read back later.

when outclassed the traditional switch

Java's switch is functional, but Kotlin's when quickly became one of my favorite constructs:

switch (state) {
    case LOADING:
        ...
        break;
    case SUCCESS:
        ...
        break;
    case ERROR:
        ...
        break;
}
Enter fullscreen mode Exit fullscreen mode
when (state) {
    State.Loading -> ...
    is State.Success -> ...
    is State.Error -> ...
}
Enter fullscreen mode Exit fullscreen mode

And unlike switch, when can directly produce a value:

val message = when (state) {
    State.Loading -> "Loading..."
    is State.Success -> "Done"
    is State.Error -> "Something went wrong"
}
Enter fullscreen mode Exit fullscreen mode

Paired with sealed types, this becomes genuinely powerful.

Sealed classes made UI state modeling feel obvious

Once I moved deeper into Android work, managing UI state became unavoidable. A typical model looks like:

sealed interface UiState {
    data object Loading : UiState
    data class Success(val data: List<User>) : UiState
    data class Error(val message: String) : UiState
}
Enter fullscreen mode Exit fullscreen mode

And handling it:

when (val state = uiState) {
    UiState.Loading -> showLoading()
    is UiState.Success -> showUsers(state.data)
    is UiState.Error -> showError(state.message)
}
Enter fullscreen mode Exit fullscreen mode

What I appreciated most is that the compiler actively helps enforce exhaustiveness — it's much harder to accidentally forget a state. This pairing became essential once I started building with Jetpack Compose, ViewModel, and StateFlow.

Smart casts quietly removed a layer of noise

Java requires an explicit cast even after a type check:

if (user instanceof Admin) {
    Admin admin = (Admin) user;
    admin.deleteUser();
}
Enter fullscreen mode Exit fullscreen mode

Kotlin just remembers what you already established:

if (user is Admin) {
    user.deleteUser()
}
Enter fullscreen mode Exit fullscreen mode

It's a small example, but it reflects a pattern I ran into constantly — Kotlin frequently already knows something I've told it, and instead of making me repeat myself, it just uses that information.

Named and default arguments cleaned up messy APIs

Java often reaches for multiple overloaded constructors, or a full builder pattern, just to handle a few optional parameters. Kotlin handles it directly:

fun createUser(
    name: String,
    age: Int = 18,
    active: Boolean = true
)
Enter fullscreen mode Exit fullscreen mode
createUser("Trishit")
createUser("Trishit", 22)
createUser("Trishit", active = false)
Enter fullscreen mode Exit fullscreen mode

Named arguments are particularly useful once a function has several parameters of the same type. Compare the ambiguity of:

createUser("Trishit", 22, false)
Enter fullscreen mode Exit fullscreen mode

against the clarity of:

createUser(
    name = "Trishit",
    age = 22,
    active = false
)
Enter fullscreen mode Exit fullscreen mode

The call site explains itself without needing a comment.

Lambdas made functional-style code feel approachable

Kotlin lowered the barrier to functional programming for me considerably:

val numbers = listOf(1, 2, 3, 4, 5)
val doubled = numbers.map { it * 2 }
Enter fullscreen mode Exit fullscreen mode
numbers.filter { it % 2 == 0 }
Enter fullscreen mode Exit fullscreen mode

The implicit it keeps simple lambdas lightweight without becoming cryptic:

users.filter { it.age >= 18 }
Enter fullscreen mode Exit fullscreen mode

Though I learned there's a limit here — leaning too hard on compact syntax everywhere can hurt readability rather than help it. Kotlin gives you a lot of expressive power, but that doesn't mean every feature belongs in every line. Readable code isn't necessarily the shortest code — a lesson that took a few messy files to really internalize.

Scope functions took real effort to click

Kotlin's scope functions — apply, let, run, also, with — confused me at first.

val user = User(
    name = "Trishit",
    age = 21
).apply {
    // configure object
}
Enter fullscreen mode Exit fullscreen mode
user?.let {
    println(it.name)
}
Enter fullscreen mode Exit fullscreen mode

Early on, these felt like unnecessary cleverness layered on top of the language. Eventually I understood they were purpose-built tools for expressing specific, common patterns more cleanly — configuring an object, running a null-safe block, chaining transformations. That understanding came with a corollary that's stuck with me since: Kotlin can be extremely readable, or it can become a puzzle, depending entirely on restraint. The language offers many ways to write the same thing; good Kotlin is knowing when to stop reaching for cleverness.

Data classes kept paying off

This deserves a second mention because I ended up leaning on data classes constantly. Compare a simple immutable point in Java:

public class Point {
    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() { return x; }
    public int getY() { return y; }

    // equals(), hashCode(), toString()
}
Enter fullscreen mode Exit fullscreen mode

against Kotlin:

data class Point(
    val x: Int,
    val y: Int
)
Enter fullscreen mode Exit fullscreen mode

with useful behavior generated automatically:

val p1 = Point(10, 20)
val p2 = p1.copy(x = 30)

println(p1)
println(p1 == p2)
Enter fullscreen mode Exit fullscreen mode

For application work — modeling API responses, UI state, database entities, or domain objects — this convenience shows up constantly, not just in toy examples.

Coroutines changed how I approached asynchronous code

Once I moved deeper into Android, asynchronous programming stopped being optional. Kotlin coroutines gave me a way to write async logic that reads sequentially instead of nesting into callback pyramids:

viewModelScope.launch {
    val user = repository.getUser()
    val posts = repository.getPosts(user.id)

    updateUi(user, posts)
}
Enter fullscreen mode Exit fullscreen mode

The code reads almost exactly like the order of operations in my head. To be clear, coroutines aren't just "threads made easy" — there's real depth to understand around structured concurrency, dispatchers, cancellation, and scope lifecycles. But the syntax itself never forces asynchronous logic to become unreadable, and that alone was a significant shift for me.

Kotlin stopped feeling like a list of features

This was probably the most important shift in how I saw the language. At first, I was cataloguing individual selling points: null safety, data classes, extension functions, coroutines, smart casts. Eventually I stopped counting features and started noticing the combined effect.

Models became concise. Functions became concise. Nullability became explicit rather than implicit. Collections could be transformed cleanly in a line or two. State could be modeled with sealed types the compiler actually checks. Asynchronous code stayed readable. And the compiler kept using information I'd already given it instead of asking me to repeat it. All of this sat inside an ecosystem whose underlying concepts were already familiar from Java. That combination — familiar foundations, dramatically less friction — is what made Kotlin stick.

From Kotlin to Android

Once Kotlin felt natural, Android became genuinely interesting to explore. Google's strong Kotlin-first support made the decision easy, and discovering Jetpack Compose alongside the newer Material design system was where things really started fitting together.

My first serious project was a simple calculator app — nothing ambitious on paper. What mattered was everything that happened while building it: wrestling with state management, configuration changes, recomposition behavior, and the small UI glitches that only show up once you're trying to actually understand a framework rather than just follow a tutorial along. I had to work out why a state change triggered recomposition, why some values survived a configuration change and others didn't, and why the UI sometimes behaved differently than I expected. Bit by bit, I wasn't just learning how to make Android work — I was learning why it worked the way it did.

Java isn't the villain here

I don't want any of this to read as a Java-bashing exercise, because it isn't one. Java is the language that taught me object-oriented programming in the first place, and that foundation is exactly what made Kotlin click as fast as it did. I already understood most of the concepts underneath it — Kotlin didn't ask me to unlearn anything, it just made those same ideas more concise and expressive. I still respect Java for the structure it taught me, structure I now recognize sitting quietly underneath a lot of well-written Kotlin.

What actually won me over

Looking back, I don't think any single feature is what made me fall for Kotlin. It was the reduction in friction between an idea in my head and the code that ended up on screen. With Java, I usually knew exactly what I wanted to express, but felt like I had to build scaffolding around it first. With Kotlin, the language increasingly got out of my way — concise without sacrificing structure, expressive without abandoning the object-oriented thinking I already valued, and modern enough that I kept discovering better ways to write the same logic.

That discovery eventually pulled me into Android, then Jetpack Compose, and further into the wider Kotlin ecosystem. And all of it started with one simple, slightly frustrated thought while staring at a wall of Java boilerplate:

There has to be a cleaner way to write this.

For me, Kotlin was that way.

Source: dev.to

arrow_back Back to Tutorials