Author profile picture

An Introduction to Inline Classes

Whether you’re writing massive data-chomping processes that run in the cloud, or mobile apps that run on low-powered cell phones, most of us want our code to run fast. And now, Kotlin’s inline classes feature allows us to create the data types that we want without giving up the performance that we need!

In this series, we’re going to take a look at inline classes from top to bottom!

Cartoon of a class of kids who are seated in a line, and are (mostly) inline.

This article explores what they are, how they work, and the trade-offs involved when choosing to use them. Then, the next article will look under the hood of inline classes to see exactly how they’re implemented.

By the way, an inline class is a special kind of value class, so although you’ll usually find them called “inline classes”, you might sometimes also find the term “inline value classes”. In this article, I’ll refer to them simply as “inline classes”.

Ready to dive in? Let’s go!

Strong Types and Simple Values: A Case for Inline Classes

It’s 8 AM on Monday morning, and after pouring yourself a fresh, steaming cup o’ joe, you pull a new ticket to work on. It reads:

Send new users a welcome email - four days after they sign up.

Since the mailing system has already been written, you pull up the interface for the mail scheduler, and this is what you see:

interface MailScheduler {
    fun sendEmail(email: Email, delay: Int)
}

You know you need to call this function… but what arguments should you send it in order to delay the email by 4 days?

The delay parameter has a type of Int. So, we know that it’s an integer, but we don’t know what unit it represents - should you pass 4 for 4 days? Or maybe it represents hours, in which case you should pass 96. Or maybe it’s in minutes, seconds or milliseconds

How could we improve this code?

Sure, we can change the name of the parameter to include the time unit, like delayInMinutes. That would certainly be an improvement. But even this is only a hint - the developer still has to pay attention to the name, and send an integer that represents the unit of time that the function expects. It’s not hard to imagine errors in calculations:

fun send(email: Email) {
    mailScheduler.sendEmail(email, WELCOME_EMAIL_DELAY_IN_DAYS * 24 * 60 * 60)
}

Oops! We accidentally converted it to seconds instead of minutes!

What would make this even better?

If the compiler could enforce that the right unit of time is sent. For example, instead of accepting an Int, let’s update our interface so that it accepts a strong type - Minutes:

interface MailScheduler {
    fun sendEmail(email: Email, delay: Minutes)
}

Now we’ve got the type system working for us! We can’t possibly send a Seconds to this function because it only accepts an argument of type Minutes! Consider how the following code can go a long way toward reducing bugs compared to the previous version:

val defaultDelay = Days(4)

fun send(email: Email) {
    mailScheduler.sendEmail(email, defaultDelay.toMinutes())
}

We get a lot more assurance when we can take full advantage of the type system.

But developers often choose not to write these kinds of simple value wrapper classes, opting instead to pass integers, floats, and booleans around the code base.

Why is that?

Often it’s because of performance reasons. As you might recall, memory on a JVM looks a little something like this:

The stack and heap on JVM.

When we create a local variable (that is, function parameters and variables defined within a function) of a primitive type - like integers, floating point numbers, and booleans - those values are stored on a part of the JVM memory called the stack. There’s not very much overhead involved in storing the bits for these primitive values on the stack.

On the other hand, whenever we instantiate an object, that object is stored on the heap1. We take a performance hit when storing and using objects - heap allocations and memory fetches are expensive. Per object, the cost is small, but when accumulated, it can have an important impact on how fast your code runs.

Wouldn’t it be great if we could get all the benefits of the type system without taking the performance hit?

In fact, Kotlin’s inline class feature lets us do just that!

Let’s take a look!

Introducing Inline Classes

Inline classes are very simple to create - just use the keyword value when defining your class, and if you’re targeting the JVM, add the annotation @JvmInline, like this:

@JvmInline
value class Hours(val value: Int)

That’s it! This class will now serve as a strong type for your value, and in many cases, it won’t have nearly the same performance costs that you’d experience with regular classes.

You can instantiate and use the inline class just like any other class. Whenever you need to reference the underlying value property, just access it like you would with any other class:

val hours = Hours(12)

println("It has been ${hours.value} hours!")

You can also add a function to it, like this:

@JvmInline 
value class Hours(val value: Int) {
    fun toMinutes() = Minutes(value * 60)
}

Key Terms You Should Know

Inline classes wrap an underlying value. That value has a type, which we call the underlying type.

Anatomy of an inline class - underlying values and underlying types

Why Inline Classes Can Perform Better

So, what is it about inline classes that can cause them to perform better than regular classes? 2

When you instantiate an inline class like this…

val period = Hours(24)

the class isn’t actually instantiated in the compiled code! In fact, as far as the JVM is concerned, it’s as if you wrote this Java code…3

int period = 24;

As you can see, there’s no notion of Hours here in this compiled version of the code - it’s just assigning the underlying value to a primitive int variable!

Similarly, when you use an inline class as the type of a function parameter…

fun wait(period: Hours) { /* ... */ }

… it can effectively compile down to this …

void wait(int period) { /* ... */ }

So, the underlying value and underlying type were inlined in our code. In other words, the compiled code just used an integer type, so we avoided the cost of creating and accessing an object on the heap.

But wait a minute!

Remember how we added a function called toMinutes() to that Hours class? Since the compiled code is using an int instead of an Hours object, what happens when you call toMinutes()? After all, int doesn’t have a function called toMinutes(), so how does this work?

The compiled code implements inline class functions as static functions! Here’s how we defined our Hours class above:

@JvmInline
value class Hours(val value: Int) {
    fun toMinutes() = Minutes(value * 60)
}

The equivalent Java code for Hours.toMinutes() would look something like this…4

public static final int toMinutes(int $this) {
	return $this * 60;
}

Similarly, if we were to call Hours(24).toMinutes() in Kotlin, it can effectively compile down to just toMinutes(24).

Okay, so that that takes care of functions, but what about fields? What if we want Hours to include some other data on it besides the main underlying value?

Everything has its trade-offs, and this is one of them. Currently in Kotlin 1.5, an inline class can’t have any other fields other than the underlying value. This might change in future versions, but for now the only field it can have is the single underlying value.

Let’s take a look at some other trade-offs.

Trade-offs and Limitations

Now that we know that inline classes can be represented by their underlying value in the compiled code, we’re ready to understand the trade-offs and limitations that are involved when we use them.

First, an inline class must include an underlying value. This means that it needs a primary constructor that accepts the underlying value as a read-only property. You can name the underlying value whatever you want.

@JvmInline value class Seconds()              // nope - needs to accept a value!
@JvmInline value class Minutes(value: Int)    // nope - value needs to be a property
@JvmInline value class Hours(var value: Int)  // nope - property needs to be read-only
@JvmInline value class Days(val value: Int)   // yes!
@JvmInline value class Months(val count: Int) // yes! - name it what you want

As we discovered above, inline classes can’t have any fields on them other than the underlying value.

// nope - "Inline class must have exactly one primary constructor parameter"
@JvmInline value class Years(val count: Int, val startYear: Int)

However, they can have properties, as long as they are calculated based only the underlying value, or from some value or object that can be statically resolved - from singletons, top-level objects, constants, and so on.

object Conversions {
    const val MINUTES_PER_HOUR = 60    
}

@JvmInline value class Hours(val value: Int) {
    val valueAsMinutes get() = value * Conversions.MINUTES_PER_HOUR
}

Class inheritance is not allowed - inline classes cannot extend another class, and they can’t be extended by another class.

open class TimeUnit
@JvmInline value class Seconds(val value: Int) : TimeUnit() // nope - cannot extend classes

@JvmInline open value class Minutes(val value: Int) // nope - "Inline classes can only be final"

If you need your inline class to be a subtype you can implement an interface rather than extend a class.

interface TimeUnit {
	val value: Int
}

@JvmInline value class Hours(override val value: Int) : TimeUnit  // yes!

Inline classes can be defined inside other classes, but they can’t be declared as inner class.

class TimeUnit {
    @JvmInline value class Seconds(val value: Int)        // Just fine
    @JvmInline inner value class Minutes(val value: Int)  // Compiler error
}

They also can’t be defined within functions.

fun main() {
    @JvmInline value class Seconds(val value: Int) // Compiler error
}

Currently, enum classes cannot be inlined.

// nope - "Modifier 'value' is not applicable to 'enum class'"
@JvmInline enum value class TimeUnits(val value: Int) {
    SECONDS_PER_MINUTE(60),
    MINUTES_PER_HOUR(60),
    HOURS_PER_DAY(24)
}

Finally, because an inline class has no identity of its own, it doesn’t make sense to use it with the identity equality operator (sometimes called the referential equality operator), which checks to see if two variables refer to the same object. Because it doesn’t make sense, you’ll get a compiler error if you try this.

val firstHoursObject = Hours(24)
val secondHoursObject = Hours(24)

val isSameObject = firstHoursObject === secondHoursObject

For what it’s worth, you can still use the identity equality operator in cases where these objects have been cast to a supertype, such as Any.

Type Aliases vs. Inline Classes

Because they both encompass an underlying type, inline classes can be confused with type aliases. But there are a few key differences that make them useful in different situations.

Type aliases provide an alternate name for the underlying type. For example, you can alias a common type like String, and give it a descriptive name that’s meaningful in a particular context, like Username. In that case, variables of type Username would actually be variables of type String in both the source code and the compiled code. For example, you can do this:

typealias Username = String

fun validate(name: Username) {
    if (name.length < 5) {
        println("Username $name is too short.")
    }
}

Notice how we can call .length on name. That’s because name is a String, even though the parameter type we declared is using the alias Username.

Inline classes, on the other hand, are wrappers of the underlying type, so when you need to use the underlying value, you have to unwrap it again. For example, let’s rewrite the code above using an inline class instead of a type alias:

@JvmInline value class Username(val value: String)

fun validate(name: Username) {
    if (name.value.length < 5) {
        println("Username ${name.value} is too short.")
    }
}

Notice how we had to call name.value.length instead of just name.length - we had to unwrap the value.

The most important difference, though, has to do with assignment compatibility. Inline classes give you a level of type safety that type aliases do not.

Type aliases are the same as their underlying type. For example, check this out:

typealias Username = String
typealias Password = String

fun authenticate(user: Username, pass: Password) { /* ... */ }

fun main(args: Array<String>) {
    val username: Username = "joe.user"
    val password: Password = "super-secret"
    authenticate(password, username)
}

In this case, Username and Password are just different names for String, so you can assign a Username to a Password and vice versa. In fact, that’s exactly what we did in the code above - we mixed up the username and password when we called authenticate()… and the compiler was just fine with this!

On the other hand, if we use inline classes for this same situation, then we’d thankfully get a compiler error:

@JvmInline value class Username(val value: String)
@JvmInline value class Password(val value: String)

fun authenticate(user: Username, pass: Password) { /* ... */ }

fun main(args: Array) {
    val username: Username = Username("joe.user")
    val password: Password = Password("super-secret")
    authenticate(password, username) // <--- Compiler error here! =)
}

This is quite powerful! The strong typing here tells us immediately that we wrote a bug. We didn’t have to wait for an automated test, a QA engineer, or a user to tell us. Nice!

What’s the Difference Between Inline Classes and Value Classes?

Before we wrap up this article, you might be wondering, “If the concept is called an inline class, why do we create them by writing value class? What’s the difference between an inline class and a value class?”

An inline class is a certain kind of value class. So, to fully understand inline classes, it’s important to understand value classes!

What is a Value Class?

When two things look the same, sometimes it doesn’t matter which one we have. For example, here are two of Jimmy’s shirts:

Two shirts that are identical.

They’re the same color, the same size, and they’re made of the same kind of material. They’ve both been recently washed. When Jimmy is picking out his shirt for the day, it doesn’t matter which of these two shirts he chooses. They’re interchangable.

Other times, when two things look the same, it does matter which one we have. For example, Jimmy has a brother named Timmy, who is his identical twin.

Twin boys

They look the same, they’re dressed the same, and when they talk, they sound the same. But when teachers are handing out report cards in school, it’s important to know which brother is which! Even though they look the same, the twins are not interchangeable.

Twin boys. Jimmy got an A+, but Timmy got a C.

Similarly, when we write code, sometimes two objects that look alike are interchangeable - like shirts that look the same. Other times, two objects that look alike still need to be treated as separate objects, each with its own identity - like twin brothers who look the same.

Value classes are used to create objects where the identity does not matter, like the shirts. As long as two instances of a value class contain the same value, they can be considered equal. This constraint frees the Kotlin compiler to make optimizations to avoid boxing the value whenever possible.

Besides not having an identity, value classes in Kotlin are also immutable5. (As you recall from above, the underlying property of an inline class must be declared with val rather than var). This constraint gives Kotlin the freedom to choose how to represent the value in memory when multiple functions or objects use it. For example, when a function is invoked with an instance of a value class as an argument, the bits of that value might be copied to a new spot in memory that represents that stack frame, or Kotlin might choose to pass a reference to the location where the value already exists in memory. It has the freedom to choose.

Are Value Classes the Same as Value Objects?

You might be wondering if value classes in Kotlin are the same thing as value objects described in such classic tomes as Patterns of Enterprise Application Architecture,6 and Domain-Driven Design: Tackling Complexity in the Heart of Software.7

Both of these works describe value objects with the same characteristics as we saw above:

  1. They have no identity
  2. They should be immutable

While the concepts are identical, the context is different. Value objects apply to the domain - you might represent a phone number in a contact list app with a value object, for example. Even if your app is written in some other programming language - one that has no notion of a value class - you could still model the phone number in a way that it doesn’t have any unique identifier property, and you could still treat it as immutable.

Wrap-up

Teacher cartoon

Are you ready to start using inline classes? As of Kotlin 1.5, they’re now a stable part of the language.

Although we’ve covered the basics, there are some fascinating considerations to keep in mind when using inline classes. In fact, if you aren’t aware of what’s happening inside, inline classes could produce code that runs more slowly than normal classes.

In the next article, we’ll pop open the hood and look inside to get an in-depth understanding of exactly how they work, so that you can get the best possible use out of them.

See you then!

Resources

  • This article was first published back in 2018 when inline classes were still an experimental feature, and there was no official documentation about them. Nowadays you can find the official docs about inline classes on the main Kotlin website.
  • More information about the future possibilities of value classes can be found in the KEEP notes.

  1. You might also be wondering about local variables that are objects, and objects that contain primitives. In the first case, the object is stored on the heap, and a reference to it is stored on the stack. In the second case, the primitive is stored within the object on the heap. [return]
  2. There are lots of conditions that can affect their performance. In this article, we’re focused on introducing the concept, so we’ll stick with the best inlining scenario. The next article will cover some edge cases. [return]
  3. Technically, a call to an inline class’ constructor still results in a static function call in the generated bytecode, but that call still only deals with the primitive literal 24 here. In other words, no object was actually instantiated. [return]
  4. This again is a slight simplification. Technically this function name gets mangled (e.g., instead of just toMinutes(), it would be something like toMinutes_tJtZqfA()). Also, the call to the Minutes constructor in Kotlin results in a static function call in the compiled code, as mentioned in the previous footnote. [return]
  5. In Kotlin, value classes guarantee at least shallow immutability, where the fields directly on the class are immutable, but fields nested within those might be mutable. [return]
  6. Fowler, M. (2003) Patterns of Enterprise Application Architecture (pp. 486-487). Addison-Wesley. [return]
  7. Evans, E. (2004) Domain-Driven Design: Tackling Complexity in the Heart of Software (pp. 97-103). Addison-Wesley. [return]

Share this article:

  • Share on Twitter
  • Share on Facebook
  • Share on Reddit