DEV Community

Cover image for Kotlin for Java Developers (Part 1)
Florian Röser
Florian Röser

Posted on

Kotlin for Java Developers (Part 1)

Java is the top dog among programming languages, and so I've seen several times java developers making the same kind of mistakes when using Kotlin.
Don't understand me wrong, it's not that these are bugs, but rather "code smells" when developers tend to develop in Kotlin as they are used to do in Java, not making use of Kotlin features.

This article should make you aware of the code smells I see most often and how you would ideally implement them in a "Kotlin way".

Part 1 of the series will cover

  • Make use of data classes
  • Leveraging Null Safety
  • Immutability By Default

(Disclosure: the header image is created using Dall-E as you can see on the broken typing in the background)

Make use of data classes

This is topic that might vanish soon since I experience more and more Java developers also having experience with record classes. Nonetheless, there are some differences between Java records and Kotlins data class.

Java way:

public class Person {
  private String name;
  private int age;

  public Person(String name, int age) {
    this.name = name;
    this.age = age;
  }
  // Getters, setters, ...
}
Enter fullscreen mode Exit fullscreen mode

or as a record:

public record Person(
    String name,
    int age
) {
}
Enter fullscreen mode Exit fullscreen mode

Kotlin way:

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

There are some differences between Java record and Kotlin data class that you might want to know about.

  • Both, Java record and Kotlin data class are immutable data carriers.
  • In Java, the fields are implicitly final and cannot be modified after construction, while in Kotlin, you can choose whether you want to make the fields mutable or not by using val or var.
  • Another difference is that record classes in Java are implicitly final and sealed, which means that they cannot be extended, while in Kotlin you can extend data classes.
  • Also in Kotlin you can override the equals, hashCode and toString methods, which is not possible in Java.
  • Kotlin provides a copy method out of the box, which is not available in Java.

Some examples:

Copying objects in Java

Person p2 = new Person(p1.getName(), p1.getAge());
Enter fullscreen mode Exit fullscreen mode

Kotlin:

val p2 = p1.copy(age = 42)
Enter fullscreen mode Exit fullscreen mode

Or destructuring declarations in Java:

String name = p1.getName();
int age = p1.getAge();
Enter fullscreen mode Exit fullscreen mode

Kotlin:

val (name, age) = p1
println(name) // "John"
println(age) // 42
Enter fullscreen mode Exit fullscreen mode

References


Leveraging Null Safety

In my opinion, null-safety in Kotlin is one of the most powerful features. It's a game changer and can save you a lot of time and headaches.
In Kotlin, null safety is built into the type system, which makes it easier to avoid null-related runtime errors.

1. Nullable Types

In Kotlin, nullable types are explicitly declared. This means you can have a variable that might hold a null value, but you must specify it explicitly in the declaration.

Non-nullable types (default behaviour)

By default, all types in Kotlin are non-nullable, this means, that a variable cannot hold a null value.

val name: String = "John" // non-nullable
name = null // Compilation error!
Enter fullscreen mode Exit fullscreen mode

Nullable types

To declare a variable that can hold a null value, you have to use the ? operator.

val name: String? = null // nullable
Enter fullscreen mode Exit fullscreen mode

2. Safe Calls

A powerful feature is the safe call operator ?.. It allows you to safely call a method or access a property without throwing a NullPointerException.

Example

val name: String? = null
println(name?.length) // Prints null instead of throwing an exception
Enter fullscreen mode Exit fullscreen mode

The ?. operator checks if the object is null and if it is, it returns null immediately, otherwise it proceeds to call the method or access the property. If the object is null, the entire expression evaluates to null.

3. Elvis Operator (?:)

The Elvis operator ?: is a shorthand for returning a default value if the expression to the left of the operator is null.

val name: String? = null
val length = name?.length ?: 0 // if name is null, default is 0
println(length) // 0
Enter fullscreen mode Exit fullscreen mode

4. The !! Operator (Not-Null Assertion)

You can use the !! operator to tell the compiler that the value is not null. If the value is null, it will throw a NullPointerException.

val name: String? = null
println(name!!) // Throws NullPointerException
Enter fullscreen mode Exit fullscreen mode

HINT:
It's not recommended to use this !! operator because it defeats the purpose of null safety.

5. Nullability in Function Parameters

When you define a function, you can specify whether a parameter can be null or not. In that case, the caller has to handle it.

fun greet(name: String?) {
    println("Hello, ${name ?: "Guest"}")
}
greet(null) // Hello, Guest
greet("Alice") // Hello, Alice
Enter fullscreen mode Exit fullscreen mode

6. Safe Casts (as? operator)

There is a safe cast operator as? that returns null if the cast is not possible.

val obj: Any = "Kotlin"
val str: String? = obj as? String
println(str) // Prints "Kotlin"

val num: Int? = obj as? Int
println(num) // Prints null
Enter fullscreen mode Exit fullscreen mode

7. Null safety in Lambdas

You can also use the null safety features in lambdas and higher functions:

val list: List<String?> = listOf("Kotlin", null, "Java")
val lengths = list.map { it?.length ?: 0 }
println(lengths) // Prints [6, 0, 4]
Enter fullscreen mode Exit fullscreen mode

8. Utilize let Function

The let function is a scope function that allows you to execute a block of code on a non-null object. It's typically used for executing code on a nullable object in a safe way.

Example also with default values:

val name: String? = null

val result = name?.let {
    println("Name is not null: $it")
    it.length // this won't be executed because name is null
} ?: "Default value"

println(result) // Prints "Default value"
Enter fullscreen mode Exit fullscreen mode

9. Best Practices

  • Avoid using the !! operator
  • Use save calls and the Elvis operator to safely handle nullable types and provide default values
  • Use nullable types thoughtfully and only when necessary

References:


Immutability By Default

Kotlin strongly encourages a functional programming style!
For a functional programming style, immutability plays a crucial role in avoiding bugs, specially in multi-threaded applications.

Maybe I'm going to write a separate article about functional programming in Kotlin or Java, but for now, let's focus on immutability.

Kotlin inherently favors immutable objects over mutable ones. This leads to simpler, more predictable code, especially in a concurrent environment.

1. Immutable Variables by Default (val)

In Kotlin, variables are immutable by default when declared using the val keyword. This quite close to declaring a final variable in Java, but with a few key differences:

  • A val variable in Kotlin is effectively read-only - the value assigned to it cannot be changed after initialisation.
  • However, if the value is an object, mutating the properties of that object is still possible, unless those properties are declared as val themselves.

*Example:

val name = "Kotlin"
// name = "Java" // Compilation error!
Enter fullscreen mode Exit fullscreen mode

Difference from Java:
In Java, we use the final keyword to ensure that a variable can't be reassigned, but the object it points to can still be mutable. The key difference in Kotlin is that immutability extends to variables by default, encouraging a more predictable and safe design for the entire application.

Example of a mutable variable:
Using the var keyword in Kotlin allows you to reassign the variable.

var age = 42
age = 43 // No compilation error
Enter fullscreen mode Exit fullscreen mode

HINT:
Kotlin encourages to use val over var whenever possible to ensure immutability.

2. Immutable Collections

It's also encouraged to work with immutable collections by default. Immutable collections prevent any modification after creation, for example, if you create a List using listOf(), it cannot be changed, no elements can be added, removed ore altered.

val numbers = listOf(1, 2, 3)
numbers.add(4) // Compilation error!
Enter fullscreen mode Exit fullscreen mode

If you need to modify a collection, you can use mutableListOf() or other mutable collection types.

val mutableNumbers = mutableListOf(1, 2, 3)
mutableNumbers.add(4) // Allowed
Enter fullscreen mode Exit fullscreen mode

Differences from Java:
In Java, collections, such as ArrayList are mutable by default, which means that the elements can be modified freely.

3. Immutable Data Classes

Kotlin's data class are immutable by default. When defining a data class, properties are typically declared as val, making the class immutable. This makes the classes great for value objects, especially when working with APIs, database records, or any other scenario where object's state should not change after creation.

data class Person(val name: String, val age: Int)
Enter fullscreen mode Exit fullscreen mode
val person = Person("Alice", 42)
person.name = "Bob" // Compilation error!
Enter fullscreen mode Exit fullscreen mode

4. Immutability in Sealed Classes

Kotlin's sealed classes can also be immutable, and they work well with immutable data models. Sealed classes are often used to represent restricted class hierarchies, like states or responses, and their immutability ensures that the state or result doesn't change unexpectedly.

sealed class Response
data class Success(val data: String) : Response()
data class Error(val message: String) : Response()

val response: Response = Success("Data loaded successfully")
response = Error("Something is wrong") // Compilation error! This would require reassignment.
Enter fullscreen mode Exit fullscreen mode

Interested? Some more Kotlin features are covered in Part 2 of the series

Image of Timescale

🚀 pgai Vectorizer: SQLAlchemy and LiteLLM Make Vector Search Simple

We built pgai Vectorizer to simplify embedding management for AI applications—without needing a separate database or complex infrastructure. Since launch, developers have created over 3,000 vectorizers on Timescale Cloud, with many more self-hosted.

Read more →

Top comments (0)