Inline Classes: How value class Disappears in the Bytecode

 Achieve zero-cost abstractions by eliminating heap allocations while maintaining strict type safety in your Kotlin codebases.

Inline Classes: How value class Disappears in the Bytecode

We’ve explored the state machines of Coroutines and the slot tables of Jetpack Compose. Today, we’re looking at one of Kotlin’s most powerful tools for writing “Type-Safe” code without the “Performance Tax”: Inline Classes (Value Classes).

In standard Object-Oriented programming, every time you wrap a primitive (like a Long) in a class (like UserId), you pay a price. You create an object on the heap, add pressure to the Garbage Collector, and introduce pointer indirection.

Inline classes change the game. They allow you to keep your type safety in the source code while the compiler “erases” the wrapper in the bytecode.

1. The Problem: The “Wrapper Overhead”

Imagine you are building a payment system. You want to distinguish between a CurrencyAmount and a TransactionId. Using raw Long values for both is dangerous—you could accidentally swap them in a function call.

Traditional Wrapping:

class TransactionId(val value: Long) // Every instance is a full Object allocation

The Inline Way:

@JvmInline
value class TransactionId(val value: Long)

By adding @JvmInline value, you are telling the Kotlin compiler: "Treat this as a unique class in my source code, but in the final bytecode, just use the underlying Long whenever possible."

2. The Bytecode Vanishing Act: Real javap Output

If we look at the actual bytecode using the javap tool, we see exactly how the "vanishing act" happens. The TransactionId type simply doesn't exist in the function signature.

The Bytecode Command: javap -c TransactionKt.class

The Output:

// Notice the parameter is a 'long', not 'TransactionId'
public static final void processTransaction(long id);

The Mental Model:

  • Source code view: TransactionId
  • Compiler view: long
  • Runtime (Boxed): Object { long }

The performance win here isn’t necessarily that the CPU runs “faster,” but that we eliminate allocations. By staying as a primitive on the stack, we bypass the heap and the Garbage Collector entirely.

3. Name Mangling: Solving the Overload Conflict

There is a catch. What happens if you have two functions that, after inlining, have the exact same JVM signature?

fun send(id: TransactionId) { ... }
fun send(id: Long) { ... }

To the JVM, these both look like send(long id). To prevent a collision, the Kotlin compiler uses Name Mangling. It appends a hashcode to the function name in the bytecode, turning it into something like send-ghp4x(long id).

4. When does “Boxing” occur?

Inline classes aren’t always primitives. Sometimes the compiler is forced to “box” the value back into an object — effectively recreating the wrapper.

Boxing happens when:

  • Nullability: Using the type as TransactionId?.
  • Generics: Storing them in collections like List<TransactionId>.
  • Interfaces: When a value class is passed as an interface type.

Future Note (Project Valhalla): Java’s upcoming Project Valhalla aims to make this optimization native to the JVM. When it lands, Kotlin value classes may eventually avoid boxing even in generic collections.

5. When NOT to use Value Classes

While they are powerful, they aren’t a silver bullet. Avoid them when:

  1. Large Collections: If you are storing millions of IDs in a List<TransactionId>, they will all be boxed into objects anyway, defeating the purpose.
  2. Java-First Libraries: If your library is mostly used by Java developers, the mangled method names (e.g., send-ghp4x) make for a terrible developer experience.

🙋‍♂️ Frequently Asked Questions (FAQs)

What is the difference between typealias and value class?

typealias is just a nickname. If you have typealias UserId = Long, you can still pass a raw Long into a UserId function. A value class is a distinct type; the compiler blocks you from mixing them up.

Can an inline class have multiple properties?

Currently, a value class must have exactly one property. This is because the compiler needs a 1:1 mapping to "inline" the value into a single primitive slot.

Can inline classes implement interfaces?

Yes! This is a huge advantage. You can have value class Email(val value: String) : Validatable. Note that calling an interface method will cause the value to be boxed temporarily.

💬 Join the Conversation!

  • Do you use value classes for IDs and Units, or do you find the mangled names in stack traces too confusing?
  • What is the one “primitive” in your app that should definitely be a value class?
  • Are you excited about Project Valhalla potentially fixing the “boxing in generics” issue?

📘 Master Your Next Technical Interview

Since Java is the foundation of Android development, mastering DSA is essential. I highly recommend “Mastering Data Structures & Algorithms in Java”. It’s a focused roadmap covering 100+ coding challenges to help you ace your technical rounds.

Comments

Popular posts from this blog

No More _state + state: Simplifying ViewModels with Kotlin 2.3

Why You Should Stop Passing ViewModels Around Your Compose UI Tree 🚫

Is Jetpack Compose Making Your APK Fatter? (And How to Fix It)