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:
<span>public</span> <span>class</span> <span>Person</span> <span>{</span><span>private</span> <span>String</span> <span>name</span><span>;</span><span>private</span> <span>int</span> <span>age</span><span>;</span><span>public</span> <span>Person</span><span>(</span><span>String</span> <span>name</span><span>,</span> <span>int</span> <span>age</span><span>)</span> <span>{</span><span>this</span><span>.</span><span>name</span> <span>=</span> <span>name</span><span>;</span><span>this</span><span>.</span><span>age</span> <span>=</span> <span>age</span><span>;</span><span>}</span><span>// Getters, setters, ...</span><span>}</span><span>public</span> <span>class</span> <span>Person</span> <span>{</span> <span>private</span> <span>String</span> <span>name</span><span>;</span> <span>private</span> <span>int</span> <span>age</span><span>;</span> <span>public</span> <span>Person</span><span>(</span><span>String</span> <span>name</span><span>,</span> <span>int</span> <span>age</span><span>)</span> <span>{</span> <span>this</span><span>.</span><span>name</span> <span>=</span> <span>name</span><span>;</span> <span>this</span><span>.</span><span>age</span> <span>=</span> <span>age</span><span>;</span> <span>}</span> <span>// Getters, setters, ...</span> <span>}</span>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:
<span>public</span> <span>record</span> <span>Person</span><span>(</span><span>String</span> <span>name</span><span>,</span><span>int</span> <span>age</span><span>)</span> <span>{</span><span>}</span><span>public</span> <span>record</span> <span>Person</span><span>(</span> <span>String</span> <span>name</span><span>,</span> <span>int</span> <span>age</span> <span>)</span> <span>{</span> <span>}</span>public record Person( String name, int age ) { }
Enter fullscreen mode Exit fullscreen mode
Kotlin way:
<span>data class</span> <span>Person</span><span>(</span><span>val</span> <span>name</span><span>:</span> <span>String</span><span>,</span> <span>var</span> <span>age</span><span>:</span> <span>Int</span><span>)</span><span>data class</span> <span>Person</span><span>(</span><span>val</span> <span>name</span><span>:</span> <span>String</span><span>,</span> <span>var</span> <span>age</span><span>:</span> <span>Int</span><span>)</span>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 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
<span>Person</span> <span>p2</span> <span>=</span> <span>new</span> <span>Person</span><span>(</span><span>p1</span><span>.</span><span>getName</span><span>(),</span> <span>p1</span><span>.</span><span>getAge</span><span>());</span><span>Person</span> <span>p2</span> <span>=</span> <span>new</span> <span>Person</span><span>(</span><span>p1</span><span>.</span><span>getName</span><span>(),</span> <span>p1</span><span>.</span><span>getAge</span><span>());</span>Person p2 = new Person(p1.getName(), p1.getAge());
Enter fullscreen mode Exit fullscreen mode
Kotlin:
<span>val</span> <span>p2</span> <span>=</span> <span>p1</span><span>.</span><span>copy</span><span>(</span><span>age</span> <span>=</span> <span>42</span><span>)</span><span>val</span> <span>p2</span> <span>=</span> <span>p1</span><span>.</span><span>copy</span><span>(</span><span>age</span> <span>=</span> <span>42</span><span>)</span>val p2 = p1.copy(age = 42)
Enter fullscreen mode Exit fullscreen mode
Or destructuring declarations in Java:
<span>String</span> <span>name</span> <span>=</span> <span>p1</span><span>.</span><span>getName</span><span>();</span><span>int</span> <span>age</span> <span>=</span> <span>p1</span><span>.</span><span>getAge</span><span>();</span><span>String</span> <span>name</span> <span>=</span> <span>p1</span><span>.</span><span>getName</span><span>();</span> <span>int</span> <span>age</span> <span>=</span> <span>p1</span><span>.</span><span>getAge</span><span>();</span>String name = p1.getName(); int age = p1.getAge();
Enter fullscreen mode Exit fullscreen mode
Kotlin:
<span>val</span> <span>(</span><span>name</span><span>,</span> <span>age</span><span>)</span> <span>=</span> <span>p1</span><span>println</span><span>(</span><span>name</span><span>)</span> <span>// "John"</span><span>println</span><span>(</span><span>age</span><span>)</span> <span>// 42</span><span>val</span> <span>(</span><span>name</span><span>,</span> <span>age</span><span>)</span> <span>=</span> <span>p1</span> <span>println</span><span>(</span><span>name</span><span>)</span> <span>// "John"</span> <span>println</span><span>(</span><span>age</span><span>)</span> <span>// 42</span>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.
<span>val</span> <span>name</span><span>:</span> <span>String</span> <span>=</span> <span>"John"</span> <span>// non-nullable</span><span>name</span> <span>=</span> <span>null</span> <span>// Compilation error!</span><span>val</span> <span>name</span><span>:</span> <span>String</span> <span>=</span> <span>"John"</span> <span>// non-nullable</span> <span>name</span> <span>=</span> <span>null</span> <span>// Compilation error!</span>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.
<span>val</span> <span>name</span><span>:</span> <span>String</span><span>?</span> <span>=</span> <span>null</span> <span>// nullable</span><span>val</span> <span>name</span><span>:</span> <span>String</span><span>?</span> <span>=</span> <span>null</span> <span>// nullable</span>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
<span>val</span> <span>name</span><span>:</span> <span>String</span><span>?</span> <span>=</span> <span>null</span><span>println</span><span>(</span><span>name</span><span>?.</span><span>length</span><span>)</span> <span>// Prints null instead of throwing an exception</span><span>val</span> <span>name</span><span>:</span> <span>String</span><span>?</span> <span>=</span> <span>null</span> <span>println</span><span>(</span><span>name</span><span>?.</span><span>length</span><span>)</span> <span>// Prints null instead of throwing an exception</span>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
.
<span>val</span> <span>name</span><span>:</span> <span>String</span><span>?</span> <span>=</span> <span>null</span><span>val</span> <span>length</span> <span>=</span> <span>name</span><span>?.</span><span>length</span> <span>?:</span> <span>0</span> <span>// if name is null, default is 0</span><span>println</span><span>(</span><span>length</span><span>)</span> <span>// 0</span><span>val</span> <span>name</span><span>:</span> <span>String</span><span>?</span> <span>=</span> <span>null</span> <span>val</span> <span>length</span> <span>=</span> <span>name</span><span>?.</span><span>length</span> <span>?:</span> <span>0</span> <span>// if name is null, default is 0</span> <span>println</span><span>(</span><span>length</span><span>)</span> <span>// 0</span>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
.
<span>val</span> <span>name</span><span>:</span> <span>String</span><span>?</span> <span>=</span> <span>null</span><span>println</span><span>(</span><span>name</span><span>!!</span><span>)</span> <span>// Throws NullPointerException</span><span>val</span> <span>name</span><span>:</span> <span>String</span><span>?</span> <span>=</span> <span>null</span> <span>println</span><span>(</span><span>name</span><span>!!</span><span>)</span> <span>// Throws NullPointerException</span>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.
<span>fun</span> <span>greet</span><span>(</span><span>name</span><span>:</span> <span>String</span><span>?)</span> <span>{</span><span>println</span><span>(</span><span>"Hello, ${name ?: "</span><span>Guest</span><span>"}"</span><span>)</span><span>}</span><span>greet</span><span>(</span><span>null</span><span>)</span> <span>// Hello, Guest</span><span>greet</span><span>(</span><span>"Alice"</span><span>)</span> <span>// Hello, Alice</span><span>fun</span> <span>greet</span><span>(</span><span>name</span><span>:</span> <span>String</span><span>?)</span> <span>{</span> <span>println</span><span>(</span><span>"Hello, ${name ?: "</span><span>Guest</span><span>"}"</span><span>)</span> <span>}</span> <span>greet</span><span>(</span><span>null</span><span>)</span> <span>// Hello, Guest</span> <span>greet</span><span>(</span><span>"Alice"</span><span>)</span> <span>// Hello, Alice</span>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.
<span>val</span> <span>obj</span><span>:</span> <span>Any</span> <span>=</span> <span>"Kotlin"</span><span>val</span> <span>str</span><span>:</span> <span>String</span><span>?</span> <span>=</span> <span>obj</span> <span>as</span><span>?</span> <span>String</span><span>println</span><span>(</span><span>str</span><span>)</span> <span>// Prints "Kotlin"</span><span>val</span> <span>num</span><span>:</span> <span>Int</span><span>?</span> <span>=</span> <span>obj</span> <span>as</span><span>?</span> <span>Int</span><span>println</span><span>(</span><span>num</span><span>)</span> <span>// Prints null</span><span>val</span> <span>obj</span><span>:</span> <span>Any</span> <span>=</span> <span>"Kotlin"</span> <span>val</span> <span>str</span><span>:</span> <span>String</span><span>?</span> <span>=</span> <span>obj</span> <span>as</span><span>?</span> <span>String</span> <span>println</span><span>(</span><span>str</span><span>)</span> <span>// Prints "Kotlin"</span> <span>val</span> <span>num</span><span>:</span> <span>Int</span><span>?</span> <span>=</span> <span>obj</span> <span>as</span><span>?</span> <span>Int</span> <span>println</span><span>(</span><span>num</span><span>)</span> <span>// Prints null</span>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:
<span>val</span> <span>list</span><span>:</span> <span>List</span><span><</span><span>String</span><span>?></span> <span>=</span> <span>listOf</span><span>(</span><span>"Kotlin"</span><span>,</span> <span>null</span><span>,</span> <span>"Java"</span><span>)</span><span>val</span> <span>lengths</span> <span>=</span> <span>list</span><span>.</span><span>map</span> <span>{</span> <span>it</span><span>?.</span><span>length</span> <span>?:</span> <span>0</span> <span>}</span><span>println</span><span>(</span><span>lengths</span><span>)</span> <span>// Prints [6, 0, 4]</span><span>val</span> <span>list</span><span>:</span> <span>List</span><span><</span><span>String</span><span>?></span> <span>=</span> <span>listOf</span><span>(</span><span>"Kotlin"</span><span>,</span> <span>null</span><span>,</span> <span>"Java"</span><span>)</span> <span>val</span> <span>lengths</span> <span>=</span> <span>list</span><span>.</span><span>map</span> <span>{</span> <span>it</span><span>?.</span><span>length</span> <span>?:</span> <span>0</span> <span>}</span> <span>println</span><span>(</span><span>lengths</span><span>)</span> <span>// Prints [6, 0, 4]</span>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:
<span>val</span> <span>name</span><span>:</span> <span>String</span><span>?</span> <span>=</span> <span>null</span><span>val</span> <span>result</span> <span>=</span> <span>name</span><span>?.</span><span>let</span> <span>{</span><span>println</span><span>(</span><span>"Name is not null: $it"</span><span>)</span><span>it</span><span>.</span><span>length</span> <span>// this won't be executed because name is null</span><span>}</span> <span>?:</span> <span>"Default value"</span><span>println</span><span>(</span><span>result</span><span>)</span> <span>// Prints "Default value"</span><span>val</span> <span>name</span><span>:</span> <span>String</span><span>?</span> <span>=</span> <span>null</span> <span>val</span> <span>result</span> <span>=</span> <span>name</span><span>?.</span><span>let</span> <span>{</span> <span>println</span><span>(</span><span>"Name is not null: $it"</span><span>)</span> <span>it</span><span>.</span><span>length</span> <span>// this won't be executed because name is null</span> <span>}</span> <span>?:</span> <span>"Default value"</span> <span>println</span><span>(</span><span>result</span><span>)</span> <span>// Prints "Default value"</span>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:
<span>val</span> <span>name</span> <span>=</span> <span>"Kotlin"</span><span>// name = "Java" // Compilation error!</span><span>val</span> <span>name</span> <span>=</span> <span>"Kotlin"</span> <span>// name = "Java" // Compilation error!</span>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.
<span>var</span> <span>age</span> <span>=</span> <span>42</span><span>age</span> <span>=</span> <span>43</span> <span>// No compilation error</span><span>var</span> <span>age</span> <span>=</span> <span>42</span> <span>age</span> <span>=</span> <span>43</span> <span>// No compilation error</span>var age = 42 age = 43 // No compilation error
Enter fullscreen mode Exit fullscreen mode
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.
<span>val</span> <span>numbers</span> <span>=</span> <span>listOf</span><span>(</span><span>1</span><span>,</span> <span>2</span><span>,</span> <span>3</span><span>)</span><span>numbers</span><span>.</span><span>add</span><span>(</span><span>4</span><span>)</span> <span>// Compilation error!</span><span>val</span> <span>numbers</span> <span>=</span> <span>listOf</span><span>(</span><span>1</span><span>,</span> <span>2</span><span>,</span> <span>3</span><span>)</span> <span>numbers</span><span>.</span><span>add</span><span>(</span><span>4</span><span>)</span> <span>// Compilation error!</span>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.
<span>val</span> <span>mutableNumbers</span> <span>=</span> <span>mutableListOf</span><span>(</span><span>1</span><span>,</span> <span>2</span><span>,</span> <span>3</span><span>)</span><span>mutableNumbers</span><span>.</span><span>add</span><span>(</span><span>4</span><span>)</span> <span>// Allowed</span><span>val</span> <span>mutableNumbers</span> <span>=</span> <span>mutableListOf</span><span>(</span><span>1</span><span>,</span> <span>2</span><span>,</span> <span>3</span><span>)</span> <span>mutableNumbers</span><span>.</span><span>add</span><span>(</span><span>4</span><span>)</span> <span>// Allowed</span>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.
<span>data class</span> <span>Person</span><span>(</span><span>val</span> <span>name</span><span>:</span> <span>String</span><span>,</span> <span>val</span> <span>age</span><span>:</span> <span>Int</span><span>)</span><span>data class</span> <span>Person</span><span>(</span><span>val</span> <span>name</span><span>:</span> <span>String</span><span>,</span> <span>val</span> <span>age</span><span>:</span> <span>Int</span><span>)</span>data class Person(val name: String, val age: Int)
Enter fullscreen mode Exit fullscreen mode
<span>val</span> <span>person</span> <span>=</span> <span>Person</span><span>(</span><span>"Alice"</span><span>,</span> <span>42</span><span>)</span><span>person</span><span>.</span><span>name</span> <span>=</span> <span>"Bob"</span> <span>// Compilation error!</span><span>val</span> <span>person</span> <span>=</span> <span>Person</span><span>(</span><span>"Alice"</span><span>,</span> <span>42</span><span>)</span> <span>person</span><span>.</span><span>name</span> <span>=</span> <span>"Bob"</span> <span>// Compilation error!</span>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.
<span>sealed</span> <span>class</span> <span>Response</span><span>data class</span> <span>Success</span><span>(</span><span>val</span> <span>data</span><span>:</span> <span>String</span><span>)</span> <span>:</span> <span>Response</span><span>()</span><span>data class</span> <span>Error</span><span>(</span><span>val</span> <span>message</span><span>:</span> <span>String</span><span>)</span> <span>:</span> <span>Response</span><span>()</span><span>val</span> <span>response</span><span>:</span> <span>Response</span> <span>=</span> <span>Success</span><span>(</span><span>"Data loaded successfully"</span><span>)</span><span>response</span> <span>=</span> <span>Error</span><span>(</span><span>"Something is wrong"</span><span>)</span> <span>// Compilation error! This would require reassignment.</span><span>sealed</span> <span>class</span> <span>Response</span> <span>data class</span> <span>Success</span><span>(</span><span>val</span> <span>data</span><span>:</span> <span>String</span><span>)</span> <span>:</span> <span>Response</span><span>()</span> <span>data class</span> <span>Error</span><span>(</span><span>val</span> <span>message</span><span>:</span> <span>String</span><span>)</span> <span>:</span> <span>Response</span><span>()</span> <span>val</span> <span>response</span><span>:</span> <span>Response</span> <span>=</span> <span>Success</span><span>(</span><span>"Data loaded successfully"</span><span>)</span> <span>response</span> <span>=</span> <span>Error</span><span>(</span><span>"Something is wrong"</span><span>)</span> <span>// Compilation error! This would require reassignment.</span>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
暂无评论内容