Recently the Valhalla team published an early-access JDK build that implements the first part of the value classes story. More details and instructions on how to try it here. While it needs time to mature, this is great news for the JVM ecosystem! It’s also a good opportunity to look at Kotlin’s own plans for value classes, (im)mutability and how it’s all related to data classes.
Data classes, records, case classes? Product types!
Starting from the "early" days of high-level programming languages, there have been many similar concepts: records (introduced in ALGOL and Pascal), structs, named tuples, case classes, data classes in Kotlin and so on. All of them serve the same fundamental purpose, which is to combine potentially heterogeneous pieces of data into a single unit. Depending on the specifics of the target platform, they may or may not come with additional semantics. But first and foremost, they exist to carry data.
Theoretically, it’s all about so-called product types, because a combined type (whatever it is) can hold the Cartesian product of its values: |X| × |Y|. And here are value classes, which, as the name suggests, also revolve around “values” and are product types as well. They are primarily designed to hold data, but their semantics differ quite a bit from existing constructs in Kotlin.
Let’s unpack and see why it can be a great addition to the language.
Value classes. Building blocks.
In short, we plan to introduce value classes to Kotlin. They somewhat reflect value classes in Java in that they don’t have identity (value classes after all), but they don’t exactly replicate the same design. Next, I’ll describe the building blocks and then the story of value class mutations, which quite stands out from Java’s perspective.
Destructuring
Remember we were talking about name-based destructuring?
We mentioned that existing data classes have the problem of reusing operator component functions, which is not ideal semantically and also increases the ABI surface. The good news is that value classes are a new kind of classes, so we can avoid adding those component functions and adopt the name-based destructuring design right away:
value class Money(val amount: BigDecimal, val currency: String)
val (amount, currency) = money // good!
val (currency) = money // good! Take only currency
val (amount, country) = money // error! There's no "country" property
equals, hashCode and toString
Positional destructuring might not have been that great, but the autogenerated methods from Any proved useful, and we’ll generate structural implementations of equals and hashCode, as well as a pretty toString:
value class Money(val amount: BigDecimal, val currency: String)
println(money) // prints "Money(amount=10.0, currency=USD)"
Value based type, shallow immutability
Value classes don’t have identity, they are entirely represented by their underlying data.
As a result, the === (identity equality) operator doesn’t make sense for them.
For the same reason, mutability also doesn’t apply to value classes. In Kotlin, this means that a value class can only have val properties:
value class Money(val amount: BigDecimal, val currency: String) // good!
value class Book(val name: String, var author: Author) // error! "author" cannot be var
We call this shallow immutability: while the value class itself is immutable, the objects it holds might still be mutable (Author in the example can have vars, for instance). Deep immutability, where no part of an object can be mutated, is a topic for another post ;)
This requirement of being identityless is what mostly distinguishes value classes from data classes and makes them unique in the language
(Im)mutability. Copy vars
So. Value classes are shallow immutable (can have only `val`s in terms of today’s Kotlin). In order to adjust the value of a value class, we need to call a constructor, invoke a copy function, use a wither, or do something similar. Note that this operation is safe as we simply make a new copy and cannot affect the current state of a program.
On the other hand, mutation of a reference, can be done by using just = operator and can be destructive as it actually changes the state. Compare:
value class ValuePostcode(val code: String)
class RefPostcode(var code: String)
var valPostcode = ValuePostcode("1021ab")
val refPostcode = RefPostcode("1021ab")
// Safe operation. Done with constructor, copy, wither
valPostcode = ValuePostcode(valPostcode.code.uppercase())
// Potentially unsafe, references to refPostcode also affected. Shortly done with "="
refPostcode.code = refPostcode.code.uppercase()
And here’s the trick. Good system design teaches us that abstractions should make it easy to do "the right thing" and discourage "the wrong thing". However, with plain value classes, adjusting values has actually become easier when using references, even though that can lead to problems!
Consciously, we may understand that value classes are preferable in certain scenarios when we want safe “mutations”. But in practice, if working with references is simply easier, that’s the path many will take. And that’s solely because the abstractions are designed this way. That’s why we want to try the route of "copy vars" that will allow us to get convenience of reference mutations and safety of value classes:
value class ValuePostcode(copy var code: String) // note "copy" here
var valPostcode = ValuePostcode("1021ab")
// Safe operation. Under the hood, we created a copy. On the language level, it's just "="
valPostcode.code = valPostcode.code.uppercase()
// Under the hood, semantically, it's still the same as invoking a constructor
// valPostcode = ValuePostcode(valPostcode.code.uppercase())
This way, modifying value classes becomes just as easy as working with references. And since value classes now offer more benefits without the downsides of mutable references, they can truly become an everyday tool that will lead us to building safer systems!
Runtime performance and the Valhalla project
Alright. So far, we’ve mostly talked about the semantics of value classes: how they get support of name-based destructuring and provide convenient equals, hashCode, and toString implementations, as well as language support for safe mutations through “copy vars”.
But what about runtime performance?
When it comes to Kotlin/JVM, it’s up to the JVM. Value classes in Kotlin are designed so that the JVM can optimize them, but their primary goal is semantics: we don’t design them specifically for performance. Thanks to Project Valhalla, we can use the same type descriptors on the JVM and get scalarization and heap flattening automatically. Moreover, inline functions will allow us to write even generic code that works with value classes and benefit from scalarization optimizations!
At the same time, this means Kotlin isn’t tied to Valhalla or even the JVM. We can and we will ship value classes independently of what happens in Java or the JVM. In practice, this means that if you target a JVM without value class support but use a modern Kotlin version, you can still take advantage of value classes and their language-level semantics. Additionally, if you target a modern JVM that supports value classes, you’ll also gain runtime performance improvements.
That said, the runtime performance of value classes can be tricky. It depends on your value class size, integrity requirements, nullability, and heap flattening. So we’d love to put runtime performance on the shoulders of the JVM and keep it out of the language design itself.
data classes?
Reiterating once more: Value classes come with name-based destructuring from day one (no components in ABI), pretty toString and equals with hashCode. They have a safer mutability story and also get runtime performance benefits when targeting modern JVMs.
At this point, you might think: “Nice, but isn’t that just like data classes, only… better?” Right.
Ultimately, value classes will become more and more common in Kotlin. Data classes will stay, we’re not deprecating them, and they’ll still be useful when reference semantics matter for your domain. But their usage will naturally decline.
In short: if you’re choosing between a data class and a value class, start with a value class first!
Useful links
-
Talk at KotlinConf'25: A (deep) dive into (shallow) immutability: Valhalla and beyond | Marat Akhin
-
Talk at JVMLS'25: Better immutability in Kotlin with Valhalla