Skip to content

Convert immutable classes to Java records - #88

Open
Knud-Aage wants to merge 14 commits into
masterfrom
BITMAG-1256-use-records-in-java
Open

Convert immutable classes to Java records#88
Knud-Aage wants to merge 14 commits into
masterfrom
BITMAG-1256-use-records-in-java

Conversation

@Knud-Aage

Copy link
Copy Markdown
Contributor

Converted 10 immutable classes to Java
Deleted equals(), hashCode(), and toString() implementations where Java's auto-generated record versions are equivalent
Updated all call sites to use record accessor names
Added dedicated unit tests for record-specific behaviour: compact constructor validation, non-canonical constructor null-coalescing, custom toString format, and component-based equality

@Knud-Aage
Knud-Aage requested review from Thommelise and ole-v-v July 7, 2026 10:30
…Metric, JobID and SelectedComponentInfo

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@ole-v-v ole-v-v left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love how much terser the class declarations have become.

We have lost some Javadoc comments, many of which I tend to find useful and helpful. Please see if you can fit the information into new Javadoc comments on the record classes except where self-evident and/or only written for the sake of there being Javadoc. Also repeated information is often needed only once.

You have made sure to retain some getters with name starting with get (for example SelectedComponentInfo#getID). Fine.
And I am sorry, we need these in all cases. This is for backward compatibility.
I suggest:

  1. In our own code use the new getters as you are generally doing already.
    For example in GeneralConversationState#start you call delay.count() instead of delay.getCount().
    Also in the last line of the GettingFile constructor I prefer using pillar.componentID() over pillar.getID().
  2. Insert (reinsert) getXx() methods in all new record classes,
    and at the same time deprecate them for removal.

Nice work. This will ease reading our code in the future.

*/
package org.bitrepository.client.conversation.selector;

import org.jetbrains.annotations.NotNull;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m in favour of not using Jetbrains-specific annotations. Also when we already use them in a few places (probably by accident).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed it to jspecify

@Override
public String toString() {
return getClass().getSimpleName() + ": componentID=" + componentID + ", componentTopic=" + componentTopic;
public @NotNull String toString() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Annotation/tag is not necessary.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed toString

@Override
public String toString() {
return getClass().getSimpleName() + ": componentID=" + componentID + ", componentTopic=" + componentTopic;
public @NotNull String toString() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not perfectly sure whether we want the explicit toString() method at all. The default one yields somthing like SelectedComponentInfo[componentID=some-id, componentTopic=some-topic]. The difference is basically the colon vs. the square brackets. I tend to think it’s close enough that we don’t want to maintain our own implementation any more?

To me it counts that the toString() method never was documented to yields a specific format and that parsing the result from toString() is always discouraged and in this case unnecessary since we got the getters. toString() is there for readable log and trace output, and the default implementation serves that purpose just fine.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed toString

* @param outputHandler The {@link OutputHandler} for handling outputting results
*/
public CompleteEventAwaiter(Settings settings, OutputHandler outputHandler) {
protected CompleteEventAwaiter(Settings settings, OutputHandler outputHandler) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the class is abstract, I guess it’s OK to declare the constructor protected.

/**
* @return The ID of the pillar chosen by this selector if finished. If unfinished null is returned
*/
public String getID() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, unfortunately we need to keep the getters named get-something for backward compatibility (we might have deprecated them, even for removal).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done it for every changed class to record

IntegrityModel store,
IntegrityAlerter alerter,
AuditTrailManager auditManager
) implements WorkflowContext {}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree to leave out the toString() method. Unfortunately we need the old get methods with names starting with get (may deprecate for removal).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reinserted getters for for every changed class

protected final String fileID;
protected final String checksum;
protected final Instant calculationDate;
public record ChecksumEntry(String fileID, String checksum, Instant calculationInstant) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice rename, thanks.

Assertions.assertEquals(CE_DATE, ce.getCalculationInstant());
}

@Test

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It’s good to think of new tests.

@Tag("pillartest")
void compactConstructorRejectsNullFileID() {
addDescription("The compact constructor must reject a null fileID");
Assertions.assertThrows(Exception.class, () -> new ChecksumEntry(null, CE_CHECKSUM, CE_DATE));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the constructor is not declared to throw any checked exception, I would prefer to strengthen the assertion to require a RuntimeException (may even require that it is either NullPointerException or IllegalArgumentException, but this would require more code lines and probably isn’t worth it).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Introduced IllegalArgumentException assertions.assertThrow

@Test
@Tag("regressiontest")
@Tag("pillartest")
void equalityIsComponentBased() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tend to think of this test as unneeded and not really one that we want. Does it test anything else than the default, auto-generated equals() and hashCode() methods of a record behave correctly? It’s not our job to test the Java language.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed it

* The topic for communication with the selected pillar
*/
protected final String componentTopic;
public record SelectedComponentInfo(@NonNull String componentID, @NonNull String componentTopic) {

@ole-v-v ole-v-v Aug 10, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wish I knew whether componentTopic was the right name for this field.

  • The name used in some places in the old class and agrees with some of the Javadoc comments.
  • On the other hand ComponentSelector feeds a “reply to” value into the field, and GettingFile uses the value as a destination. Also the name of the getter getDestination() supports this interpretation.

Let it be or add the alternative interpretation to the Javadoc comment, for example “The topic or destination for comm...”.

Comment on lines +56 to +57
Assertions.assertThrows(IllegalArgumentException.class, () -> new ChecksumEntry(null, CE_CHECKSUM, CE_DATE));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn’t being clear. The current implementation throws IllegalArgumentException, not documented. I would find an implementation throwing NullPointerException in this place perfectly fine too. For example if we replace the use of out own ArgumentValidator with Java’s Objects.requireNonNull(). So IMHO the test should accept either.

Easy fix:

        Assertions.assertThrows(RuntimeException.class, () -> new ChecksumEntry(null, CE_CHECKSUM, CE_DATE));

(since both ``IllegalArgumentExceptionandNullPointerException` are subclasses of `RuntimeException`).

Gold-plated test, maybe a bit too complicated and/or long-winded:

        RuntimeException exception = Assertions.assertThrows(
                RuntimeException.class, () -> new ChecksumEntry(null, CE_CHECKSUM, CE_DATE));
        Assertions.assertTrue(exception instanceof NullPointerException || exception instanceof IllegalArgumentException,
                "Expected: NullPointerException or IllegalArgumentException. Actual: " + exception);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed for both of the methods that has IllegalArgumentException in them

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don’t need to change compactConstructorRejectsEmptyFileID(). For an empty string IllegalArgumentException is the right exception type. NullPointerException would not make sense.

}

@Test
void equalityIsComponentBased() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest leaving out.

Comment on lines +67 to +70
RuntimeException exception = Assertions.assertThrows(
RuntimeException.class, () -> new ChecksumEntry("", CE_CHECKSUM, CE_DATE));
Assertions.assertTrue(exception instanceof NullPointerException || exception instanceof IllegalArgumentException,
"Expected: NullPointerException or IllegalArgumentException. Actual: " + exception);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I said elsewhere, the change in this method is not needed. We prefer it as it was before. It was simpler and a more precise test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed it back to the former

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