Kotlin 2.5 Lets Value Classes Hold Multiple Properties
Before Kotlin 2.5.0-Beta2, inline value classes could define custom wrapper types for a single value. If a type needed multiple properties, only regular classes or data classes were available.
With data classes, even if two instances contain the same data, they can still be distinguished by reference, even if that distinction has no practical meaning for the type.
This could limit runtime optimizations and cause code to unnecessarily depend on whether object references remain unchanged.
For example:
data class Point(val x: Int, val y: Int)
val a = Point(1, 2)
val b = Point(1, 2)
val c = a
println(a == b) // true: same values
println(a === b) // false: two different objects
println(a === c) // true: same reference
For coordinates, we usually only care whether it's (1, 2). Whether it's the first object created or another created later has no meaning to the concept of "coordinates". But regular classes and data classes still allow code to observe this difference using ===.
Kotlin 2.5.0-Beta2 introduces experimental full value classes. The values of these types are composed of the individual properties, and instances have no identity you can rely on. Directly comparing these values with === is rejected by the compiler: You cannot rely on whether they are the same object to distinguish them.
For example, you can declare a full value class Complex to represent complex numbers with real and imaginary parts:
value class Complex(val real: Double, val imaginary: Double)
Similar to data classes, the compiler automatically generates equals(), hashCode(), and toString() for full value classes.
However, there are two differences from data classes:
- No
componentN()functions are generated. But name-based destructuring works without any additional enabling. - No
copy()function is generated. In the future,copy varscould be used instead, which we won't discuss now.
Abstract Value Classes
Full value classes can be abstract.
Unlike interfaces, they can define initialization logic that runs when each subclass is constructed.
For example, the OrderedInterval value class below receives start and end as constructor parameters. The IntInterval value class stores these values in its own start and end properties.
sealed value class OrderedInterval<T : Comparable<T>> protected constructor(
start: T,
end: T,
) {
init {
require(start <= end)
}
}
value class IntInterval(
val start: Int,
val end: Int
) : OrderedInterval<Int>(start, end)
Value Objects
Full value classes can also have no properties. Use value object to declare value objects.
They are suitable for types with only one possible value and no instance identity, such as states or tokens:
sealed interface UserResponse
value class UserFound(val user: User) : UserResponse
value object UserUnknown : UserResponse
How to Enable
This feature is still experimental.
To try it, add the following compiler option to your build file:
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xfull-value-classes")
}
}
Changes to Class Declarations
With these new value classes, value class no longer necessarily means inline.
Inline value classes may directly use the wrapped value at runtime. This representation can also affect external behavior such as JVM method signatures and serialization results.
Full value classes do not follow the same inline representation rules, so tools such as Spring and serialization libraries may handle the two types differently.
To clearly distinguish them, according to the current migration plan, the compiler will in the future require using the inline modifier, or the @JvmInline value class declaration, to explicitly mark inline value classes. A declaration that only uses value class will no longer be considered an inline value class.
The Kotlin team plans to phase in this change:
- Kotlin 2.5.20 will issue a warning for single-field value classes without explicit inline marker.
- Kotlin 2.6.0 or 2.7.0 will upgrade that warning to an error.
You can use the -Xinline-value-class-migration=warning and -Xinline-value-class-migration=strict compiler options to prepare for this change ahead of time.
Preparing Library Classes for Migration to Value Classes
As a library author, you can use the @WillBecomeValue annotation to prepare for migrating regular classes to full value classes in the future. The compiler will warn when user code depends on object identity, such as reference equality (===), synchronization, or identity hash codes. These warnings give users time to adjust their code before the class is migrated.
This annotation is still experimental and must be explicitly enabled with @OptIn(ExperimentalValueClassApi::class). Users of the library do not need to enable anything extra when using classes annotated with it.