Is it okay to make a record cloneable?
Asked Answered
A

2

6

I couldn't find any material on Google saying about the use of Cloneable records.

I was thinking of something like this:

record Foo() implements Cloneable {
    public Foo clone() {...}
}

Is it a good thing? Should we avoid it in favor of the future withers?

Arin answered 30/4, 2021 at 13:15 Comment(6)
You should avoid Cloneable in general.Insensitive
Do you have any material explaining why?Arin
About Java cloneableInsensitive
Records are a new Java feature (since Java 14). The only reason to avoid them for now would be because your code would need to run on older Java versions, or because you are using something else (libraries etc.) that don't understand records. That doesn't mean you should avoid using this feature forever.Galvani
It is OK to make a record cloneable. You should follow all the usual caveats about why you probably don't want to use clone at all (though this has nothing to do with records), and you must conform to the specification of java.lang.Record which constrains the behavior of the constructor+accessors+equals(), but if you can do all that, its OK.Muoimuon
The standard advice for Clonable is "just write a copy constructor instead." That's perfectly good advice for records too.Muoimuon
C
14

Apart from the fundamental problems of Cloneable there's another strong reason why one shouldn't make a record Cloneable:

Records are inherently immutable. One of the biggest advantages of immutable classes is that one can stop worrying about object identity: any two objects with the same values can be used entirely interchangeable. The JavaDoc has a concept called Value-based Classes that goes a bit further (by disallowing public constructors and making any explicit use of object identity a mistake), but does describe the basic idea well.

Therefore creating a clone() of a record would only ever produce a second object that for all intents and purpose should behave exactly the same as the original and can't ever change to behave differently (since records are immutable).

This suggests that there's no valid reason to clone() a Record.

And just for completeness sake: it is possible (but again, not suggested) to implement Cloneable in a record:

record Foo(String a) implements Cloneable {
  public Foo clone() {
    try {
      return (Foo) super.clone();
    } catch (CloneNotSupportedException e) {
      throw new RuntimeException("this can't happen", e);
    }
  }
}

Running this test code proves that a clone is actually created:

Foo original = new Foo("bar");
Foo clonedFoo = original.clone();
System.out.println(original + " / " + clonedFoo);
System.out.println(System.identityHashCode(original) + " / " + System.identityHashCode(clonedFoo));

produces this output:

Foo[a=bar] / Foo[a=bar]
670700378 / 1190654826
Cotenant answered 30/4, 2021 at 13:41 Comment(11)
Thanks. So in practice, it should work just fine, but it wouldn't be of any use.Arin
Yes, I've added a sample demonstrating that it works.Cotenant
I think you're (dangerously) confusing records (which are a language feature about classes whose API and representation are transparently related) and primitive classes (nee value types). "Value-based classes" are entirely about classes that are intended to have value semantics; records do not necessarily.Muoimuon
The claim made here that records have no use at all for clone() is overblown. There are all the usual reasons that clone() is problematic, but records neither add nor subtract from these.Muoimuon
The claim that a clone of a record is indistinguishable from the original is still incorrect even if you ignore identity. A clone() method is free to deeply clone components, some of which might be mutable (e.g., arrays.) These are "advanced" uses of records, and might be something you would reasonably discourage in general, but they're not ruled out by the design or implementation.Muoimuon
@BrianGoetz: yes, I agree that I assumed that records would only be used for immutable classes, but they don't necessarily need to be deeply immutable. But I still think that immutable records will be the primary use case and the majority of uses. And personally I'd think using records for non-immutable uses cases is risky as well, since I don't think I'm the only one with that blind spot and/or bias.Cotenant
@JoachimSauer Full agreement with all of that -- yes immutability is the primary case, and many of the other cases should give users cause to more deeply explore what they're trying to achieve. But the OP asked if there was any reason a record cannot implement Clonable. And the simple answer is, the two concepts are compatible, even if the intersection is small. So by all means, say "you can, but it's risky, and you're probably doing something questionable, are you sure you want that?" And perhaps: "LIke with every other class besides arrays, prefer a copy constructor."Muoimuon
@BrianGoetz well, records have stable final fields which means that when using super.clone(), modifying the cloned fields to achieve deep cloning is not possible. Of course, you still can achieve deep cloning by just using the copy constructor (as for final classes, it makes no difference), but then, there is no benefit in implementing Cloneable, as that interface does not provide a clone() method but only enables the super.clone() shallow copying facility.Andrew
@Andrew I'm not trying to defend Clonable, but the OP asked if there was anything about records that was inconsistent with Clonable, and the answer is no. If a copy constructor does the job, it is a better choice, no question.Muoimuon
@JoachimSauer and Holger: The reason I'm pushing back here is not that I'm some sort of secret fan of Clonable; it's that I'm a rabid anti-fan of making things more complicated than they are. Clonable sucks, but its suckage is completely independent of records. Clonable records suck exactly as much as Clonable classes, no more, and no less. Let's not burden people's perceptions of records with irrelevant coupling to other features. Records are great, Clonable sucks, and neither affects the other. That's a simple story.Muoimuon
@BrianGoetz that’s understood. I was referring to your sentence “A clone() method is free to deeply clone components”. For ordinary classes, super.clone() can be used to create a shallow copy of exactly the same type, which is relevant to non-final classes, followed by updating final fields via reflection, to make it a deep copy. This doesn’t work with records, due to the fact that its final fields can not be modified via reflection like with other classes. The only way to deeply copy a record is via constructor. So it’s not exactly the same as with other classes.Andrew
G
1

For anyone who's looking for a clean solution on copying/cloning records in Java, I advise you to use Builder pattern. This library does it well so you don't need to write cumbersome builders yourself: https://github.com/Randgalt/record-builder. Using this library, OPs example will look like this:

@RecordBuilder    
record Foo(String arg) {}

Usage:

Foo original = new Foo("bar");
Foo clonedFoo = Foo.builder(original).arg("foo").build();

NOTE: I'd expect Lombok to support self-instance constructors for @Builder-generated builders soon.

Goody answered 11/6 at 22:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.