This is essentially impossible, in the sense that this just isn't how records work. You may be able to hack it together but that is very much not what records were meant to do, and as a consequence, if you do, the code will be confusing, the API will need considerable extra documentation to explain it doesn't work the way you think it does, and future lang features will probably make your API instantly obsoleted (it feels out of date and works even more weirdly now).
The essential nature of records
Records are designed to be deconstructable and reconstructable, and to support these features intrinsically, as in, without the need to write any code to enable any of this. Records just 'get all that stuff', for free, but at a cost: They are inherently defined by their 'parts'. This has all sorts of effects - they cannot extend anything (Because that would mean they are defined by a combination of their parts and the parts their supertype declares, that's more complex than is intended), and the parts are treated as final, and you can't make it act like somehow it isn't actually just a thing that groups together its parts, which is the problem with your code.
Let's make that 'if you try to do it, future language features will ruin your day' aspect of it and focus on deconstruction and the with
concept.
Yes, this mostly isn't part of java yet, but the record feature is specifically designed to be expanded to encompass deconstruction and all the features that it brings, and work is far along. Specifically, Brian Goetz, who is in charge of this feature and various features that expand on a more holistic idea (records is merely a small part of that idea), really loves this stuff and has repeatedly written about it. Including quite complete feature proposals.
Specifically, for records, you are soon going to be able to write this (note, as is usual with OpenJDK feature proposals, don't focus on the syntax or about concerns such as '.. but, does that mean 'with' is now a keyword?' - actual syntax is the very last thing that is fleshed out.
DatePair dp = ....;
DatePair newDp = dp with {
days = 20;
}
The concept of a 'deconstruction' is the same as a constructor, but in reverse: Take an object and break it apart into its constituent parts. For records, this is obvious, and in fact (and this is the key, why you can't do what you want in this way), baked in - records deconstruct by way of the elements you listed for the record (so, here, start
, end
, and days
) and you probably won't be able to change this.
The obj with {block;}
operation is syntax sugar for:
- Deconstruct
obj
.
- For each item that the deconstruction has produced, declare a local variable.
- Run
block
. The local vars are available, and aren't final - change whatever you want.
- Construct a new object of the same type as
obj
, using those local vars to pass to its constructor.
Your idea can only work if the deconstructor deconstructs solely into LocalDate start
and LocalDate end
, leaving long days
out of it entirely. It would require records to be able to state: Actually, this is my constructor, with different arguments from the record's component list, and once you open the door to writing your own constructor, you therefore then also have to write your own deconstructor.
The thing is, there is no syntax for deconstructors right now. Thus, if records did allow you to write your own constructor (with a different list of params than the record components), that means that if in the future a language feature is released such as with
, that requires a deconstructor, that some records can't support it. That's annoying to the java lang team: They'd love to say: "We introduced with
, which works with all records already! In the future once we release the deconstructor feature it should also be usable for classes that have a deconstructor".
That is to say, while I can't say I 100% know exactly what Brian and the OpenJDK team is thinking, I'd be incredibly surprised if they aren't thinking like the above.
The point of the above dive into OpenJDK's plans for near-future java is to explain that [A] why you can't make your own constructor in records, and [B] why there won't be a language feature coming along that will, unless it is part of a set of very significant updates (including deconstructor syntax, and that won't happen unless there are features that use it).
Good news!
Fortunately, there are very simple alternate strategies you can use here.
This seems to be the best fit for your needs, usable in today's java:
public record DatePair(LocalDate start, LocalDate end) {
public long days() {
return ChronoUnit.DAYS.between(start, end);
}
}
This removes days
from being treated as a component part of DatePair, but then, that is the point - components in records fundamentally can be changed independently of the other parts of it (possibly you can add code that then says the new state is invalid, but now you force folks to set start
and days
both simultaneously, you can't 'calculate it out', which seems like API so bad you wouldn't want such a thing).
It also suffers from the notion that days is now calculated every time instead of being 'cached'. You can solve that by writing your own cache e.g. with guava cachebuilder but that's quite a big bazooka to kill a mosquito. If this truly is the calculation you need, it's relatively cheap, I'd just write it like this and not worry about performance unless you are holding a profiler report that says this days calculation is the key culprit.
If this still isn't acceptable, then the thing you want just isn't what record
represents. You might as well ask how you represent arbitrary strings with an enum
. You just cannot do that - that is not what enums are about. Hence, you end up at:
import lombok.*;
@Value
@lombok.experimental.Accessors(fluent = true)
public class DatePair {
@With private final LocalDate start, end;
@With private final long days;
public DatePair(LocalDate start, LocalDate end) {
this.start = start;
this.end = end;
this.days = ChronoUnit.DAYS.between(start, end);
}
}
I strongly recommend you don't add the accessors line (this turns getStart()
into just start()
. Records notwithstanding, get
is just better (it plays far better with auto-complete which is ubiquitous in java editor environments, and is more common. In fact, the java core libs themselves do it 'right' and use get
, see e.g. java.time.LocalDate
). But, if you really really want it - that's how you do it. Or add a lombok.config
file and say there that you want fluent style accessor names.
Or let your IDE generate it all, which will be a ton of code you'll have to maintain. I understand the 'draw' of using records here, but records can't do lots of things. They can't extend anything either. They can't memoize calculated stuff by way of a field either.