From: Sébastien Lesaint Date: Wed, 20 Jul 2016 12:34:25 +0000 (+0200) Subject: add index and uniqueIndex to Collectors X-Git-Tag: 6.1-RC1~529 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=8a59810c57e65e6dbbe8245dfcbce25e96cd4dc1;p=sonarqube.git add index and uniqueIndex to Collectors --- diff --git a/sonar-core/src/main/java/org/sonar/core/util/stream/Collectors.java b/sonar-core/src/main/java/org/sonar/core/util/stream/Collectors.java index e13d3c14271..a1f57685262 100644 --- a/sonar-core/src/main/java/org/sonar/core/util/stream/Collectors.java +++ b/sonar-core/src/main/java/org/sonar/core/util/stream/Collectors.java @@ -20,15 +20,27 @@ package org.sonar.core.util.stream; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableListMultimap; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.BinaryOperator; +import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collector; +import static java.util.Objects.requireNonNull; + public final class Collectors { + + private static final int DEFAULT_HASHMAP_CAPACITY = 0; + private Collectors() { // prevents instantiation } @@ -49,6 +61,9 @@ public final class Collectors { /** * A Collector into an {@link ImmutableList} of the specified expected size. + * + *

Note: using this method with a parallel stream will likely not have the expected memory usage benefit as all + * processing threads will use a List with a capacity large enough for the final size.

*/ public static Collector, List> toList(int expectedSize) { // use ArrayList rather than ImmutableList.Builder because initial capacity of builder can not be specified @@ -78,6 +93,9 @@ public final class Collectors { /** * A Collector into an {@link ImmutableSet} of the specified expected size. + * + *

Note: using this method with a parallel stream will likely not have the expected memory usage benefit as all + * processing threads will use a Set with a capacity large enough for the final size.

*/ public static Collector, Set> toSet(int expectedSize) { // use HashSet rather than ImmutableSet.Builder because initial capacity of builder can not be specified @@ -102,6 +120,9 @@ public final class Collectors { * Does {@code java.util.stream.Collectors.toCollection(() -> new ArrayList<>(size));} which is equivalent to * {@link #toArrayList()} but avoiding array copies when the size of the resulting list is already known. * + *

Note: using this method with a parallel stream will likely not have the expected memory usage benefit as all + * processing threads will use a ArrayList with a capacity large enough for the final size.

+ * * @see java.util.stream.Collectors#toList() * @see java.util.stream.Collectors#toCollection(Supplier) */ @@ -120,10 +141,185 @@ public final class Collectors { * Does {@code java.util.stream.Collectors.toCollection(() -> new HashSet<>(size));} which is equivalent to * {@link #toHashSet()} but avoiding array copies when the size of the resulting set is already known. * + *

Note: using this method with a parallel stream will likely not have the expected memory usage benefit as all + * processing threads will use a HashSet with a capacity large enough for the final size.

+ * * @see java.util.stream.Collectors#toSet() * @see java.util.stream.Collectors#toCollection(Supplier) */ public static Collector> toHashSet(int size) { return java.util.stream.Collectors.toCollection(() -> new HashSet<>(size)); } + + /** + * Creates an {@link ImmutableMap} from the stream where the values are the values in the stream and the keys are the + * result of the provided {@link Function keyFunction} applied to each value in the stream. + * + *

+ * The {@link Function keyFunction} must return a unique (according to the key's type {@link Object#equals(Object)} + * and/or {@link Comparable#compareTo(Object)} implementations) value for each of them, otherwise a + * {@link IllegalArgumentException} will be thrown. + *

+ * + *

+ * {@link Function keyFunction} can't return {@code null}, otherwise a {@link NullPointerException} will be thrown. + *

+ * + * @throws NullPointerException if {@code keyFunction} is {@code null}. + * @throws NullPointerException if result of {@code keyFunction} is {@code null}. + * @throws IllegalArgumentException if {@code keyFunction} returns the same value for multiple entries in the stream. + */ + public static Collector, ImmutableMap> uniqueIndex(Function keyFunction) { + return uniqueIndex(keyFunction, Function.identity()); + } + + /** + * Same as {@link #uniqueIndex(Function)} but using an underlying {@link Map} initialized with a capacity for the + * specified expected size. + * + *

Note: using this method with a parallel stream will likely not have the expected memory usage benefit as all + * processing threads will use a Map with a capacity large enough for the final size.

+ * + *

+ * {@link Function keyFunction} can't return {@code null}, otherwise a {@link NullPointerException} will be thrown. + *

+ * + * @throws NullPointerException if {@code keyFunction} is {@code null}. + * @throws NullPointerException if result of {@code keyFunction} is {@code null}. + * @throws IllegalArgumentException if {@code keyFunction} returns the same value for multiple entries in the stream. + * @see #uniqueIndex(Function) + */ + public static Collector, ImmutableMap> uniqueIndex(Function keyFunction, int expectedSize) { + return uniqueIndex(keyFunction, Function.identity(), expectedSize); + } + + /** + * Creates an {@link ImmutableMap} from the stream where the values are the result of {@link Function valueFunction} + * applied to the values in the stream and the keys are the result of the provided {@link Function keyFunction} + * applied to each value in the stream. + * + *

+ * The {@link Function keyFunction} must return a unique (according to the key's type {@link Object#equals(Object)} + * and/or {@link Comparable#compareTo(Object)} implementations) value for each of them, otherwise a + * {@link IllegalArgumentException} will be thrown. + *

+ * + *

+ * Neither {@link Function keyFunction} nor {@link Function valueFunction} can return {@code null}, otherwise a + * {@link NullPointerException} will be thrown. + *

+ * + * @throws NullPointerException if {@code keyFunction} or {@code valueFunction} is {@code null}. + * @throws NullPointerException if result of {@code keyFunction} or {@code valueFunction} is {@code null}. + * @throws IllegalArgumentException if {@code keyFunction} returns the same value for multiple entries in the stream. + */ + public static Collector, ImmutableMap> uniqueIndex(Function keyFunction, + Function valueFunction) { + return uniqueIndex(keyFunction, valueFunction, DEFAULT_HASHMAP_CAPACITY); + } + + /** + * Same as {@link #uniqueIndex(Function, Function)} but using an underlying {@link Map} initialized with a capacity + * for the specified expected size. + * + *

Note: using this method with a parallel stream will likely not have the expected memory usage benefit as all + * processing threads will use a Map with a capacity large enough for the final size.

+ * + *

+ * Neither {@link Function keyFunction} nor {@link Function valueFunction} can return {@code null}, otherwise a + * {@link NullPointerException} will be thrown. + *

+ * + * @throws NullPointerException if {@code keyFunction} or {@code valueFunction} is {@code null}. + * @throws NullPointerException if result of {@code keyFunction} or {@code valueFunction} is {@code null}. + * @throws IllegalArgumentException if {@code keyFunction} returns the same value for multiple entries in the stream. + * @see #uniqueIndex(Function, Function) + */ + public static Collector, ImmutableMap> uniqueIndex(Function keyFunction, + Function valueFunction, int expectedSize) { + requireNonNull(keyFunction, "Key function can't be null"); + requireNonNull(valueFunction, "Value function can't be null"); + BiConsumer, E> accumulator = (map, element) -> { + K key = requireNonNull(keyFunction.apply(element), "Key function can't return null"); + V value = requireNonNull(valueFunction.apply(element), "Value function can't return null"); + + putAndFailOnDuplicateKey(map, key, value); + }; + BinaryOperator> merger = (m1, m2) -> { + for (Map.Entry entry : m2.entrySet()) { + putAndFailOnDuplicateKey(m1, entry.getKey(), entry.getValue()); + } + return m1; + }; + return Collector.of( + newHashMapSupplier(expectedSize), + accumulator, + merger, + ImmutableMap::copyOf, + Collector.Characteristics.UNORDERED); + } + + private static Supplier> newHashMapSupplier(int expectedSize) { + return () -> expectedSize == DEFAULT_HASHMAP_CAPACITY ? new HashMap<>() : new HashMap<>(expectedSize); + } + + private static void putAndFailOnDuplicateKey(Map map, K key, V value) { + V existingValue = map.put(key, value); + if (existingValue != null) { + throw new IllegalArgumentException(String.format("Duplicate key %s", key)); + } + } + + /** + * Creates an {@link com.google.common.collect.ImmutableListMultimap} from the stream where the values are the values + * in the stream and the keys are the result of the provided {@link Function keyFunction} applied to each value in the + * stream. + * + *

+ * Neither {@link Function keyFunction} nor {@link Function valueFunction} can return {@code null}, otherwise a + * {@link NullPointerException} will be thrown. + *

+ * + * @throws NullPointerException if {@code keyFunction} or {@code valueFunction} is {@code null}. + * @throws NullPointerException if result of {@code keyFunction} or {@code valueFunction} is {@code null}. + */ + public static Collector, ImmutableListMultimap> index(Function keyFunction) { + return index(keyFunction, Function.identity()); + } + + /** + * Creates an {@link com.google.common.collect.ImmutableListMultimap} from the stream where the values are the result + * of {@link Function valueFunction} applied to the values in the stream and the keys are the result of the provided + * {@link Function keyFunction} applied to each value in the stream. + * + *

+ * Neither {@link Function keyFunction} nor {@link Function valueFunction} can return {@code null}, otherwise a + * {@link NullPointerException} will be thrown. + *

+ * + * @throws NullPointerException if {@code keyFunction} or {@code valueFunction} is {@code null}. + * @throws NullPointerException if result of {@code keyFunction} or {@code valueFunction} is {@code null}. + */ + public static Collector, ImmutableListMultimap> index(Function keyFunction, + Function valueFunction) { + requireNonNull(keyFunction, "Key function can't be null"); + requireNonNull(valueFunction, "Value function can't be null"); + BiConsumer, E> accumulator = (map, element) -> { + K key = requireNonNull(keyFunction.apply(element), "Key function can't return null"); + V value = requireNonNull(valueFunction.apply(element), "Value function can't return null"); + + map.put(key, value); + }; + BinaryOperator> merger = (m1, m2) -> { + for (Map.Entry entry : m2.build().entries()) { + m1.put(entry.getKey(), entry.getValue()); + } + return m1; + }; + return Collector.of( + ImmutableListMultimap::builder, + accumulator, + merger, + ImmutableListMultimap.Builder::build); + } } diff --git a/sonar-core/src/test/java/org/sonar/core/util/stream/CollectorsTest.java b/sonar-core/src/test/java/org/sonar/core/util/stream/CollectorsTest.java index a8c02735f51..9bac9ee7d8f 100644 --- a/sonar-core/src/test/java/org/sonar/core/util/stream/CollectorsTest.java +++ b/sonar-core/src/test/java/org/sonar/core/util/stream/CollectorsTest.java @@ -21,69 +21,366 @@ package org.sonar.core.util.stream; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Multimap; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.stream.Stream; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; +import static org.assertj.guava.api.Assertions.assertThat; +import static org.sonar.core.util.stream.Collectors.index; +import static org.sonar.core.util.stream.Collectors.toArrayList; +import static org.sonar.core.util.stream.Collectors.toHashSet; +import static org.sonar.core.util.stream.Collectors.toList; +import static org.sonar.core.util.stream.Collectors.toSet; +import static org.sonar.core.util.stream.Collectors.uniqueIndex; public class CollectorsTest { + + private static final MyObj MY_OBJ_1_A = new MyObj(1, "A"); + private static final MyObj MY_OBJ_1_C = new MyObj(1, "C"); + private static final MyObj MY_OBJ_2_B = new MyObj(2, "B"); + private static final MyObj MY_OBJ_3_C = new MyObj(3, "C"); + private static final List SINGLE_ELEMENT_LIST = Arrays.asList(MY_OBJ_1_A); + private static final List LIST_WITH_DUPLICATE_ID = Arrays.asList(MY_OBJ_1_A, MY_OBJ_2_B, MY_OBJ_1_C); + private static final List LIST = Arrays.asList(MY_OBJ_1_A, MY_OBJ_2_B, MY_OBJ_3_C); + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + @Test public void toList_builds_an_ImmutableList() { - List res = Arrays.asList(1, 2, 3, 4, 5).stream().collect(Collectors.toList()); + List res = Arrays.asList(1, 2, 3, 4, 5).stream().collect(toList()); assertThat(res).isInstanceOf(ImmutableList.class) .containsExactly(1, 2, 3, 4, 5); } @Test public void toList_with_size_builds_an_ImmutableList() { - List res = Arrays.asList(1, 2, 3, 4, 5).stream().collect(Collectors.toList(30)); + List res = Arrays.asList(1, 2, 3, 4, 5).stream().collect(toList(30)); assertThat(res).isInstanceOf(ImmutableList.class) .containsExactly(1, 2, 3, 4, 5); } @Test public void toSet_builds_an_ImmutableSet() { - Set res = Arrays.asList(1, 2, 3, 4, 5).stream().collect(Collectors.toSet()); + Set res = Arrays.asList(1, 2, 3, 4, 5).stream().collect(toSet()); assertThat(res).isInstanceOf(ImmutableSet.class) .containsExactly(1, 2, 3, 4, 5); } @Test public void toSet_with_size_builds_an_ImmutableSet() { - Set res = Arrays.asList(1, 2, 3, 4, 5).stream().collect(Collectors.toSet(30)); + Set res = Arrays.asList(1, 2, 3, 4, 5).stream().collect(toSet(30)); assertThat(res).isInstanceOf(ImmutableSet.class) .containsExactly(1, 2, 3, 4, 5); } @Test public void toArrayList_builds_an_ArrayList() { - List res = Arrays.asList(1, 2, 3, 4, 5).stream().collect(Collectors.toArrayList()); + List res = Arrays.asList(1, 2, 3, 4, 5).stream().collect(toArrayList()); assertThat(res).isInstanceOf(ArrayList.class) .containsExactly(1, 2, 3, 4, 5); } @Test public void toArrayList_with_size_builds_an_ArrayList() { - List res = Arrays.asList(1, 2, 3, 4, 5).stream().collect(Collectors.toArrayList(30)); + List res = Arrays.asList(1, 2, 3, 4, 5).stream().collect(toArrayList(30)); assertThat(res).isInstanceOf(ArrayList.class) .containsExactly(1, 2, 3, 4, 5); } @Test public void toHashSet_builds_an_HashSet() { - Set res = Arrays.asList(1, 2, 3, 4, 5).stream().collect(Collectors.toHashSet()); + Set res = Arrays.asList(1, 2, 3, 4, 5).stream().collect(toHashSet()); assertThat(res).isInstanceOf(HashSet.class) .containsExactly(1, 2, 3, 4, 5); } @Test public void toHashSet_with_size_builds_an_ArrayList() { - Set res = Arrays.asList(1, 2, 3, 4, 5).stream().collect(Collectors.toHashSet(30)); + Set res = Arrays.asList(1, 2, 3, 4, 5).stream().collect(toHashSet(30)); assertThat(res).isInstanceOf(HashSet.class) .containsExactly(1, 2, 3, 4, 5); } + + @Test + public void uniqueIndex_empty_stream_returns_empty_map() { + assertThat(Collections.emptyList().stream().collect(uniqueIndex(MyObj::getId))).isEmpty(); + assertThat(Collections.emptyList().stream().collect(uniqueIndex(MyObj::getId, 6))).isEmpty(); + assertThat(Collections.emptyList().stream().collect(uniqueIndex(MyObj::getId, MyObj::getText))).isEmpty(); + assertThat(Collections.emptyList().stream().collect(uniqueIndex(MyObj::getId, MyObj::getText, 10))).isEmpty(); + } + + @Test + public void uniqueIndex_fails_when_there_is_duplicate_keys() { + Stream stream = LIST_WITH_DUPLICATE_ID.stream(); + + expectedDuplicateKey1IAE(); + + stream.collect(uniqueIndex(MyObj::getId)); + } + + @Test + public void uniqueIndex_with_expected_size_fails_when_there_is_duplicate_keys() { + Stream stream = LIST_WITH_DUPLICATE_ID.stream(); + + expectedDuplicateKey1IAE(); + + stream.collect(uniqueIndex(MyObj::getId, 1)); + } + + @Test + public void uniqueIndex_with_valueFunction_fails_when_there_is_duplicate_keys() { + Stream stream = LIST_WITH_DUPLICATE_ID.stream(); + + expectedDuplicateKey1IAE(); + + stream.collect(uniqueIndex(MyObj::getId, MyObj::getText)); + } + + @Test + public void uniqueIndex_with_valueFunction_and_expected_size_fails_when_there_is_duplicate_keys() { + Stream stream = LIST_WITH_DUPLICATE_ID.stream(); + + expectedDuplicateKey1IAE(); + + stream.collect(uniqueIndex(MyObj::getId, MyObj::getText, 10)); + } + + @Test + public void uniqueIndex_fails_if_key_function_is_null() { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("Key function can't be null"); + + uniqueIndex(null); + } + + @Test + public void uniqueIndex_with_expected_size_fails_if_key_function_is_null() { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("Key function can't be null"); + + uniqueIndex(null, 2); + } + + @Test + public void uniqueIndex_with_valueFunction_fails_if_key_function_is_null() { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("Key function can't be null"); + + uniqueIndex(null, MyObj::getText); + } + + @Test + public void uniqueIndex_with_valueFunction_and_expected_size_fails_if_key_function_is_null() { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("Key function can't be null"); + + uniqueIndex(null, MyObj::getText, 9); + } + + @Test + public void uniqueIndex_with_valueFunction_fails_if_value_function_is_null() { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("Value function can't be null"); + + uniqueIndex(MyObj::getId, null); + } + + @Test + public void uniqueIndex_with_valueFunction_and_expected_size_fails_if_value_function_is_null() { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("Value function can't be null"); + + uniqueIndex(MyObj::getId, null, 9); + } + + @Test + public void uniqueIndex_fails_if_key_function_returns_null() { + expectKeyFunctionCantReturnNullNPE(); + + SINGLE_ELEMENT_LIST.stream().collect(uniqueIndex(s -> null)); + } + + @Test + public void uniqueIndex_with_expected_size_fails_if_key_function_returns_null() { + expectKeyFunctionCantReturnNullNPE(); + + SINGLE_ELEMENT_LIST.stream().collect(uniqueIndex(s -> null, 90)); + } + + @Test + public void uniqueIndex_with_valueFunction_fails_if_key_function_returns_null() { + expectKeyFunctionCantReturnNullNPE(); + + SINGLE_ELEMENT_LIST.stream().collect(uniqueIndex(s -> null, MyObj::getText)); + } + + @Test + public void uniqueIndex_with_valueFunction_and_expected_size_fails_if_key_function_returns_null() { + expectKeyFunctionCantReturnNullNPE(); + + SINGLE_ELEMENT_LIST.stream().collect(uniqueIndex(s -> null, MyObj::getText, 9)); + } + + @Test + public void uniqueIndex_with_valueFunction_fails_if_value_function_returns_null() { + expectValueFunctionCantReturnNullNPE(); + + SINGLE_ELEMENT_LIST.stream().collect(uniqueIndex(MyObj::getId, s -> null)); + } + + @Test + public void uniqueIndex_with_valueFunction_and_expected_size_fails_if_value_function_returns_null() { + expectValueFunctionCantReturnNullNPE(); + + SINGLE_ELEMENT_LIST.stream().collect(uniqueIndex(MyObj::getId, s -> null, 9)); + } + + @Test + public void uniqueIndex_returns_map() { + assertThat(LIST.stream().collect(uniqueIndex(MyObj::getId))).containsOnly(entry(1, MY_OBJ_1_A), entry(2, MY_OBJ_2_B), entry(3, MY_OBJ_3_C)); + } + + @Test + public void uniqueIndex_with_expected_size_returns_map() { + assertThat(LIST.stream().collect(uniqueIndex(MyObj::getId, 3))).containsOnly(entry(1, MY_OBJ_1_A), entry(2, MY_OBJ_2_B), entry(3, MY_OBJ_3_C)); + } + + @Test + public void uniqueIndex_with_valueFunction_returns_map() { + assertThat(LIST.stream().collect(uniqueIndex(MyObj::getId, MyObj::getText))).containsOnly(entry(1, "A"), entry(2, "B"), entry(3, "C")); + } + + @Test + public void uniqueIndex_with_valueFunction_and_expected_size_returns_map() { + assertThat(LIST.stream().collect(uniqueIndex(MyObj::getId, MyObj::getText, 9))).containsOnly(entry(1, "A"), entry(2, "B"), entry(3, "C")); + } + + @Test + public void index_empty_stream_returns_empty_map() { + assertThat(Collections.emptyList().stream().collect(index(MyObj::getId))).isEmpty(); + assertThat(Collections.emptyList().stream().collect(index(MyObj::getId, MyObj::getText))).isEmpty(); + } + + @Test + public void index_fails_if_key_function_is_null() { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("Key function can't be null"); + + index(null); + } + + @Test + public void index_with_valueFunction_fails_if_key_function_is_null() { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("Key function can't be null"); + + index(null, MyObj::getText); + } + + @Test + public void index_with_valueFunction_fails_if_value_function_is_null() { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("Value function can't be null"); + + index(MyObj::getId, null); + } + + @Test + public void index_fails_if_key_function_returns_null() { + expectKeyFunctionCantReturnNullNPE(); + + SINGLE_ELEMENT_LIST.stream().collect(index(s -> null)); + } + + @Test + public void index_with_valueFunction_fails_if_key_function_returns_null() { + expectKeyFunctionCantReturnNullNPE(); + + SINGLE_ELEMENT_LIST.stream().collect(index(s -> null, MyObj::getText)); + } + + @Test + public void index_with_valueFunction_fails_if_value_function_returns_null() { + expectValueFunctionCantReturnNullNPE(); + + SINGLE_ELEMENT_LIST.stream().collect(index(MyObj::getId, s -> null)); + } + + @Test + public void index_supports_duplicate_keys() { + Multimap multimap = LIST_WITH_DUPLICATE_ID.stream().collect(index(MyObj::getId)); + + assertThat(multimap.keySet()).containsOnly(1, 2); + assertThat(multimap.get(1)).containsOnly(MY_OBJ_1_A, MY_OBJ_1_C); + assertThat(multimap.get(2)).containsOnly(MY_OBJ_2_B); + } + + @Test + public void uniqueIndex_supports_duplicate_keys() { + Multimap multimap = LIST_WITH_DUPLICATE_ID.stream().collect(index(MyObj::getId, MyObj::getText)); + + assertThat(multimap.keySet()).containsOnly(1, 2); + assertThat(multimap.get(1)).containsOnly("A", "C"); + assertThat(multimap.get(2)).containsOnly("B"); + } + + @Test + public void uniqueIndex_returns_multimap() { + Multimap myObjImmutableListMultimap = LIST.stream().collect(index(MyObj::getId)); + + assertThat(myObjImmutableListMultimap).hasSize(3); + assertThat(myObjImmutableListMultimap).contains(entry(1, MY_OBJ_1_A), entry(2, MY_OBJ_2_B), entry(3, MY_OBJ_3_C)); + } + + @Test + public void index_with_valueFunction_returns_map() { + Multimap multimap = LIST.stream().collect(index(MyObj::getId, MyObj::getText)); + + assertThat(multimap).hasSize(3); + assertThat(multimap).contains(entry(1, "A"), entry(2, "B"), entry(3, "C")); + } + + private void expectedDuplicateKey1IAE() { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Duplicate key 1"); + } + + private void expectKeyFunctionCantReturnNullNPE() { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("Key function can't return null"); + } + + private void expectValueFunctionCantReturnNullNPE() { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("Value function can't return null"); + } + + private static final class MyObj { + private final int id; + private final String text; + + public MyObj(int id, String text) { + this.id = id; + this.text = text; + } + + public int getId() { + return id; + } + + public String getText() { + return text; + } + } }