Kotlin Tip #45: Utilize value classes to avoid runtime overhead when wrapping values — 100 Kotlin Tips in 100 Days

Raphael De Lio
Kotlin with Raphael De Lio
3 min readJun 6, 2024

Twitter | LinkedIn | YouTube | Instagram
Tip #44: Implement collection plus and minus operators for adding or removing elements

Value classes are a way to optimize by wrapping a value in a class with less memory use. They are made using the “value” modifier before “class”.

Their main goal is to act like a primitive type during runtime for better performance, but still have class type benefits at compile time, like adding methods or using interfaces.

Here’s how you can define a value class:

@JvmInline
value class UserId(val id: String)

In this example, “UserId” is a value class that wraps a “String”. The “@JvmInline” annotation tells the Kotlin compiler to treat this class as if it were a simple “String” at runtime, avoiding extra memory use. However, during compile time, “UserId” is a different type from “String”, which adds type safety and makes your code clearer.

If you have functions that need user IDs, you might have used “String” directly before. By using a value class like “UserId”, your code becomes easier to understand and reduces the chance of confusing different types of “String” values.

To illustrate the practical application, consider this example:

Using “UserId” means the function “fetchUser” clearly shows it wants a user ID, not just any string. This makes the code more type-safe, easier to understand, and maintain. At the same time, it avoids the overhead of creating a separate object, so it behaves essentially like passing a primitive string.

Look at this illustrative bytecode comparison. When using a regular class, the compiler generates instructions for object creation, field access, and method invocations. This involves allocating memory on the heap, potentially leading to garbage collection overhead.

// Regular class bytecode
...
NEW UserId // val myUserId = UserId()
DUP
LDC "user123"
INVOKESPECIAL UserId.<init>(Ljava/lang/String;)V // myUserId.id = "user123"
ASTORE 1 // (store myUserId)
ALOAD 1 // (load myUserId)
GETFIELD UserId.id : Ljava/lang/String; // val idString = myUserId.id
INVOKEVIRTUAL java/io/PrintStream.println(Ljava/lang/String;)V // println(idString)
...

However, with a value class, the compiler optimizes away the object and operates directly on the underlying value, resulting in a streamlined execution similar to working with primitive types:

// Value class bytecode
...
LDC "user123" // val idString = "user123"
ASTORE 1 // (store idString)
ALOAD 1 // (load idString)
INVOKEVIRTUAL java/io/PrintStream.println(Ljava/lang/String;)V // println(idString)
...

A major benefit of value classes is they don’t add extra workload at runtime on the JVM, blending clearer, safer code with high performance. But, it’s key to remember value classes have limits, like not having properties with backing fields or not being able to extend other classes.

I hope you have enjoyed this tip of our series! Don’t forget to subscribe and stay tuned for more Kotlin tips!

Stay curious!

Tip #46: Choose IntArray Over Array for Performance Efficiency

Contribute

Writing takes time and effort. I love writing and sharing knowledge, but I also have bills to pay. If you like my work, please, consider donating through Buy Me a Coffee: https://www.buymeacoffee.com/RaphaelDeLio

Or by sending me BitCoin: 1HjG7pmghg3Z8RATH4aiUWr156BGafJ6Zw

Follow Me on Social Media

Stay connected and dive deeper into the world of Kotlin with me! Follow my journey across all major social platforms for exclusive content, tips, and discussions.

Twitter | LinkedIn | YouTube | Instagram

--

--