Are Java records intended to eventually become value types?
Disclaimer: This answer only extends the other answers by summarizing some implications and giving some examples. You should not make any decisions relying on this information because pattern matching and value types are still subjects of change.
There are two interesting documents about data classes aka records vs value types:
The older version from February 2018
http://cr.openjdk.java.net/~briangoetz/amber/datum_2.html#are-data-classes-the-same-as-value-types
and the newer version from February 2019
https://cr.openjdk.java.net/~briangoetz/amber/datum.html#are-records-the-same-as-value-types
Each document includes a paragraph about differences between records and value types. The older version says
The lack of layout polymorphism means we have to give up something else: self-reference. A value type V cannot refer, directly or indirectly, to another V.
Moreover,
Unlike value types, data classes are well suited to representing tree and graph nodes.
However,
But value classes need not give up any encapsulation, and in fact encapsulation is essential for some applications of value types
Let's clarify that:
You won't be able to implement node-based composited data structures like linked lists or hierarchical trees with value types. However, you can use the value types for the elements of those data structures. Moreover, value types support some forms of encapsulation in opposite to records which don't at all. This means you can have additional fields in value types which you haven't defined in the class header and which are hidden to the user of the value type. Records can't do that because their representation is restricted to their API, i.e. all their fields are declared in the class header (and only there!).
Let's have some examples to illustrate all that.
E.g. you will be able to create composited logical expressions with records but not with value types:
sealed interface LogExpr { boolean eval(); }
record Val(boolean value) implements LogExpr {}
record Not(LogExpr logExpr) implements LogExpr {}
record And(LogExpr left, LogExpr right) implements LogExpr {}
record Or(LogExpr left, LogExpr right) implements LogExpr {}
This will not work with value types because this demands the ability of self-references of the same value type. You want to be able to create expressions like "Not(Not(Val(true)))".
E.g. you can also use records to define the class Fraction:
record Fraction(int numerator, int denominator) {
Fraction(int numerator, int denominator) {
if (denominator == 0) {
throw new IllegalArgumentException("Denominator cannot be 0!");
}
}
public double asFloatingPoint() { return ((double) numerator) / denominator; }
// operations like add, sub, mult or div
}
What about calculating the floating point value of that fraction? You can add a method asFloatingPoint() to the record Fraction. And it will always calculate (and recalculate) the same floating point value each time it is invoked. (Records and value types are immutable by default). However, you cannot precalculate and store the floating point value inside this record in a way hidden to the user. And you won't like to explicitly declare the floating point value as a third parameter in the class header. Luckily, value types can do that:
inline class Fraction(int numerator, int denominator) {
private final double floatingPoint;
Fraction(int numerator, int denominator) {
if (denominator == 0) {
throw new IllegalArgumentException("Denominator cannot be 0!");
}
floatingPoint = ((double) numerator) / denominator;
}
public double asFloatingPoint() { return floatingPoint; }
// operations like add, sub, mult or div
}
Of course, hidden fields can be one reason why you want to use value types. They are only one aspect and probably a minor one though. If you create many instances of Fraction and maybe store them in collections, you will benefit a lot from the flattened memory layout. That's definitely a more important reason to prefer value types to records.
There are some situations where you want to benefit from both records and value types.
E.g. you might want to develop a game in which you move your piece through a map.
You saved a history of moves in a list some time ago where every move stores a number of steps into one direction. And you want to compute the next position dependending on that list of moves now.
If your class Move is a value type, then the list can use the flattened memory layout.
And if your class Move also is a record at the same time you can use pattern matching without the need of defining an explicit deconstruction pattern.
Your code can look like that:
enum Direction { LEFT, RIGHT, UP, DOWN }´
record Position(int x, int y) { }
inline record Move(int steps, Direction dir) { }
public Position move(Position position, List<Move> moves) {
int x = position.x();
int y = position.y();
for(Move move : moves) {
x = x + switch(move) {
case Move(var s, LEFT) -> -s;
case Move(var s, RIGHT) -> +s;
case Move(var s, UP) -> 0;
case Move(var s, DOWN) -> 0;
}
y = y + switch(move) {
case Move(var s, LEFT) -> 0;
case Move(var s, RIGHT) -> 0;
case Move(var s, UP) -> -s;
case Move(var s, DOWN) -> +s;
}
}
return new Position(x, y);
}
Of course, there are many other ways to implement the same behavior. However, records and value types give you some more options for implementations which can be very useful.
Records and primitive classes (the new name for value types) have a lot in common -- they are implicitly final and shallowly immutable. So it is understandable that the two might be seen as the same thing. In reality, they are different, and there is room for both of them to co-exist, but they can also work together.
Both of these new kinds of classes involve some sort of restriction, in exchange for certain benefits. (Just like enum
, where you give up control over instantiation, and are rewarded with a more streamlined declaration, support in switch
, etc.)
A record
requires you to give up on extension, mutability, and the ability to decouple the representation from the API. In return, you get implementations of constructors, accessors, equals
, hashCode
, and more.
A primitive class
requires you to give up on identity, which includes giving up on extension and mutability, as well as some other things (e.g., synchronization). In return, you get a different set of benefits -- flattened representation, optimized calling sequences, and state-based equals
and hashCode
.
If you are willing to make both compromises, you can get both sets of benefits -- this would be a primitive record
. There are lots of use cases for primitive records, so classes that are records today could be primitive records tomorrow, and would just get faster.
But, we don't want to force all records to be primitive or for all primitives to be records. There are primitive classes that want to use encapsulation, and records that want identity (so they can organize into trees or graphs), and this is fine.