Are Java records intended to eventually become value types?

Solution 1:

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.

Solution 2:

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 been 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.

Solution 3:

Note: I might not be so correct as this is about future motivations in Java or the intent of the community about value types. The answer is based on my personal knowledge and the information available openly on the internet.

We all know that the Java Community is so big and mature enough that they do not (and could not) add any random feature for experiments until & unless stated otherwise. Keeping this in mind, I remember this article on the OpenJDK website which briefly describes the idea of value types in Java. One thing to notice here is that it is written/updated in April 2014 while the record first came out in Java 14 in March 2020.

But in the above article itself, they did give the example of record while explaining the value types. Most of its description does match to the current record as well.

The JVM type system is almost entirely nominal as opposed to structural. Likewise, components of value types should be identified by names, not just their element number. (This makes value types more like records than tuples.)

And with no surprise, Brian Goetz was also a co-author of this article.

But there are other places in the universe where the record is also represented as data classes. See this article, it's also written/updated by Brain. The interesting part is here.

Values Victor will say "a data class is really just a more transparent value type."

Now, considering all these steps together, it does look like record is a feature motivated by (or for) tuples, data classes, value types, etc... etc... but they are not a substitute for each other.

As Brain mentioned in the comments:-

A better way to interpret the documents cited here that tuple-like types are one possible use for value types, but by far not the only one. And it is possible to have record types that need an identity. So the two will commonly work together, but neither subsumes the other -- each brings something unique to the table.

Coming to your concern about performance increase, here is an article that compared the performance of Java 14 records (preview) vs traditional class. You might find it interesting. I did not see any significant improvements in the performance from the results of the above link.

As far as I know, the stack is significantly faster than that of the heap. So due to the fact that record is actually a special class only, which then goes to the heap than to the stack (value type/primitive type should live in the stack like the int, remember Brian “Codes like a class, works like an int!” ). By the way, this is my personal view, I might be wrong for my statements on stack and heap here. I'll be more than happy to see if anyone corrects me or supports me on this.