Randgalt / record-builder

Record builder generator for Java records
Apache License 2.0
756 stars 55 forks source link

Heavy copying of collections #114

Closed freelon closed 2 years ago

freelon commented 2 years ago

If using the useImmutableCollections and addSingleItemCollectionBuilders options the generated builder does copy collections on every change on the record. Example:

@RecordBuilderFull
public record Example(String name, List<Foo> foo) implements ExampleBuilder.With {}
...
Example e1 = ExampleBuilder.builder().name("a").foo( *some large collection*).build();
Example e2 = e1.with().name("b").build();
Example e3 = e2.with().name("c").build();

Whenever I do something like example.with().name("...").build() internally with() creates a new ExampleBuilder which instantiates a new ArrayList with all items from my old example object. Then calling build() creates a new immutable list of the array list. If I have a large collection and change other fields of my record frequently a lot (2 per record with-...-build-cycle) of list-copying is done in the background, although the list is never changed. So in the above example my large collection would already been copied six times without need.

I would prefer if the list was only copied into a new ArrayList if it actually changes. I think this can be done rather easily by using for instance a flag for every collection to indicate if the collection was modified. If a change is to be made and the flag is false, copy the current collection into a new ArrayList and then set the flag. After that, just add the collection item(s).

I'd be glad to create a PR if you're interested. Thanks for the great library so far :)

Randgalt commented 2 years ago

Hi - yes a PR would be appreciated.

Randgalt commented 2 years ago

Hi @freelon - I found a bug with the PR and reverted it. The problem is the determination if a collection/map has been changed to mutable is a check that it's an ArrayList or a HashMap, etc. This breaks if the source record's list, map is passed in. You can see this with this test:

    @Test
    void testSourceRecordNotModified() {
        List<Number> list = new ArrayList<>();
        Map<Number, FullRecord> map = new HashMap<>();
        FullRecord record = new FullRecord(list, map, "a");
        FullRecordBuilder builder = FullRecordBuilder.builder(record);
        builder.addNumbers(10);
        builder.addFullRecords(10, new FullRecord(List.of(), Map.of(), "b"));
        Assertions.assertTrue(list.isEmpty());
        Assertions.assertTrue(map.isEmpty());
    }

The solution is to add a boolean flag for each collection to test if it's been converted to modifiable. Or maybe some kind of wrapper - I'd have to think about it.