Why Kotlin's Better Types Matter a Lot
Data classes and inline classes are a game changer for clean code
Kotlin has lots of advantages over Java, but one that I find easily overlooked is their two special classes: data classes and inline classes. Data classes seem at first just like some minor time save during development, but I believe they can also lead to better code. And inline classes are so often overlooked completely.
So let's look at why I think that these two classes actually have an impact on good code.
Cover image created by the author using Carbon.
Data classes lead to cleaner code
Kotlin's data classes are essentially just a quicker way to create POJOs.
They generate getters, setters, hashcode
, equals
and toString
automatically for you.
Java 17 introduced something similar in the form of records, although there are some slight differences. Pretty much everything I'll say about them, also apply to records.
For those that don't know, the following is a data class:
data class Person(val name: String, var age: Int)
Without it, in pre-17 Java we'd have to write the following:
public class Person {
private final String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age && Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
The obvious part is that we save a lot of code that has to be written and, worse than that, read.
But the less obvious, but much more important difference is that changing something requires a lot of changes – some of which are easy to miss and likely won't cause any tests to fail.
It's pretty easy to add a field and forget to also add it to the hashCode
or equals
method.
Making it hard to change or create a class also discourages programmers from doing so. For instance, in 99% of the cases having a boolean value in the constructor of a class means that that class should be split into two different ones. Yet, you'll find that in any bigger code base, simply because adding a boolean field takes around 5 minutes, while creating another class could take up to 20 if it's big. While 15 minutes might seem like a small price to pay for cleaner architecture, very few will actually choose that option.
Inline classes can make cleaner code performant (or even viable)
Inline classes are essentially as close to custom value types as you can currently get on the JVM. Let's say you have the following inline class: (example taken from the kotlin documentation).
@JvmInline
value class Name(val s: String) {
init {
require(s.length > 0) { }
}
val length: Int
get() = s.length
fun greet() {
println("Hello, $s")
}
}
Kotlin will then try to prevent the creation of an object as much as possible and instead use the raw type (in this case String
).
Instance method calls as well as property getters will then instead be compiled to static method calls.
The main reason why I like this is that it finally allows a performant way to implement tiny types. While I'm a big fan of that idea, I could never use it in any serious project since my job demands high performance standards, where the object churn of that technique in Java would be a very real problem. Nevertheless, I believe that the benefits are huge. And with the quick way to create a new class and the possibility to do that without creating an extreme amount of instances, this now becomes a very real and easy-to-implement style.
Downsides of inline classes
Sadly, I also see some downsides to inline classes:
- They only work with one parameter. This is a major limitation, which means that when you need to group multiple values together, you'll still need to create an object every time.
- It sometimes provides false security. When "instantiating" an inline class it's easy to think that it will never actually be created. However, in some cases that's not true. Mainly when it is being used as one of its interfaces or when it is used in a generic method.
- They have to be final. Although I don't think this is a big problem as I don't see many reasons to extend a class with only one input field.
Summary
When comparing Java and Kotlin, data types and inline classes are usually just quickly mentioned as nice-to-haves. But I think that is really underselling them. They not only provide ways to reduce boilerplate code, but are also safer to use, can allow for a performant usage of tiny types and similar clean code styles and reduce the friction to crafting cleaner code – and friction is the enemy of good software.