From 84c5300087b931a045545c2f2fa83042fefef6c2 Mon Sep 17 00:00:00 2001 From: Johannes Schaefer Date: Sun, 22 May 2022 17:15:37 +0200 Subject: [PATCH] Performance feature: copy collections only when they were changed. Fixes #114. --- .../recordbuilder/core/RecordBuilder.java | 18 ++ .../processor/CollectionBuilderUtils.java | 177 +++++++++++++++--- .../InternalRecordBuilderProcessor.java | 67 ++++--- .../recordbuilder/test/CollectionCopying.java | 34 ++++ .../recordbuilder/test/TestCollections.java | 2 +- .../test/TestImmutableCollections.java | 148 +++++++++++++++ 6 files changed, 382 insertions(+), 64 deletions(-) create mode 100644 record-builder-test/src/main/java/io/soabase/recordbuilder/test/CollectionCopying.java create mode 100644 record-builder-test/src/test/java/io/soabase/recordbuilder/test/TestImmutableCollections.java diff --git a/record-builder-core/src/main/java/io/soabase/recordbuilder/core/RecordBuilder.java b/record-builder-core/src/main/java/io/soabase/recordbuilder/core/RecordBuilder.java index 952cf251..90757415 100644 --- a/record-builder-core/src/main/java/io/soabase/recordbuilder/core/RecordBuilder.java +++ b/record-builder-core/src/main/java/io/soabase/recordbuilder/core/RecordBuilder.java @@ -252,6 +252,24 @@ * without {@code new}. */ boolean addStaticBuilder() default true; + + /** + * If {@link #addSingleItemCollectionBuilders()} and {@link #useImmutableCollections()} are enabled the builder + * uses an internal class to track changes to lists. This is the name of that class. + */ + String mutableListClassName() default "_MutableList"; + + /** + * If {@link #addSingleItemCollectionBuilders()} and {@link #useImmutableCollections()} are enabled the builder + * uses an internal class to track changes to sets. This is the name of that class. + */ + String mutableSetClassName() default "_MutableSet"; + + /** + * If {@link #addSingleItemCollectionBuilders()} and {@link #useImmutableCollections()} are enabled the builder + * uses an internal class to track changes to maps. This is the name of that class. + */ + String mutableMapClassName() default "_MutableMap"; } @Retention(RetentionPolicy.CLASS) diff --git a/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/CollectionBuilderUtils.java b/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/CollectionBuilderUtils.java index 8bd28fd1..cc09178a 100644 --- a/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/CollectionBuilderUtils.java +++ b/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/CollectionBuilderUtils.java @@ -22,24 +22,38 @@ import java.util.*; import static io.soabase.recordbuilder.processor.RecordBuilderProcessor.generatedRecordBuilderAnnotation; +import static io.soabase.recordbuilder.processor.RecordBuilderProcessor.recordBuilderGeneratedAnnotation; class CollectionBuilderUtils { private final boolean useImmutableCollections; private final boolean addSingleItemCollectionBuilders; + private final boolean addClassRetainedGenerated; private final String listShimName; private final String mapShimName; private final String setShimName; private final String collectionShimName; + private final String listMakerMethodName; + private final String mapMakerMethodName; + private final String setMakerMethodName; + private boolean needsListShim; private boolean needsMapShim; private boolean needsSetShim; private boolean needsCollectionShim; - private static final TypeName listType = TypeName.get(List.class); - private static final TypeName mapType = TypeName.get(Map.class); - private static final TypeName setType = TypeName.get(Set.class); - private static final TypeName collectionType = TypeName.get(Collection.class); + private boolean needsListMutableMaker; + private boolean needsMapMutableMaker; + private boolean needsSetMutableMaker; + + private static final Class listType = List.class; + private static final Class mapType = Map.class; + private static final Class setType = Set.class; + private static final Class collectionType = Collection.class; + private static final TypeName listTypeName = TypeName.get(listType); + private static final TypeName mapTypeName = TypeName.get(mapType); + private static final TypeName setTypeName = TypeName.get(setType); + private static final TypeName collectionTypeName = TypeName.get(collectionType); private static final TypeVariableName tType = TypeVariableName.get("T"); private static final TypeVariableName kType = TypeVariableName.get("K"); @@ -49,14 +63,33 @@ class CollectionBuilderUtils { private static final ParameterizedTypeName parameterizedSetType = ParameterizedTypeName.get(ClassName.get(Set.class), tType); private static final ParameterizedTypeName parameterizedCollectionType = ParameterizedTypeName.get(ClassName.get(Collection.class), tType); + private static final Class mutableListType = ArrayList.class; + private static final Class mutableMapType = HashMap.class; + private static final Class mutableSetType = HashSet.class; + private static final ClassName mutableListTypeName = ClassName.get(mutableListType); + private static final ClassName mutableMapTypeName = ClassName.get(mutableMapType); + private static final ClassName mutableSetTypeName = ClassName.get(mutableSetType); + private final TypeSpec mutableListSpec; + private final TypeSpec mutableSetSpec; + private final TypeSpec mutableMapSpec; + CollectionBuilderUtils(List recordComponents, RecordBuilder.Options metaData) { useImmutableCollections = metaData.useImmutableCollections(); addSingleItemCollectionBuilders = metaData.addSingleItemCollectionBuilders(); + addClassRetainedGenerated = metaData.addClassRetainedGenerated(); - listShimName = adjustShimName(recordComponents, "__list", 0); - mapShimName = adjustShimName(recordComponents, "__map", 0); - setShimName = adjustShimName(recordComponents, "__set", 0); - collectionShimName = adjustShimName(recordComponents, "__collection", 0); + listShimName = disambiguateGeneratedMethodName(recordComponents, "__list", 0); + mapShimName = disambiguateGeneratedMethodName(recordComponents, "__map", 0); + setShimName = disambiguateGeneratedMethodName(recordComponents, "__set", 0); + collectionShimName = disambiguateGeneratedMethodName(recordComponents, "__collection", 0); + + listMakerMethodName = disambiguateGeneratedMethodName(recordComponents, "__ensureListMutable", 0); + setMakerMethodName = disambiguateGeneratedMethodName(recordComponents, "__ensureSetMutable", 0); + mapMakerMethodName = disambiguateGeneratedMethodName(recordComponents, "__ensureMapMutable", 0); + + mutableListSpec = buildMutableCollectionSubType(metaData.mutableListClassName(), mutableListTypeName, parameterizedListType, tType); + mutableSetSpec = buildMutableCollectionSubType(metaData.mutableSetClassName(), mutableSetTypeName, parameterizedSetType, tType); + mutableMapSpec = buildMutableCollectionSubType(metaData.mutableMapClassName(), mutableMapTypeName, parameterizedMapType, kType, vType); } enum SingleItemsMetaDataMode { @@ -65,7 +98,8 @@ enum SingleItemsMetaDataMode { EXCLUDE_WILDCARD_TYPES } - record SingleItemsMetaData(Class singleItemCollectionClass, List typeArguments, TypeName wildType) {} + record SingleItemsMetaData(Class singleItemCollectionClass, List typeArguments, TypeName wildType) { + } Optional singleItemsMetaData(RecordClassType component, SingleItemsMetaDataMode mode) { if (addSingleItemCollectionBuilders && (component.typeName() instanceof ParameterizedTypeName parameterizedTypeName)) { @@ -73,15 +107,15 @@ Optional singleItemsMetaData(RecordClassType component, Sin ClassName wildcardClass = null; int typeArgumentQty = 0; if (isList(component)) { - collectionClass = ArrayList.class; + collectionClass = mutableListType; wildcardClass = ClassName.get(Collection.class); typeArgumentQty = 1; } else if (isSet(component)) { - collectionClass = HashSet.class; + collectionClass = mutableSetType; wildcardClass = ClassName.get(Collection.class); typeArgumentQty = 1; } else if (isMap(component)) { - collectionClass = HashMap.class; + collectionClass = mutableMapType; wildcardClass = (ClassName) component.rawTypeName(); typeArgumentQty = 2; } @@ -110,33 +144,36 @@ Optional singleItemsMetaData(RecordClassType component, Sin } boolean isImmutableCollection(RecordClassType component) { - return useImmutableCollections && (isList(component) || isMap(component) || isSet(component) || component.rawTypeName().equals(collectionType)); + return useImmutableCollections && (isList(component) || isMap(component) || isSet(component) || component.rawTypeName().equals(collectionTypeName)); } boolean isList(RecordClassType component) { - return component.rawTypeName().equals(listType); + return component.rawTypeName().equals(listTypeName); } boolean isMap(RecordClassType component) { - return component.rawTypeName().equals(mapType); + return component.rawTypeName().equals(mapTypeName); } boolean isSet(RecordClassType component) { - return component.rawTypeName().equals(setType); + return component.rawTypeName().equals(setTypeName); } - void add(CodeBlock.Builder builder, RecordClassType component) { + void addShimCall(CodeBlock.Builder builder, RecordClassType component) { if (useImmutableCollections) { if (isList(component)) { needsListShim = true; + needsListMutableMaker = true; builder.add("$L($L)", listShimName, component.name()); } else if (isMap(component)) { needsMapShim = true; + needsMapMutableMaker = true; builder.add("$L($L)", mapShimName, component.name()); } else if (isSet(component)) { needsSetShim = true; + needsSetMutableMaker = true; builder.add("$L($L)", setShimName, component.name()); - } else if (component.rawTypeName().equals(collectionType)) { + } else if (component.rawTypeName().equals(collectionTypeName)) { needsCollectionShim = true; builder.add("$L($L)", collectionShimName, component.name()); } else { @@ -147,22 +184,67 @@ void add(CodeBlock.Builder builder, RecordClassType component) { } } + String shimName(RecordClassType component) { + if (isList(component)) { + return listShimName; + } else if (isMap(component)) { + return mapShimName; + } else if (isSet(component)) { + return setShimName; + } else if (component.rawTypeName().equals(collectionTypeName)) { + return collectionShimName; + } else { + throw new IllegalArgumentException(component + " is not a supported collection type"); + } + } + + String mutableMakerName(RecordClassType component) { + if (isList(component)) { + return listMakerMethodName; + } else if (isMap(component)) { + return mapMakerMethodName; + } else if (isSet(component)) { + return setMakerMethodName; + } else { + throw new IllegalArgumentException(component + " is not a supported collection type"); + } + } + void addShims(TypeSpec.Builder builder) { if (!useImmutableCollections) { return; } if (needsListShim) { - builder.addMethod(buildMethod(listShimName, listType, parameterizedListType, tType)); + builder.addMethod(buildShimMethod(listShimName, listTypeName, collectionType, parameterizedListType, tType)); } if (needsSetShim) { - builder.addMethod(buildMethod(setShimName, setType, parameterizedSetType, tType)); + builder.addMethod(buildShimMethod(setShimName, setTypeName, collectionType, parameterizedSetType, tType)); } if (needsMapShim) { - builder.addMethod(buildMethod(mapShimName, mapType, parameterizedMapType, kType, vType)); + builder.addMethod(buildShimMethod(mapShimName, mapTypeName, mapType, parameterizedMapType, kType, vType)); } if (needsCollectionShim) { - builder.addMethod(buildCollectionsMethod()); + builder.addMethod(buildCollectionsShimMethod()); + } + } + + void addMutableMakers(TypeSpec.Builder builder) { + if (!useImmutableCollections) { + return; + } + + if (needsListMutableMaker) { + builder.addMethod(buildMutableMakerMethod(listMakerMethodName, mutableListSpec.name, parameterizedListType, tType)); + builder.addType(mutableListSpec); + } + if (needsSetMutableMaker) { + builder.addMethod(buildMutableMakerMethod(setMakerMethodName, mutableSetSpec.name, parameterizedSetType, tType)); + builder.addType(mutableSetSpec); + } + if (needsMapMutableMaker) { + builder.addMethod(buildMutableMakerMethod(mapMakerMethodName, mutableMapSpec.name, parameterizedMapType, kType, vType)); + builder.addType(mutableMapSpec); } } @@ -187,34 +269,71 @@ private boolean hasWildcardTypeArguments(ParameterizedTypeName parameterizedType return false; } - private String adjustShimName(List recordComponents, String baseName, int index) { + private String disambiguateGeneratedMethodName(List recordComponents, String baseName, int index) { var name = (index == 0) ? baseName : (baseName + index); if (recordComponents.stream().anyMatch(component -> component.name().equals(name))) { - return adjustShimName(recordComponents, baseName, index + 1); + return disambiguateGeneratedMethodName(recordComponents, baseName, index + 1); } return name; } - private MethodSpec buildMethod(String name, TypeName mainType, ParameterizedTypeName parameterizedType, TypeVariableName... typeVariables) { + private MethodSpec buildShimMethod(String name, TypeName mainType, Class abstractType, ParameterizedTypeName parameterizedType, TypeVariableName... typeVariables) { var code = CodeBlock.of("return (o != null) ? $T.copyOf(o) : $T.of()", mainType, mainType); + TypeName[] wildCardTypeArguments = parameterizedType.typeArguments.stream().map(WildcardTypeName::subtypeOf).toList().toArray(new TypeName[0]); + var extendedParameterizedType = ParameterizedTypeName.get(ClassName.get(abstractType), wildCardTypeArguments); return MethodSpec.methodBuilder(name) .addAnnotation(generatedRecordBuilderAnnotation) .addModifiers(Modifier.PRIVATE, Modifier.STATIC) .addTypeVariables(Arrays.asList(typeVariables)) .returns(parameterizedType) - .addParameter(parameterizedType, "o") + .addParameter(extendedParameterizedType, "o") .addStatement(code) .build(); } - private MethodSpec buildCollectionsMethod() { + private MethodSpec buildMutableMakerMethod(String name, String mutableCollectionType, ParameterizedTypeName parameterizedType, TypeVariableName... typeVariables) { + var nullCase = CodeBlock.of("if (o == null) return new $L<>()", mutableCollectionType); + var isMutableCase = CodeBlock.of("if (o instanceof $L) return o", mutableCollectionType); + var defaultCase = CodeBlock.of("return new $L<>(o)", mutableCollectionType); + return MethodSpec.methodBuilder(name) + .addAnnotation(generatedRecordBuilderAnnotation) + .addModifiers(Modifier.PRIVATE, Modifier.STATIC) + .addTypeVariables(Arrays.asList(typeVariables)) + .returns(parameterizedType) + .addParameter(parameterizedType, "o") + .addStatement(nullCase) + .addStatement(isMutableCase) + .addStatement(defaultCase) + .build(); + } + + private TypeSpec buildMutableCollectionSubType(String className, ClassName mutableCollectionType, ParameterizedTypeName parameterizedType, TypeVariableName... typeVariables) { + TypeName[] typeArguments = new TypeName[]{}; + typeArguments = Arrays.stream(typeVariables).toList().toArray(typeArguments); + + TypeSpec.Builder builder = TypeSpec.classBuilder(className) + .addAnnotation(generatedRecordBuilderAnnotation) + .addModifiers(Modifier.PRIVATE, Modifier.STATIC) + .superclass(ParameterizedTypeName.get(mutableCollectionType, typeArguments)) + .addTypeVariables(Arrays.asList(typeVariables)) + .addMethod(MethodSpec.constructorBuilder().addAnnotation(generatedRecordBuilderAnnotation).addStatement("super()").build()) + .addMethod(MethodSpec.constructorBuilder().addAnnotation(generatedRecordBuilderAnnotation).addParameter(parameterizedType, "o").addStatement("super(o)").build()); + + if (addClassRetainedGenerated) { + builder.addAnnotation(recordBuilderGeneratedAnnotation); + } + + return builder.build(); + } + + private MethodSpec buildCollectionsShimMethod() { var code = CodeBlock.builder() .add("if (o instanceof Set) {\n") .indent() - .addStatement("return $T.copyOf(o)", setType) + .addStatement("return $T.copyOf(o)", setTypeName) .unindent() .addStatement("}") - .addStatement("return (o != null) ? $T.copyOf(o) : $T.of()", listType, listType) + .addStatement("return (o != null) ? $T.copyOf(o) : $T.of()", listTypeName, listTypeName) .build(); return MethodSpec.methodBuilder(collectionShimName) .addAnnotation(generatedRecordBuilderAnnotation) diff --git a/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/InternalRecordBuilderProcessor.java b/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/InternalRecordBuilderProcessor.java index 84102fdc..6bf79e29 100644 --- a/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/InternalRecordBuilderProcessor.java +++ b/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/InternalRecordBuilderProcessor.java @@ -16,7 +16,6 @@ package io.soabase.recordbuilder.processor; import static io.soabase.recordbuilder.processor.CollectionBuilderUtils.SingleItemsMetaDataMode.EXCLUDE_WILDCARD_TYPES; -import static io.soabase.recordbuilder.processor.CollectionBuilderUtils.SingleItemsMetaDataMode.STANDARD; import static io.soabase.recordbuilder.processor.CollectionBuilderUtils.SingleItemsMetaDataMode.STANDARD_FOR_SETTER; import static io.soabase.recordbuilder.processor.ElementUtils.getBuilderName; import static io.soabase.recordbuilder.processor.ElementUtils.getWithMethodName; @@ -112,6 +111,7 @@ class InternalRecordBuilderProcessor { collectionMetaData.ifPresent(meta -> add1CollectionBuilders(meta, component)); }); collectionBuilderUtils.addShims(builder); + collectionBuilderUtils.addMutableMakers(builder); builderType = builder.build(); } @@ -333,7 +333,7 @@ private void addComponentCallsAsArguments(int index, CodeBlock.Builder codeBlock } RecordClassType parameterComponent = recordComponents.get(parameterIndex); if (parameterIndex == index) { - collectionBuilderUtils.add(codeBlockBuilder, parameterComponent); + collectionBuilderUtils.addShimCall(codeBlockBuilder, parameterComponent); } else { codeBlockBuilder.add("$L()", prefixedName(parameterComponent, true)); } @@ -417,9 +417,7 @@ private MyRecordBuilder(int p1, T p2, ...) { .addAnnotation(generatedRecordBuilderAnnotation); recordComponents.forEach(component -> { constructorBuilder.addParameter(component.typeName(), component.name()); - var collectionMetaData = collectionBuilderUtils.singleItemsMetaData(component, STANDARD); - collectionMetaData.ifPresentOrElse(meta -> constructorBuilder.addStatement("this.$L = new $T<>($L)", component.name(), meta.singleItemCollectionClass(), component.name()), - () -> constructorBuilder.addStatement("this.$L = $L", component.name(), component.name())); + constructorBuilder.addStatement("this.$L = $L", component.name(), component.name()); }); builder.addMethod(constructorBuilder.build()); } @@ -551,7 +549,7 @@ private CodeBlock buildCodeBlock() { var recordComponent = recordComponents.get(index); if (collectionBuilderUtils.isImmutableCollection(recordComponent)) { codeBuilder.add("$[$L = ", recordComponent.name()); - collectionBuilderUtils.add(codeBuilder, recordComponents.get(index)); + collectionBuilderUtils.addShimCall(codeBuilder, recordComponents.get(index)); codeBuilder.add(";\n$]"); } }); @@ -819,34 +817,34 @@ private void add1MapBuilder(CollectionBuilderUtils.SingleItemsMetaData meta, Rec For a single map record component, add a methods similar to: public T addP(K key, V value) { - if (this.p == null) { - this.p = new HashMap<>(); - } + this.p = __ensureMapMutable(p); this.p.put(key, value); return this; } public T addP(Stream i) { - if (p == null) { - p = new HashMap<>(); - } + this.p = __ensureMapMutable(p); i.forEach(this.p::put); return this; } public T addP(Iterable i) { - if (p == null) { - p = new HashMap<>(); - } + this.p = __ensureMapMutable(p); i.forEach(this.p::put); return this; } */ for (var i = 0; i < 3; ++i) { - var codeBlockBuilder = CodeBlock.builder() - .beginControlFlow("if (this.$L == null)", component.name()) - .addStatement("this.$L = new $T<>()", component.name(), HashMap.class) - .endControlFlow(); + var codeBlockBuilder = CodeBlock.builder(); + if (collectionBuilderUtils.isImmutableCollection(component)) { + codeBlockBuilder + .addStatement("this.$L = $L($L)", component.name(), collectionBuilderUtils.mutableMakerName(component), component.name()); + } else { + codeBlockBuilder + .beginControlFlow("if (this.$L == null)", component.name()) + .addStatement("this.$L = new $T<>()", component.name(), meta.singleItemCollectionClass()) + .endControlFlow(); + } var methodSpecBuilder = MethodSpec.methodBuilder(metaData.singleItemBuilderPrefix() + capitalize(component.name())) .addJavadoc("Add to the internally allocated {@code HashMap} for {@code $L}\n", component.name()) .addModifiers(Modifier.PUBLIC) @@ -874,25 +872,19 @@ private void add1ListBuilder(CollectionBuilderUtils.SingleItemsMetaData meta, Re For a single list or set record component, add methods similar to: public T addP(I i) { - if (this.p == null) { - this.p = new ArrayList<>(); - } + this.list = __ensureListMutable(list); this.p.add(i); return this; } public T addP(Stream i) { - if (this.p == null) { - this.p = new ArrayList<>(); - } + this.list = __ensureListMutable(list); this.p.addAll(i); return this; } public T addP(Iterable i) { - if (this.p == null) { - this.p = new ArrayList<>(); - } + this.list = __ensureListMutable(list); this.p.addAll(i); return this; } @@ -908,10 +900,17 @@ public T addP(Iterable i) { var parameterClass = ClassName.get((i == 1) ? Stream.class : Iterable.class); parameter = ParameterizedTypeName.get(parameterClass, WildcardTypeName.subtypeOf(meta.typeArguments().get(0))); } - var codeBlockBuilder = CodeBlock.builder() - .beginControlFlow("if (this.$L == null)", component.name()) - .addStatement("this.$L = new $T<>()", component.name(), meta.singleItemCollectionClass()) - .endControlFlow() + var codeBlockBuilder = CodeBlock.builder(); + if (collectionBuilderUtils.isImmutableCollection(component)) { + codeBlockBuilder + .addStatement("this.$L = $L($L)", component.name(), collectionBuilderUtils.mutableMakerName(component), component.name()); + } else { + codeBlockBuilder + .beginControlFlow("if (this.$L == null)", component.name()) + .addStatement("this.$L = new $T<>()", component.name(), meta.singleItemCollectionClass()) + .endControlFlow(); + } + codeBlockBuilder .add(addClockBlock.build()) .addStatement("return this"); var methodSpecBuilder = MethodSpec.methodBuilder(metaData.singleItemBuilderPrefix() + capitalize(component.name())) @@ -960,8 +959,8 @@ public MyRecordBuilder p(T p) { var collectionMetaData = collectionBuilderUtils.singleItemsMetaData(component, STANDARD_FOR_SETTER); var parameterSpecBuilder = collectionMetaData.map(meta -> { CodeBlock.Builder codeSpec = CodeBlock.builder(); - codeSpec.addStatement("this.$L = ($L != null) ? new $T<>($L) : null", component.name(), component.name(), meta.singleItemCollectionClass(), component.name()); - methodSpec.addJavadoc("Re-create the internally allocated {@code $L} for {@code $L} by copying the argument\n", meta.singleItemCollectionClass().getSimpleName(), component.name()) + codeSpec.addStatement("this.$L = $L($L)", component.name(), collectionBuilderUtils.shimName(component), component.name()); + methodSpec.addJavadoc("Re-create the internally allocated {@code $T} for {@code $L} by copying the argument\n", component.typeName(), component.name()) .addCode(codeSpec.build()); return ParameterSpec.builder(meta.wildType(), component.name()); }).orElseGet(() -> { diff --git a/record-builder-test/src/main/java/io/soabase/recordbuilder/test/CollectionCopying.java b/record-builder-test/src/main/java/io/soabase/recordbuilder/test/CollectionCopying.java new file mode 100644 index 00000000..f4eb1832 --- /dev/null +++ b/record-builder-test/src/main/java/io/soabase/recordbuilder/test/CollectionCopying.java @@ -0,0 +1,34 @@ +/** + * Copyright 2019 Jordan Zimmerman + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.soabase.recordbuilder.test; + +import io.soabase.recordbuilder.core.RecordBuilder; + +import java.time.Instant; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +@RecordBuilder +@RecordBuilder.Options( + addSingleItemCollectionBuilders = true, + useImmutableCollections = true, + mutableListClassName = "PersonalizedMutableList" +) +public record CollectionCopying(List list, Set set, Map map, Collection collection, + int count) implements CollectionCopyingBuilder.With { +} diff --git a/record-builder-test/src/test/java/io/soabase/recordbuilder/test/TestCollections.java b/record-builder-test/src/test/java/io/soabase/recordbuilder/test/TestCollections.java index d9ed71f4..16eef65f 100644 --- a/record-builder-test/src/test/java/io/soabase/recordbuilder/test/TestCollections.java +++ b/record-builder-test/src/test/java/io/soabase/recordbuilder/test/TestCollections.java @@ -27,7 +27,7 @@ class TestCollections { @Test void testRecordBuilderOptionsCopied() { try { - assertNotNull(CollectionInterfaceRecordBuilder.class.getDeclaredMethod("__list", List.class)); + assertNotNull(CollectionInterfaceRecordBuilder.class.getDeclaredMethod("__list", Collection.class)); } catch (NoSuchMethodException e) { Assertions.fail(e); } diff --git a/record-builder-test/src/test/java/io/soabase/recordbuilder/test/TestImmutableCollections.java b/record-builder-test/src/test/java/io/soabase/recordbuilder/test/TestImmutableCollections.java new file mode 100644 index 00000000..fdc195dd --- /dev/null +++ b/record-builder-test/src/test/java/io/soabase/recordbuilder/test/TestImmutableCollections.java @@ -0,0 +1,148 @@ +/** + * Copyright 2019 Jordan Zimmerman + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.soabase.recordbuilder.test; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.*; + +public class TestImmutableCollections { + @Test + public void testImmutableListNotCopiedWhenNotChanged() { + var item = CollectionCopyingBuilder.builder() + .addList("a") + .addList("b") + .addList("c") + .build(); + Assertions.assertEquals(item.list(), List.of("a", "b", "c")); + + var oldList = item.list(); + + var copy = item.with() + .count(1) + .build(); + + Assertions.assertSame(oldList, copy.list()); + + var otherCopy = item.with() + .count(2) + .build(); + + Assertions.assertSame(oldList, otherCopy.list()); + } + + @Test + public void testImmutableSetNotCopiedWhenNotChanged() { + var item = CollectionCopyingBuilder.builder() + .addSet(Arrays.asList("1", "2", "3")) + .build(); + Assertions.assertEquals(item.set(), Set.of("1", "2", "3")); + + var oldSet = item.set(); + + var copy = item.with() + .count(1) + .build(); + + Assertions.assertSame(oldSet, copy.set()); + + var otherCopy = item.with() + .count(2) + .build(); + + Assertions.assertSame(oldSet, otherCopy.set()); + } + + @Test + public void testImmutableCollectionNotCopiedWhenNotChanged() { + var item = CollectionCopyingBuilder.builder() + .collection(List.of("foo", "bar", "baz")) + .build(); + Assertions.assertEquals(item.collection(), List.of("foo", "bar", "baz")); + + var oldCollection = item.collection(); + + var copy = item.with() + .count(1) + .build(); + + Assertions.assertSame(oldCollection, copy.collection()); + + var otherCopy = item.with() + .count(2) + .build(); + + Assertions.assertSame(oldCollection, otherCopy.collection()); + } + + @Test + public void testImmutableMapNotCopiedWhenNotChanged() { + var item = CollectionCopyingBuilder.builder() + .addMap(Instant.MAX, "future") + .addMap(Instant.MIN, "before") + .build(); + Assertions.assertEquals(item.map(), Map.of(Instant.MAX, "future", Instant.MIN, "before")); + + var oldMap = item.map(); + + var copy = item.with() + .count(1) + .build(); + + Assertions.assertSame(oldMap, copy.map()); + + var otherCopy = item.with() + .count(2) + .build(); + + Assertions.assertSame(oldMap, otherCopy.map()); + } + + @Test + void testSourceListNotModified() { + var item = new CollectionCopying<>(new ArrayList<>(), null, null, null, 0); + var modifiedItem = CollectionCopyingBuilder.builder(item) + .addList("a") + .build(); + + Assertions.assertEquals(modifiedItem.list(), List.of("a")); + Assertions.assertTrue(item.list().isEmpty()); + } + + @Test + void testSourceSetNotModified() { + var item = new CollectionCopying<>(null, new HashSet<>(), null, null, 0); + var modifiedItem = CollectionCopyingBuilder.builder(item) + .addSet("a") + .build(); + + Assertions.assertEquals(modifiedItem.set(), Set.of("a")); + Assertions.assertTrue(item.set().isEmpty()); + } + + @Test + void testSourceMapNotModified() { + var item = new CollectionCopying<>(null, null, new HashMap<>(), null, 0); + var modifiedItem = CollectionCopyingBuilder.builder(item) + .addMap(Instant.MIN, "a") + .build(); + + Assertions.assertEquals(modifiedItem.map(), Map.of(Instant.MIN, "a")); + Assertions.assertTrue(item.map().isEmpty()); + } +}