Skip to Content
Course content

2: Kotlin vs Java: Key Differences

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

When I first started transitioning my teams from Java to Kotlin, the most common thing I heard was: "Oh, so Kotlin is basically just Java, but with less boilerplate." On the surface, that seems true. You write fewer lines of code, you lose the semicolons, and the syntax feels cleaner. But that's a dangerous way to think about it.

If you treat Kotlin as just "Java Lite," you'll end up writing Java code using Kotlin syntax, and you'll miss the actual power of the language. The biggest proof of this is how the two languages handle the "Billion Dollar Mistake": the NullPointerException (NPE).

In Java, any object can be null. You spend half your life writing if (user != null) { ... } blocks, and you still miss one, and the app crashes in production. In Kotlin, the type system itself knows whether a variable can be null or not. This isn't just a convenience; it's a compile-time guarantee.

"Kotlin is just a shorter version of Java" → Nullability is a first-class citizen

Look at this comparison. Imagine we're building a user profile system where the email is required, but the middle name is optional.

// Java
public class User {
    public String email; 
    public String middleName;
}

// This compiles, but will crash at runtime if middleName is null
User user = new User();
int length = user.middleName.length(); 

In Java, the compiler has no idea that middleName might be null. In Kotlin, we explicitly mark nullable types with a question mark.

// Kotlin
class User(
    val email: String,       // Cannot be null
    val middleName: String?  // Can be null
)

val user = User("dev@example.com", null)
// val length = user.middleName.length // This won't even compile!
val length = user.middleName?.length // Safe call: returns null if middleName is null

I love this because it shifts the burden of memory safety from my brain to the compiler. If it compiles, the null-handling is already accounted for.

"You need a verbose POJO for every data object" → Data classes automate state

If you've spent years in Java, you're probably used to the "POJO slog"—writing a class, then generating getters, setters, equals(), hashCode(), and toString(). It's mindless work that clutters your files.

Kotlin recognizes that most of our classes are just containers for data. By adding the data keyword, the compiler generates all that noise for you behind the scenes. It also gives you a copy() method, which is essential for immutable state management (something I highly recommend you embrace as you move forward).

// This one line does the work of about 50 lines of Java
data class Product(val id: Int, val name: String, val price: Double)

val laptop = Product(1, "MacBook", 1200.0)
val discountedLaptop = laptop.copy(price = 1100.0) // Easy immutability

"Utility classes are the only way to organize helpers" → Extension functions let you 'inject' logic

In Java, when you want to add a helper method to a class you don't own (like String or List), you end up with things like StringUtils.capitalize(myString). It's functional, but it feels disconnected.

Kotlin allows "Extension Functions." You can essentially add a method to an existing class without inheriting from it. It makes your code read like a natural sentence rather than a series of nested utility calls.

// Adding a custom validation to the existing String class
fun String.isValidEmail(): Boolean {
    return this.contains("@") && this.contains(".")
}

val email = "test@kotlin.com"
if (email.isValidEmail()) { // Looks like a member of the String class!
    println("Ready to go.")
}

I've found that this drastically reduces the number of "Utils" folders in my projects, which are usually just dumping grounds for code that doesn't have a clear home.




📋 Practical Task

Refactoring the Legacy User Profile

You have been handed a legacy Java-style class that is causing frequent NullPointerExceptions and is bloated with boilerplate. Your task is to refactor it into a modern Kotlin implementation.

The Legacy Java Code:

public class UserProfile {
    private String username;
    private String bio; // This can be null

    public UserProfile(String username, String bio) {
        this.username = username;
        this.bio = bio;
    }

    public String getUsername() { return username; }
    public String getBio() { return bio; }

    @Override
    public boolean equals(Object o) { /* ... tedious implementation ... */ return true; }
    @Override
    public int hashCode() { /* ... tedious implementation ... */ return 0; }
}

Requirements:

  • Convert UserProfile into a data class.
  • Ensure username is non-nullable and bio is nullable.
  • Create an extension function for the UserProfile class called getBioLength(). This function should return the length of the bio if it exists, or 0 if the bio is null.
  • In your main function, instantiate a UserProfile with a null bio and call your extension function to verify it returns 0.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.