From 3731fed4cabed534c9e52e36f1d8f30c6ae25238 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Wed, 10 Dec 2025 13:27:32 +0100 Subject: [PATCH] Revise MultiValueMapCollector implementation and tests See https://github.com/spring-projects/spring-data-commons/issues/3420 Closes gh-35958 --- .../util/MultiValueMapCollector.java | 65 +++++++++++++++---- .../util/MultiValueMapCollectorTests.java | 34 +++++++--- 2 files changed, 80 insertions(+), 19 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/util/MultiValueMapCollector.java b/spring-core/src/main/java/org/springframework/util/MultiValueMapCollector.java index 83e0f46a62c..c02edbbb5db 100644 --- a/spring-core/src/main/java/org/springframework/util/MultiValueMapCollector.java +++ b/spring-core/src/main/java/org/springframework/util/MultiValueMapCollector.java @@ -17,7 +17,7 @@ package org.springframework.util; import java.util.EnumSet; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map.Entry; import java.util.Set; @@ -28,32 +28,74 @@ import java.util.function.Supplier; import java.util.stream.Collector; /** - * A {@link Collector} for building a {@link MultiValueMap} from a {@link java.util.stream.Stream}. - *
- * Moved from {@code org.springframework.data.util.MultiValueMapCollector}. + * A {@link Collector} for building a {@link MultiValueMap} from a + * {@link java.util.stream.Stream Stream}. * - * @author Jens Schauder + *

Copied from the Spring Data Commons project. * - * @param – the type of input elements to the reduction operation - * @param – the type of the key elements - * @param – the type of the value elements + * @author Jens Schauder + * @author Florian Hof + * @author Sam Brannen + * @since 7.0.2 + * @param the type of input elements to the reduction operation + * @param the key type + * @param the value element type */ -public class MultiValueMapCollector implements Collector, MultiValueMap> { +public final class MultiValueMapCollector implements Collector, MultiValueMap> { + private final Function keyFunction; + private final Function valueFunction; - public MultiValueMapCollector(Function keyFunction, Function valueFunction) { + + private MultiValueMapCollector(Function keyFunction, Function valueFunction) { this.keyFunction = keyFunction; this.valueFunction = valueFunction; } + + /** + * Create a new {@code MultiValueMapCollector} from the given key and value + * functions. + * @param the type of input elements to the reduction operation + * @param the key type + * @param the value element type + * @param keyFunction a {@code Function} which converts an element of type + * {@code T} to a key of type {@code K} + * @param valueFunction a {@code Function} which converts an element of type + * {@code T} to an element of type {@code V}; supply {@link Function#identity()} + * if no conversion should be performed + * @return a new {@code MultiValueMapCollector} + * @see #indexingBy(Function) + */ + public static MultiValueMapCollector of(Function keyFunction, Function valueFunction) { + return new MultiValueMapCollector<>(keyFunction, valueFunction); + } + + /** + * Create a new {@code MultiValueMapCollector} using the given {@code indexer}. + *

Delegates to {@link #of(Function, Function)}, supplying the given + * {@code indexer} as the key function and {@link Function#identity()} + * as the value function. + *

For example, if you would like to collect the elements of a {@code Stream} + * of strings into a {@link MultiValueMap} keyed by the lengths of the strings, + * you could create such a {@link Collector} via + * {@code MultiValueMapCollector.indexingBy(String::length)}. + * @param the key type + * @param the value element type + * @param indexer a {@code Function} which converts a value of type {@code V} + * to a key of type {@code K} + * @return a new {@code MultiValueMapCollector} based on an {@code indexer} + * @see #of(Function, Function) + */ public static MultiValueMapCollector indexingBy(Function indexer) { return new MultiValueMapCollector<>(indexer, Function.identity()); } + @Override public Supplier> supplier() { - return () -> CollectionUtils.toMultiValueMap(new HashMap>()); + return () -> CollectionUtils.toMultiValueMap(new LinkedHashMap>()); } @Override @@ -80,4 +122,5 @@ public class MultiValueMapCollector implements Collector characteristics() { return EnumSet.of(Characteristics.IDENTITY_FINISH, Characteristics.UNORDERED); } + } diff --git a/spring-core/src/test/java/org/springframework/util/MultiValueMapCollectorTests.java b/spring-core/src/test/java/org/springframework/util/MultiValueMapCollectorTests.java index de9c080fce8..589acc07186 100644 --- a/spring-core/src/test/java/org/springframework/util/MultiValueMapCollectorTests.java +++ b/spring-core/src/test/java/org/springframework/util/MultiValueMapCollectorTests.java @@ -16,9 +16,10 @@ package org.springframework.util; +import java.util.function.Function; import java.util.stream.Stream; -import org.junit.Test; +import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -26,16 +27,33 @@ import static org.assertj.core.api.Assertions.assertThat; * Tests for {@link MultiValueMapCollector}. * * @author Florian Hof + * @author Sam Brannen + * @since 7.0.2 */ class MultiValueMapCollectorTests { @Test - void indexingBy() { - MultiValueMapCollector collector = MultiValueMapCollector.indexingBy(String::length); - MultiValueMap content = Stream.of("abc", "ABC", "123", "1234", "abcdef", "ABCDEF").collect(collector); - assertThat(content.get(3)).containsOnly("abc", "ABC", "123"); - assertThat(content.get(4)).containsOnly("abcdef", "ABCDEF"); - assertThat(content.get(6)).containsOnly("1234"); - assertThat(content.get(1)).isNull(); + void ofFactoryMethod() { + Function keyFunction = i -> (i % 2 == 0 ? "even" :"odd"); + Function valueFunction = i -> -i; + + var collector = MultiValueMapCollector.of(keyFunction, valueFunction); + var multiValueMap = Stream.of(1, 2, 3, 4, 5).collect(collector); + + assertThat(multiValueMap).containsOnlyKeys("even", "odd"); + assertThat(multiValueMap.get("odd")).containsOnly(-1, -3, -5); + assertThat(multiValueMap.get("even")).containsOnly(-2, -4); } + + @Test + void indexingByFactoryMethod() { + var collector = MultiValueMapCollector.indexingBy(String::length); + var multiValueMap = Stream.of("abc", "ABC", "123", "1234", "cat", "abcdef", "ABCDEF").collect(collector); + + assertThat(multiValueMap).containsOnlyKeys(3, 4, 6); + assertThat(multiValueMap.get(3)).containsOnly("abc", "ABC", "123", "cat"); + assertThat(multiValueMap.get(4)).containsOnly("1234"); + assertThat(multiValueMap.get(6)).containsOnly("abcdef", "ABCDEF"); + } + }