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, ...
}
or as a record:
public record Person(
String name,
int age
) {
}
Kotlin way:
data class Person(val name: String, var age: Int)
There are some differences between Java record
and Kotlin data class
that you might want to know about.
- Both, Java
record
and Kotlindata 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 usingval
orvar
. - Another difference is that
record
classes in Java are implicitlyfinal
andsealed
, which means that they cannot be extended, while in Kotlin you can extenddata class
es. - Also in Kotlin you can override the
equals
,hashCode
andtoString
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());
Kotlin:
val p2 = p1.copy(age = 42)
Or destructuring declarations in Java:
String name = p1.getName();
int age = p1.getAge();
Kotlin:
val (name, age) = p1
println(name) // "John"
println(age) // 42
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!
Nullable types
To declare a variable that can hold a null
value, you have to use the ?
operator.
val name: String? = null // nullable
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
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
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
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
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
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]
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"
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!
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
HINT:
Kotlin encourages to useval
overvar
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!
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
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)
val person = Person("Alice", 42)
person.name = "Bob" // Compilation error!
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.
Interested? Some more Kotlin features are covered in Part 2 of the series
Top comments (0)