Avoid redundant object construction in DataBinder.createMap()

Previously, createMap() invoked createIndexedValue() – and therefore
createObject() for non-simple value types – once per matching parameter
name rather than once per distinct map key, causing redundant nested
object construction for map entries whose value type has multiple
constructor parameters.

To address that, this commit aligns createMap() with createList() and
createArray() by skipping construction for keys that have already been
resolved.

Closes gh-37019
This commit is contained in:
Sam Brannen
2026-08-26 13:34:57 +02:00
parent b28569119f
commit a00fb1b5ae
2 changed files with 39 additions and 0 deletions
@@ -1069,6 +1069,9 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
if (map == null) {
map = CollectionFactory.createMap(paramType, 16);
}
else if (map.containsKey(key)) {
continue;
}
String indexedPath = name.substring(0, endIdx + 1);
map.put(key, createIndexedValue(paramPath, paramType, elementType, indexedPath, valueResolver));
@@ -21,6 +21,7 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import jakarta.validation.constraints.NotNull;
import org.jspecify.annotations.Nullable;
@@ -158,6 +159,27 @@ class DataBinderConstructTests {
assertThat(map.get("c").param1()).isEqualTo("value3");
}
@Test // gh-37019
void dataClassWithMapBindingConstructsValueOncePerKey() {
CountingRecord.constructorCallCount.set(0);
MapValueResolver valueResolver = new MapValueResolver(Map.of(
"countingMap[a].param1", "value1", "countingMap[a].param2", "value2",
"countingMap[b].param1", "value3", "countingMap[b].param2", "value4"));
DataBinder binder = initDataBinder(CountingMapRecord.class);
binder.construct(valueResolver);
CountingMapRecord target = getTarget(binder);
Map<String, CountingRecord> map = target.countingMap();
assertThat(map).hasSize(2);
assertThat(map.get("a").param1()).isEqualTo("value1");
assertThat(map.get("a").param2()).isEqualTo("value2");
assertThat(map.get("b").param1()).isEqualTo("value3");
assertThat(map.get("b").param2()).isEqualTo("value4");
assertThat(CountingRecord.constructorCallCount).hasValue(2);
}
@Test
void dataClassWithArrayBinding() {
MapValueResolver valueResolver = new MapValueResolver(Map.of(
@@ -327,6 +349,20 @@ class DataBinderConstructTests {
}
record CountingRecord(String param1, String param2) {
static final AtomicInteger constructorCallCount = new AtomicInteger();
CountingRecord {
constructorCallCount.incrementAndGet();
}
}
private record CountingMapRecord(Map<String, CountingRecord> countingMap) {
}
private record DataClassArrayRecord(DataClass[] dataClassArray) {
}