Reputation: 79
I would like to know how to rewrite this class
public class ClassA {
private final String foo;
private final String bar;
public ClassA(String foo) {
this.foo = foo;
this.bar = foo.toUpperCase();
}
// getters...
}
as a record class.
The best I've managed to do is this
public record ClassA(String foo, String bar) {
public ClassA(String foo) {
this(foo, foo.toUpperCase());
}
}
The problem is that this solution creates two constructors while I want only one which accepts the string foo
Upvotes: 3
Views: 936
Reputation: 95466
This is more of a response to https://stackoverflow.com/a/73137529/3553087 than any sort of recommendation as to how to do this. The cited answer is better than the OP's version, but it still is subverting the spirit of records, which is that they are nominal tuples, perhaps with some invariants that constrain the components.
Here's an example of a good use of records with an invariant:
record Range(int low, int hi) {
public Range {
if (low > hi) throw new IllegalArgumentException();
}
}
The canonical constructor validates the arguments, rejecting invalid ones, and thereafter, the whole thing operates like a transparent, immutable container for some tuple, deriving a useful API (constructor, deconstruction pattern (as of Java 19), accessors, equals, hashCode, toString) from the tuple.
The equivalent here is to be honest and admit what what you are writing is a tuple of an arbitrary string, and its uppercase version:
record StringWithCachedUppercase(String value, String uppercased) {
public StringWithCachedUppercase {
if (!uppercased.equals(value.toUpperCase(Local.ROOT)))
throw new IllegalArgumentException();
}
public StringWithCachedUppercase(String value) {
this(value, value.toUpperCase(Locale.ROOT));
}
}
Why is the linked answer less desirable? Because the canonical constructor pulls a fast one, and undermines the reasonable intuition that new XyRecord(x, y).y()
should return something related to the y
passed into the constructor. Maybe it's a normalized version, but it should be recognizable -- in the linked answer, it is completely ignored.
Some may balk at "but then you're computing the uppercased version twice", but that's no excuse for using the mechanism wrong. (And, since the whole rationale for this sort of thing is "I want to cache this seemingly-expensive computation", the only point of using it at all is if you're going to ask for the uppercase version many, many times. In which case an O(1) additional cost of construction is not relevant.)
This example illustrates a common case where records are an "almost", which is "tuple, but caching derived quantities". We considered this case at great length during the design of records, but in the end, concluded it should remain outside of the design center for records.
If you are really interested in caching derived quantities, then they can be computed lazily and cached in a WHM:
record StringWrapper(String s) {
static Map<StringWrapper, String> uppers = Collections.synchronizedMap(new WeakHashMap<>());
public String uppercase() {
return uppers.computeIfAbsent(this, r -> r.s.toUpperCase(Locale.ROOT));
}
}
Upvotes: 5
Reputation: 29038
Record classes always have a so-called canonical constructor, which expects arguments for all the field you've declared. This constructor would be generated by the compiler by default, but you can provide your own one, the key point: a canonical constructor is available for every record at runtime.
Here's a quote from the Java Language Specification:
To ensure proper initialization of its record components, a record class does not implicitly declare a default constructor (§8.8.9). Instead, a record class has a canonical constructor, declared explicitly or implicitly, that initializes all the component fields of the record class.
All non-canonical constructors, which are constructors with signatures that differ from the canonical, like your case constructor that expects a single argument ClassA(String)
, should delegate the call to the canonical constructor using so-called explicit constructor invocation, i.e. using this()
(precisely as you've done), otherwise such constructor would not compile.
A record declaration may contain declarations of constructors that are not canonical constructors. The body of every non-canonical constructor in a record declaration must start with an alternate constructor invocation (
§8.8.7.1
), or a compile-time error occurs.
Conclusion: since you've declared a record that has two fields, and you also need a non-canonical constructor expecting one argument, there would be two constructors: canonical and a non-canonical. There are no workarounds.
In addition, as @Brian Goetz has rightfully pointed out, it's worth to override the canonical constructor as well. Otherwise, there would be an ambiguity in regard whether the second argument of the record is an uppercase version of the first or not because of the possibility to use two constructors with the different logic.
If we define the record as follows:
public record ClassA(String foo, String bar) {
public ClassA(String foo) {
this(foo, foo.toUpperCase(Locale.ROOT));
}
}
The code below:
System.out.println(new ClassA("foo", "arbitrary string which not FOO"));
System.out.println(new ClassA("foo"));
Will produce the output:
ClassA[foo=foo, bar=arbitrary string which not FOO] // unexpected result
ClassA[foo=foo, bar=FOO] // desired result
To ensure that the second argument at any circumstances would be equal to the first argument turned to uppercase, we have to override the canonical constructor:
public record ClassA(String foo, String bar) {
public ClassA(String foo) {
this(foo, foo); // doesn't matter what would be provided as the second argument
}
public ClassA(String foo, String bar) {
this.foo = foo;
this.bar = foo.toUpperCase(Locale.ROOT);
}
}
Upvotes: 3