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));
}
}