Skip to main content

Kotlin Inline Classes

Introduction

Kotlin inline classes are a powerful feature introduced to help reduce the runtime overhead associated with creating wrapper types. When we need to wrap primitive types or other values for type safety or to add additional functionality, traditional wrapper classes add memory overhead due to heap allocation. Inline classes solve this problem by "inlining" the underlying value where possible, keeping the type safety benefits without the performance cost.

In this article, we'll explore how inline classes work in Kotlin, when to use them, and how they can improve your application's performance.

What Are Inline Classes?

Inline classes are a way to create a distinct type that, at runtime, is represented only by its underlying value. They are particularly useful when you need to add type safety to a primitive value or add functionality without introducing the overhead of object creation.

Basic Syntax

To create an inline class, you use the value class keyword (or the older inline class syntax in earlier Kotlin versions):

kotlin
// Modern syntax (Kotlin 1.5+)
@JvmInline
value class Password(val value: String)

// Older syntax (pre-Kotlin 1.5)
// inline class Password(val value: String)

Here, Password is an inline class that wraps a String. At compile time, it provides type safety, but at runtime, it's represented just by the underlying String in most cases.

How Inline Classes Work

Let's take a closer look at how inline classes function behind the scenes:

kotlin
@JvmInline
value class Meters(val value: Double)

fun calculateArea(width: Meters, height: Meters): Double {
return width.value * height.value
}

fun main() {
val width = Meters(5.0)
val height = Meters(10.0)
val area = calculateArea(width, height)
println("Area: $area square meters")
}

Output:

Area: 50.0 square meters

In this example, Meters is an inline class wrapping a Double. Although we're creating instances of Meters, at runtime, the compiler will optimize this to directly use the Double values, avoiding the overhead of object creation.

Benefits of Inline Classes

1. Type Safety Without Overhead

The primary benefit of inline classes is providing type safety without the overhead of creating objects:

kotlin
@JvmInline
value class UserId(val id: Int)
@JvmInline
value class ArticleId(val id: Int)

// Without inline classes, both functions would have the same signature:
// fun getUser(id: Int): User
// fun getArticle(id: Int): Article

// With inline classes:
fun getUser(id: UserId): User {
// Implementation
return User(id.id, "John Doe")
}

fun getArticle(id: ArticleId): Article {
// Implementation
return Article(id.id, "Kotlin Inline Classes")
}

fun main() {
val userId = UserId(1)
val articleId = ArticleId(2)

val user = getUser(userId)
// This would cause a compilation error:
// val user = getUser(articleId)

println("Found user: ${user.name}")
}

In this example, UserId and ArticleId are both backed by Int, but they're distinct types. This prevents passing a user ID where an article ID is expected, catching potential bugs at compile time.

2. Performance Improvements

Let's measure the performance difference between a regular class and an inline class:

kotlin
// Regular class
data class RegularWrapper(val value: Int)

// Inline class
@JvmInline
value class InlineWrapper(val value: Int)

fun main() {
val iterations = 10_000_000

// Test with regular class
val startRegular = System.currentTimeMillis()
var sumRegular = 0
for (i in 1..iterations) {
val wrapper = RegularWrapper(i)
sumRegular += wrapper.value
}
val endRegular = System.currentTimeMillis()

// Test with inline class
val startInline = System.currentTimeMillis()
var sumInline = 0
for (i in 1..iterations) {
val wrapper = InlineWrapper(i)
sumInline += wrapper.value
}
val endInline = System.currentTimeMillis()

println("Regular class time: ${endRegular - startRegular} ms")
println("Inline class time: ${endInline - startInline} ms")
}

In this performance test, the inline class version typically performs significantly better, especially with a large number of iterations, due to reduced heap allocations.

Limitations and Restrictions

Inline classes come with several limitations that you should be aware of:

1. Single Value

Inline classes must contain exactly one primary constructor property:

kotlin
// Valid
@JvmInline
value class Name(val value: String)

// Invalid - would cause compilation error
/*
@JvmInline
value class Person(val name: String, val age: Int)
*/

2. No Inheritance

Inline classes cannot inherit from other classes and cannot be inherited from:

kotlin
// Cannot extend other classes
/*
@JvmInline
value class SpecialString(val value: String) : CharSequence
*/

// Cannot be abstract
/*
@JvmInline
abstract value class AbstractValue(val value: Int)
*/

However, inline classes can implement interfaces:

kotlin
interface Printable {
fun print()
}

@JvmInline
value class PrintableString(val value: String) : Printable {
override fun print() {
println(value)
}
}

fun main() {
val message = PrintableString("Hello, Inline Classes!")
message.print()
}

Output:

Hello, Inline Classes!

3. Boxing in Certain Situations

In some cases, the inline class value needs to be "boxed" into a real object:

  • When used as a generic type
  • When assigned to a reference of interface type
  • When used in functions that use inline class as a receiver
kotlin
@JvmInline
value class BoxedInt(val value: Int)

fun main() {
// Boxed: stored as BoxedInt object
val list: List<BoxedInt> = listOf(BoxedInt(1), BoxedInt(2))

// Unboxed: stored and used as an Int
val x = BoxedInt(10)
val y = x.value + 5

println("y = $y")
}

Output:

y = 15

Real-World Applications

1. Type-Safe ID Classes

One common use case for inline classes is creating type-safe IDs for your domain entities:

kotlin
@JvmInline
value class CustomerId(val value: Long)
@JvmInline
value class OrderId(val value: Long)
@JvmInline
value class ProductId(val value: Long)

class OrderService {
fun getOrder(id: OrderId): Order = Order(id, CustomerId(123), listOf(ProductId(456)))

// This prevents accidentally passing a CustomerId where an OrderId is expected
// fun getOrder(id: CustomerId): Order // This would be a different method
}

data class Order(val id: OrderId, val customerId: CustomerId, val products: List<ProductId>)

fun main() {
val orderService = OrderService()
val order = orderService.getOrder(OrderId(789))
println("Fetched order: ${order.id.value}")

// This would cause a compilation error:
// val wrongOrder = orderService.getOrder(CustomerId(789))
}

Output:

Fetched order: 789

2. Domain-Specific Types

Inline classes are excellent for creating domain-specific types that add meaning to primitive values:

kotlin
@JvmInline
value class EmailAddress(val value: String) {
init {
require(value.matches(Regex(".+@.+\\..+"))) { "Invalid email format" }
}

val domain: String
get() = value.substringAfterLast("@")
}

fun sendEmail(to: EmailAddress, subject: String, body: String) {
println("Sending email to ${to.value} (domain: ${to.domain})")
println("Subject: $subject")
println("Body: $body")
}

fun main() {
try {
val email = EmailAddress("[email protected]")
sendEmail(email, "Hello", "This is a test email")

// This would throw an IllegalArgumentException:
// val invalidEmail = EmailAddress("invalid-email")
} catch (e: IllegalArgumentException) {
println("Error: ${e.message}")
}
}

Output:

Sending email to [email protected] (domain: example.com)
Subject: Hello
Body: This is a test email

3. Units of Measurement

Inline classes are perfect for representing units of measurement:

kotlin
@JvmInline
value class Meters(val value: Double) {
operator fun plus(other: Meters) = Meters(value + other.value)
operator fun times(factor: Double) = Meters(value * factor)
}

@JvmInline
value class Seconds(val value: Double)

fun calculateSpeed(distance: Meters, time: Seconds): Double {
return distance.value / time.value
}

fun main() {
val distance = Meters(100.0)
val time = Seconds(20.0)

val speed = calculateSpeed(distance, time)
println("Speed: $speed meters per second")

// Using operator overloading
val totalDistance = Meters(50.0) + Meters(25.0)
val doubledDistance = totalDistance * 2.0

println("Total distance: ${totalDistance.value} meters")
println("Doubled: ${doubledDistance.value} meters")

// This would cause a compilation error:
// calculateSpeed(time, distance)
}

Output:

Speed: 5.0 meters per second
Total distance: 75.0 meters
Doubled: 150.0 meters

Inline Classes vs. Typealias

It's important to understand the difference between inline classes and type aliases:

kotlin
typealias UsernameAlias = String
@JvmInline
value class Username(val value: String)

fun validateUsername(name: Username) {
println("Validating username: ${name.value}")
}

fun main() {
val username = Username("john_doe")
validateUsername(username)

val usernameAlias: UsernameAlias = "jane_doe"
// This would cause a compilation error:
// validateUsername(usernameAlias)

// This works because UsernameAlias is just an alias for String
val regularString: String = usernameAlias
println(regularString)
}

Output:

Validating username: john_doe
jane_doe

Type aliases provide alternative names for existing types but don't create new types. Inline classes, on the other hand, create entirely new types with distinct identities.

Best Practices

  1. Use for Wrapper Types: Use inline classes when wrapping primitive types or other value types.

  2. Keep It Simple: Since inline classes are limited to a single property, they're best for simple wrappers.

  3. Add Domain Validation: Add validation in the init block to ensure the wrapped value meets your domain requirements.

  4. Consider Boxing Impact: Be aware that using inline classes with generics or interfaces will cause boxing.

  5. Document Carefully: Make it clear in your code documentation that a type is an inline class, as this has implications for API design.

Summary

Kotlin inline classes provide a powerful mechanism to create type-safe wrappers around primitive types and other values without the performance overhead of traditional classes. They're particularly useful for:

  • Creating type-safe IDs and domain-specific types
  • Adding domain validation to primitive values
  • Improving performance in code that creates many instances of small wrapper types
  • Implementing domain-specific units of measurement

While they come with limitations such as the single-value restriction and limited inheritance capabilities, inline classes offer an excellent balance between type safety and performance when used appropriately.

Additional Resources

Exercises

  1. Create an inline class that represents a percentage value (0-100) with validation.
  2. Implement an inline class for currency amounts with methods for addition, subtraction, and comparison.
  3. Design a simple API using inline classes for type safety (e.g., UserService with methods that take UserID, Email, etc.).
  4. Create a benchmark comparing the performance of regular classes versus inline classes for a specific use case.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)