Inline value classes in Kotlin. Comparison with type aliases and data classes.

Anatolii Frolov
5 min readJul 10, 2023

--

Photo by Clément Hélardot on Unsplash

Introduction

With the introduction of Kotlin 1.5.0, value classes (previously known as inline classes) have reached a stable state. However, this has led to some confusion as we now have three similar tools in Kotlin: type aliases, data classes, and value classes. This raises the question of whether we can entirely replace type aliases and data classes with value classes.

Can we eliminate type aliases and data classes altogether and replace them with value classes? Let’s figure it out.

Problem definition

It’s no secret that classes in Kotlin are highly significant and assist us in resolving various issues, including:

  • They provide clear and meaningful names, helping us understand the type of object being used.
  • They ensure type-safety, preventing the accidental use of an object from one class where another class is expected. This helps catch potential errors during the compilation process, avoiding serious bugs.

Primitive types like Int or Double also provide type safety (meaning you can’t pass an Int where a String or Double is required), but they lack a meaningful name. For example, the Int type could be a person’s age, weight, height, and so on. We know for sure that this is an integer, but semantic type safety is not guaranteed.

Let’s see what problem we can face with using only primitive types.

So we have a function that will print information about a person like below.

fun printPersonInfo(name: String, surname: String, age: Int, weight: Int) {
println("Hello, my name is $name $surname. I am $age years old. My weight is $weight kg.")
}

There is a possibility that the function may receive arguments that are different from what we expected. While named arguments can address this issue, let’s consider a scenario where the values have already been passed to the function.

val personName = "Walter"
val personSurname = "White"
val personAge = 30
val personWeight = 100


printPersonInfo(personName, personSurname, personAge, personWeight)
// Hello, my name is Walter White. I am 30 years old. My weight is 100 kg.

printPersonInfo(personSurname, personName, personWeight, personAge)
// Hello, my name is White Walter. I am 100 years old. My weight is 30 kg.
// no compilation error

This programming error cannot be detected at compile time and will result in unexpected behavior. As we can see, we got a mix of the first and last name, as well as the age and weight of the person.

Type aliases

More information about type aliases at the link.

Let’s try to solve our problem in a different way using type aliases.

typealias PersonName = String
typealias PersonSurname = String
typealias PersonAge = Int
typealias PersonWeight = Int

To achieve this, let’s make a slight modification to our function.

fun printPersonInfo(name: PersonName, surname: PersonSurname, age: PersonAge, weight: PersonWeight) {
println("Hello, my name is $name $surname. I am $age years old. My weight is $weight kg.")
}

But this did not solve our problem. Because type aliases hide all the same primitives and don’t give us the semantic type safety we expect.

printPersonInfo(personName, personSurname, personAge, personWeight)
// Hello, my name is Walter White. I am 30 years old. My weight is 100 kg.

printPersonInfo(personSurname, personName, personWeight, personAge)
// Hello, my name is White Walter. I am 100 years old. My weight is 30 kg.
// no compilation error

Data classes

More information about data classes at the link.

Perhaps, data classes are the solution to our problem and we should try to apply them. Need to check it out.

To do this, we will create data classes

data class Name(val data: String)
data class Surname(val data: String)
data class Age(val data: Int)
data class Weight(val data: Int)

and change our function

fun printPersonInfo(name: Name, surname: Surname, age: Age, weight: Weight) {
println("Hello, my name is ${name.data} ${surname.data}. I am ${age.data} years old. My weight is ${weight.data} kg.")
}

Let’s create class instances and pass them to the function.

val studentName = Name("Walter")
val studentSurname = Surname("White")
val studentAge = Age(30)
val studentWeight = Weight(100)


printPersonInfo(studentName, studentSurname, studentAge, studentWeight)
//Hello, my name is Walter White. I am 30 years old. My weight is 100 kg.

printPersonInfo(studentName, studentSurname, studentWeight, studentAge)
// error

I have good and bad news. The good news is that we have found a solution to the problem, and now if we try to mix our function arguments, the compiler will point out an error to us. But the bad news is that creating instances of data classes can be resource-intensive. Primitive values can be stored on the stack, which is faster and more efficient. In contrast, data class instances are stored on the heap, requiring more time and memory.

We require an alternative solution that is both fast and cost-effective like using primitives, while still maintaining semantic type-safety like using classes.

Fortunately, Kotlin offers such a solution.

Inline value classes

More information about value classes at the link.

Let’s create some inline value classes and rewrite our function using them.

@JvmInline
value class PersonName(val value: String)

@JvmInline
value class PersonSurname(val value: String)

@JvmInline
value class PersonAge(val value: Int)

@JvmInline
value class PersonWeight(val value: Int)

fun printPersonInfo(name: PersonName, surname: PersonSurname, age: PersonAge, weight: PersonWeight) {
println("Hello, my name is ${name.value} ${surname.value}. I am ${age.value} years old. My weight is ${weight.value} kg.")
}

Then we create instances of our classes and pass them to the function.

val name = PersonName("Walter")
val surname = PersonSurname("White")
val age = PersonAge(30)
val weight = PersonWeight(100)

printPersonInfo(name, surname, age, weight)
//Hello, my name is Walter White. I am 30 years old. My weight is 100 kg.

printPersonInfo(surname, name, weight, age)
// error

And… everything works. Great! This result suits us. Let’s see why this is the best solution for our problem.

Conclusion

From the previous example. Under the hood, the compiler handles value classes as a type alias, but with a significant distinction: value classes are not interchangeable in assignments. Consequently, the following code will fail to compile:

printPersonInfo(surname, name, weight, age)  
// error

Value classes and data classes have similarities, but their main distinction lies in the allocation costs. Value classes have no allocation costs, unlike data classes. However, it’s important to note that value classes can only contain a single primitive field. Therefore, there are valid reasons to choose a data class instead of a value class, depending on the specific requirements and characteristics of the data being modeled.

Happy coding!

--

--