What are inline value classes in Kotlin and when you should use them

Alex Forrester
Kt. Academy
Published in
10 min readNov 21, 2023

--

Picture of inline skaters

A value class in Kotlin holds a single immutable value which can be inlined on compilation removing the wrapper type and using the underlying value itself. They are mostly used for simple data types to transform a String, Int, Long, Double etc. which would ordinarily be defined only by its identifier to an explicit type. They don’t have the concept of identity. They are completely defined by the data they hold.

Let’s take the example of a name. It would typically be defined by a String with its name as the identifier:

val name: String = "Sarah Smith"

This can be modelled as a value class which has a single property initialised in the primary constructor: (For JVM backends the @JvmInline annotation notifies the JVM that this is an inline class as explained here)

@JvmInline
value class Name(val value: String)
val name = Name("Sarah Smith")

fun main() {
// Property name can be any valid identifier
println("Name is ${name.value}")
}

This format improves readability and comes with a major benefit that at compile time if the value class can be unboxed then it will be, similar to a primitive int being preferred to an Integer with the resultant performance gain. They support some features of regular classes - value classes are final and can’t be extended. They can have methods and properties (which are computed with no backing fields, delegates or lateinit properties). These are implemented as static methods. They can implement interfaces (with delegation implementation of interfaces as well). They can also have init blocks and secondary constructors (from Kotlin version 1.9) which can be useful to validate the wrapped property’s value as shown below:

interface NameFormat {
fun containsSpaceCharacter(): Boolean
}

@JvmInline
value class Name(val value: String) : NameFormat {
init {
require(value.isNotEmpty()) {
"Name shouldn't be empty"
}
}

constructor(firstName: String, lastName: String) : this("$firstName $lastName".trim()) {
require(firstName.isNotBlank() && lastName.isNotBlank()) {
"First name and Last name should not be blank"
}
}

val length: Int
get() = value.length

fun isLongName() = value.length > 20

override fun containsSpaceCharacter() = value.contains(" ")
}

fun main() {
val name1 = Name("Sarah Smith")
val name2 = Name("Sarah", "Smith")
println(name1.length) // property getter is called as a static method
println(name2.isLongName()) // function is called as a static method
println(name2.containsSpaceCharacter()) // overridden function is called as a static method
}

Full details of value class features are in the official Kotlin docs where the example above has been adapted from.

Value validation

The characteristic of having a sole value makes them ideal for validation. In the example above the name value can be validated in one place. This also means if the validation is subject to change it can be done in one place. If the name was used in multiple classes then each one of these classes would have to validate the assignment of the name. The validation could potentially be littered around different places in the codebase and is more error prone.

Protecting against type misuse

For the benefits of modelling types let’s take the example of creating a rectangle requiring a width of 10 and height of 50. (No x and y coordinates will be used for simplicity).

data class Rectangle(val width: Int, val height: Int)
val rectangle = Rectangle(10, 50)
Rectangle with width 10, height 50
Rectangle with width 10, height 50

The identity of the parameter names guides how to define the measurements of the rectangle and using named parameters can also assist in making the initialisation clearer:

data class Rectangle(val width: Int, val height: Int)
val rectangle = Rectangle(width = 10, height = 50)

There is nothing stopping the height and width being unintentionally swapped, however, and producing the wrong result.

val rectangle = Rectangle(50, 10)
Rectangle with width 50 and height 10
Rectangle with width 50 and height 10

Kotlin provides the keyword typealias which allows creating an alternative name for an existing type. This can make the Rectangle’s parameters more readable:

typealias Width = Int
typealias Height = Int

data class Rectangle(val width: Width, val height: Height)

val rectangle = Rectangle(10, 50)
val rectangle2 = Rectangle(50, 10)

This is an improvement as the signature of the function is clearer. The issue remains, however, that on instantiation the order of the arguments might be wrong. It’s not a type safe solution. Here value classes can help.

Creating a value class for a parameter makes its use clear and protects against misuse in cases where arguments can be swapped around or ordered differently when there are more than two parameters.

@JvmInline
value class Width(val value: Int)

@JvmInline
value class Height(val value: Int)

data class Rectangle(val width: Width, val height: Height)

val width = Width(10)
val height = Height(50)

val rectangle = Rectangle(width, height)

// Will not compile
// val rectangle2 = Rectangle(height, width)

Passing the wrong type now will not compile.

Unit of measurement protection

There is a further benefit of using value classes when dealing with units of measurement. In the examples above the length unit which the Rectangle’s dimensions are specified in is an Int value. There might be KDoc documentation (the equivalent of Java’s Javadoc) for the Rectangle class which details the unit measurement as millimetres. The parameter names could detail the unit as well:

@JvmInline
value class Width(val value: Int)

@JvmInline
value class Height(val value: Int)

data class Rectangle(val widthInMillimetres: Width, val heightInMillimetres: Height)

val width = Width(10)
val height = Height(50)

val rectangle = Rectangle(widthInMillimetres = width, heightInMillimetres = height)

Still, this doesn’t protect against misuse of creating the Rectangle in another length unit such as metres, inches etc., or even a different type of measurement, such as pixels, dp (density independent pixels) etc.

Creating value classes for the measurement combined with the unit of measurement clarifies the expected types:

@JvmInline
value class WidthInMillimetres(val value: Int)

@JvmInline
value class HeightInMillimetres(val value: Int)

val widthInMillimetres = WidthInMillimetres(10)
val heightInMillimetres = HeightInMillimetres(50)

data class Rectangle(val widthInMillimetres: WidthInMillimetres, val heightInMillimetres: HeightInMillimetres)

val rectangle = Rectangle(widthInMillimetres, heightInMillimetres)

// Will not compile
// val rectangle2 = Rectangle(heightInMillimetres, widthInMillimetres)

The type has provided detail about the unit of measurement by the naming, minimising the possibility that an incorrect value will be used. This technique does, however, expose low level detail and couples the measurement with the measurement unit itself. One final optimisation is to create the height and width types and a unit of measurement type as value classes and wrap a value class with another value class:

@JvmInline
value class Millimetres(val value: Int)

@JvmInline
value class Width(val millimetres: Millimetres)

@JvmInline
value class Height(val millimetres: Millimetres)

data class Rectangle(val width: Width, val height: Height)

fun main() {

val height = Height(Millimetres(50))
val width = Width(Millimetres(10))

val rectangle = Rectangle(width, height)
println("Rectangle width is ${rectangle.width.millimetres.value}")
println("Rectangle height is ${rectangle.height.millimetres.value}")
}

This might be adding too much complexity depending on your use case, but it is an option and will have performance gains. It does, however, present one of the issues with value classes in that they can be overused with the temptation to convert every primitive and String to a value class bloating the codebase. It’s preferential to use them in critical areas and also where they have multiple use and call sites to improve clarity. This could be creating units of measurement value classes such as Millimetres and Metres and using them independently in your codebase for typical assignments. Creating a value class for a String label which will be used in one place is probably not necessary.

Performance Improvements

The same functionality can be obtained by using regular classes and data classes as wrappers, but a new object has to be created for each class on the heap which has a performance overhead.

Prior to Kotlin version 1.5 value classes (which were added in experimental form in Kotlin 1.4.30) were called inline classes which more clearly describe how they can be transformed at compile time. If the value/ inline class can be inlined then the property of the value class gets unwrapped and used instead of its containing class. This has a performance improvement as the overheard of creating a wrapper class doesn’t happen.

Let’s revisit the name value class and use it as a parameter in an Employee data class and examine whether it is inlined.

@JvmInline
value class Name(val value: String)
data class Occupation(val value: String)
data class Employee(val userId: String, val name: Name, val age: Int, val occupation: Occupation)

fun main() {
val userId = "sarahsmith"
val name = Name("Sarah Smith")
val age = 26
val occupation = Occupation("Chef")
val employee = Employee(userId, name, age, occupation)

println("Employee's userId is ${employee.userId}")
println("Employee's name is ${employee.name.value}")
println("Employee's age is ${employee.age}")
println("Employee's occupation is ${employee.occupation.value}")
}

When run this produces the Kotlin Bytecode below: (The option to display the Kotlin Bytecode can be found in Injellij IDEA/ Android Studio from Tools > Kotlin > Show Kotlin Bytecode).

LOCALVARIABLE this Lcom/digian/kotlin/constructs/valueclasses/employee/Employee; L0 L2 0
LOCALVARIABLE userId Ljava/lang/String; L0 L2 1
LOCALVARIABLE name Ljava/lang/String; L0 L2 2
LOCALVARIABLE age I L0 L2 3
LOCALVARIABLE occupation Lcom/digian/kotlin/constructs/valueclasses/employee/Occupation; L0 L2 4

// When decompiled into Java
public final class Employee {
@NotNull
private final String userId;
@NotNull
private final String name;
private final int age;
@NotNull
private final Occupation occupation;
...
}

Typical usage of the value class will be inlined as shown in the example. The name value class has been unwrapped and is a plain String type as the userId is.

When it is used as another type, however, the wrapper type will stay and the value will be boxed which happens when the value class is used as a Generic type, an Interface and in a Collection. Whether the value gets boxed when using nullable types is quite complex and depends on whether the value type parameter is nullable or not and whether at the call site the parameter is nullable or not. The basic rule is that the underlying value is used if the call site parameter is not nullable whether or not the underlying value is or is not nullable in the value class, but there are exceptions. There is more detail here.

Generic Inline Classes

In Kotlin 1.7.20 generic inline classes were introduced which allow the underlying type of value classes to be a type parameter as in the following example:

sealed class Lesson
data object Maths : Lesson()
data object History : Lesson()
data object English : Lesson()

@JvmInline
value class Favourite<T>(val value: T)

fun getFavourite(favouriteLesson: Favourite<Lesson>): String {

return when (favouriteLesson.value) {
is Maths -> "Maths"
is English -> "English"
is History -> "History"
}
}

data class Student(val name: Name, val favouriteLesson: Favourite<Lesson>)

fun main() {

val student = Student(Name("Sarah Smith"), favouriteLesson = Favourite(Maths))
println("${student.name.value}'s favourite lesson is ${getFavourite(student.favouriteLesson)}")
}

In the decompiled byte code the parameter is mapped to the upper bound of Object, not the type argument Lesson which you would expect from Generic behaviour.

// Decompiled Java code
public static final String getFavourite_iOBe__0(@NotNull Object favouriteLesson) {...}

The corresponding non inline generic class maps to the type argument:

class Favourite<T>(val value: T)

fun getFavourite(favouriteLesson: Favourite<Lesson>): String {...}

// When decompiled into Java
public static final String getFavourite(@NotNull Favourite favouriteLesson) {...}

In the example this can be worked around by specifying the upper bound which will produce the non inline type argument:

@JvmInline
value class Favourite<T : Lesson>(val value: T)

fun getFavourite(favouriteLesson: Favourite<Lesson>): String {...}

// When decompiled into Java
public static final String getFavourite_iOBe__0(@NotNull Favourite favouriteLesson) {...}

There’s a possibility that in the Kotlin roadmap reified types could be added to value classes so that the type information is preserved and the parameter is mapped to the type argument. Also, the Java team has been working on a project called Valhalla, which adds value types to Java and the JVM which could address this issue.

Note on example above: The getFavourite function in its decompiled form above has some characters appended to it.

public static final String getFavourite_iOBe__0(@NotNull Favourite favouriteLesson) {...}

As Svetlana Isakova explains in ‘New Language Features Preview in Kotlin 1.4.30’.

By default, such names are mangled to prevent accidental usages from Java or conflicting overloads. If you annotate a function with @JvmName, it changes the name of this function in the bytecode and makes it possible to call it from Java and pass a value directly:

@JvmName("getFavouriteLesson")
fun getFavourite(favouriteLesson: Favourite<Lesson>): String {..}

// When decompiled into Java
@JvmName(name = "getFavouriteLesson")
@NotNull
public static final String getFavouriteLesson(@NotNull Favourite favouriteLesson) {

To wrap up here’s a summary of value class benefits:

Value validation: Validate value assignment in one place.

Protecting against type misuse: Specify an exact type rather than common generic data types for type safe assignments.

Unit of measurement protection: Clarify units and minimise setting wrong values.

Performance improvements: Benefit from less object creation on the heap and let the JVM optimise values for performance.

Generic Inline Classes: Benefit from the underlying type of value classes being a type parameter and use type safety for class hierarchies which are wrapped with value classes.

Knowing when to use value classes to benefit from these features is subjective, but when modelling a data or domain layer used judicially this leads to more readable property definitions and can make the intended usage of common data types clearer.

The repository with all the code examples is below:

Get your skates on and start inlining today!

References

Inline value classes | Kotlin Documentation (kotlinlang.org)

From Inline to Value Classes — YouTube

Generic inline classes in Kotlin 1.7.20 — YouTube

Keep Design Notes on Kotlin Value Classes

Inline Classes and Autoboxing (Updated for Kotlin 1.5+) — Dave Leeds on Kotlin (typealias.com)

--

--

Android Developer, Lead author of ‘How to Build Android Apps with Kotlin’ (Packt 2022)