Skip to content

Conversation

mes71
Copy link

@mes71 mes71 commented Aug 27, 2025

  • Introduced @copyWith annotation to generate copyWith(...) methods for immutable classes.
  • Implemented Javac handler to inject copyWith method at compile-time.
  • Implemented Eclipse handler to support Eclipse IDE.
  • Added unit tests covering:
    • basic field copying
    • nullable and final fields
    • chaining copyWith calls
    • nested objects
    • immutability and distinct instances
  • Updated META-INF/services to register new handlers.

This addition enables developers to create modified copies of objects easily while keeping original instances immutable, similar to Lombok's @with.

- Introduced @copyWith annotation to generate copyWith(...) methods for immutable classes.
- Implemented Javac handler to inject copyWith method at compile-time.
- Implemented Eclipse handler to support Eclipse IDE.
- Added unit tests covering:
    - basic field copying
    - nullable and final fields
    - chaining copyWith calls
    - nested objects
    - immutability and distinct instances
- Updated META-INF/services to register new handlers.

This addition enables developers to create modified copies of objects easily
while keeping original instances immutable, similar to Lombok's @with.
@rzwitserloot
Copy link
Collaborator

I'm trying to understand the point of this. The immediate concern I have is that it elevates magic values to special status: 0, null, false, and '\0' now mean: Copy the 'old' value and anything else means' this is the new value.

These magic values aren't editable, as far as I can gather. Even if they were, that complicates the feature a lot.

Java is going to release with syntax which would almost entirely obsolete this, assuming they do a good job on making deconstructors culturally backwards compatible (by no means a guarantee; cultural backwards compatibility ^1 does not appear to be a thing OpenJDK's main decision makers seem to care a lot about unfortunately).

I see no real reason why it couldn't be, so I'm assuming (hoping?) they will be, and these features therefore have a slightly higher bar to vault over to get into lombok: The pain they fix is a relatively short term pain.

What's this do that toBuilder doesn't solve in a much, much better way? This:

Bridge existingBridge = BridgeDb.get("Øresund Bridge");
Bridge plannedBridge = existingBridge.copyWith(null, null, 2030, 0, 0, false, 12495);

Seems like an unreadable, dodgy mess. Contrast with:

Bridge existingBridge = BridgeDb.get("Øresund Bridge");
Bridge plannedBridge = existingBridge.toBuilder()
  .buildYear(2030)
  .totalWidth(12495);
  .build();

Or the hypothetical future:

Bridge existingBridge = BridgeDb.get("Øresund Bridge");
Bridge plannedBridge = existingBridge with {
  buildYear = 2030;
  totalWidth = 12495;
};

If you asked to implement this feature and we didn't give you an answer, I apologise.


[1] 'cultural backwards compatibility' is a term I invented; it means: Existing libraries that tried to 'solve' the thing the language feature adds (therefore necessarily not very well; there's a reason we need the language feature after all!) either get the new stuff for free or can release a binary/source backwards compatible update that gives them the feature in a way that doesn't telegraph "this API is less good than it could have been; it clearly predates this language change". A nice example of culturally backwards compatible is Generics (ArrayList predates it; it was updated to have generics, that update was backwards compatible, and looking at the design of ArrayList today, there's nothing about it that makes you go: Ooof, dang, if only this was designed after generics, it'd be so much better! Maybe that remove takes Object instead of T, but that's a corner case, and debatable). Another even better example is lambdas; existing APIs 'mostly just got it for free'; any API that had an interface that so happened to have only one non-Object, non-defaulted method in it was a Functional Interface. An example of not-so culturally backwards compatible: records. get prefixes were a lot more popular on the whole than no get prefix. (example: LocalDate and the rest of java.time, for one). Whilst either adding the get prefix, or even more involved, having a method that allows records to decree the name of their accessors would be more complicated, it'd have made the feature culturally backwards compatible. Conjecture: records are, relatively speaking, a bust. Their adaption at least compared to lambdas and generics is minimal. Their failure to be culturally backwards compatible explains it. Existing libraries didn't shift their stuff over, which means records continue to feel 'weird and new', even years after their release.

@mes71 mes71 closed this Sep 23, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants