]> source.dussan.org Git - sonarqube.git/commitdiff
add index and uniqueIndex to Collectors
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Wed, 20 Jul 2016 12:34:25 +0000 (14:34 +0200)
committerSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Thu, 21 Jul 2016 15:03:57 +0000 (17:03 +0200)
sonar-core/src/main/java/org/sonar/core/util/stream/Collectors.java
sonar-core/src/test/java/org/sonar/core/util/stream/CollectorsTest.java

index e13d3c14271df25f2a845bea29a76af3ed1938e7..a1f57685262c436b6dfdbb9025939c7e7b0a6595 100644 (file)
 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.
+   *
+   * <p>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.</p>
    */
   public static <T> Collector<T, List<T>, List<T>> 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.
+   *
+   * <p>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.</p>
    */
   public static <T> Collector<T, Set<T>, Set<T>> 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.
    *
+   * <p>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.</p>
+   *
    * @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.
    *
+   * <p>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.</p>
+   *
    * @see java.util.stream.Collectors#toSet()
    * @see java.util.stream.Collectors#toCollection(Supplier)
    */
   public static <T> Collector<T, ?, HashSet<T>> 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.
+   *
+   * <p>
+   * 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.
+   * </p>
+   *
+   * <p>
+   * {@link Function keyFunction} can't return {@code null}, otherwise a {@link NullPointerException} will be thrown.
+   * </p>
+   *
+   * @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 <K, E> Collector<E, Map<K, E>, ImmutableMap<K, E>> uniqueIndex(Function<? super E, K> keyFunction) {
+    return uniqueIndex(keyFunction, Function.<E>identity());
+  }
+
+  /**
+   * Same as {@link #uniqueIndex(Function)} but using an underlying {@link Map} initialized with a capacity for the
+   * specified expected size.
+   *
+   * <p>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.</p>
+   *
+   * <p>
+   * {@link Function keyFunction} can't return {@code null}, otherwise a {@link NullPointerException} will be thrown.
+   * </p>
+   *
+   * @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 <K, E> Collector<E, Map<K, E>, ImmutableMap<K, E>> uniqueIndex(Function<? super E, K> keyFunction, int expectedSize) {
+    return uniqueIndex(keyFunction, Function.<E>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.
+   *
+   * <p>
+   * 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.
+   * </p>
+   *
+   * <p>
+   * Neither {@link Function keyFunction} nor {@link Function valueFunction} can return {@code null}, otherwise a
+   * {@link NullPointerException} will be thrown.
+   * </p>
+   *
+   * @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 <K, E, V> Collector<E, Map<K, V>, ImmutableMap<K, V>> uniqueIndex(Function<? super E, K> keyFunction,
+    Function<? super E, V> 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.
+   *
+   * <p>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.</p>
+   *
+   * <p>
+   * Neither {@link Function keyFunction} nor {@link Function valueFunction} can return {@code null}, otherwise a
+   * {@link NullPointerException} will be thrown.
+   * </p>
+   *
+   * @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 <K, E, V> Collector<E, Map<K, V>, ImmutableMap<K, V>> uniqueIndex(Function<? super E, K> keyFunction,
+    Function<? super E, V> valueFunction, int expectedSize) {
+    requireNonNull(keyFunction, "Key function can't be null");
+    requireNonNull(valueFunction, "Value function can't be null");
+    BiConsumer<Map<K, V>, 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<Map<K, V>> merger = (m1, m2) -> {
+      for (Map.Entry<K, V> 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 <K, V> Supplier<Map<K, V>> newHashMapSupplier(int expectedSize) {
+    return () -> expectedSize == DEFAULT_HASHMAP_CAPACITY ? new HashMap<>() : new HashMap<>(expectedSize);
+  }
+
+  private static <K, V> void putAndFailOnDuplicateKey(Map<K, V> 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.
+   *
+   * <p>
+   * Neither {@link Function keyFunction} nor {@link Function valueFunction} can return {@code null}, otherwise a
+   * {@link NullPointerException} will be thrown.
+   * </p>
+   *
+   * @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 <K, E> Collector<E, ImmutableListMultimap.Builder<K, E>, ImmutableListMultimap<K, E>> index(Function<? super E, K> keyFunction) {
+    return index(keyFunction, Function.<E>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.
+   *
+   * <p>
+   * Neither {@link Function keyFunction} nor {@link Function valueFunction} can return {@code null}, otherwise a
+   * {@link NullPointerException} will be thrown.
+   * </p>
+   *
+   * @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 <K, E, V> Collector<E, ImmutableListMultimap.Builder<K, V>, ImmutableListMultimap<K, V>> index(Function<? super E, K> keyFunction,
+    Function<? super E, V> valueFunction) {
+    requireNonNull(keyFunction, "Key function can't be null");
+    requireNonNull(valueFunction, "Value function can't be null");
+    BiConsumer<ImmutableListMultimap.Builder<K, V>, 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<ImmutableListMultimap.Builder<K, V>> merger = (m1, m2) -> {
+      for (Map.Entry<K, V> entry : m2.build().entries()) {
+        m1.put(entry.getKey(), entry.getValue());
+      }
+      return m1;
+    };
+    return Collector.of(
+      ImmutableListMultimap::builder,
+      accumulator,
+      merger,
+      ImmutableListMultimap.Builder::build);
+  }
 }
index a8c02735f51e73ff10da95f9425bee7b89350e9e..9bac9ee7d8fc22a8f40bbd849f29cb31d00dc0e7 100644 (file)
@@ -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<MyObj> SINGLE_ELEMENT_LIST = Arrays.asList(MY_OBJ_1_A);
+  private static final List<MyObj> LIST_WITH_DUPLICATE_ID = Arrays.asList(MY_OBJ_1_A, MY_OBJ_2_B, MY_OBJ_1_C);
+  private static final List<MyObj> 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<Integer> res = Arrays.asList(1, 2, 3, 4, 5).stream().collect(Collectors.toList());
+    List<Integer> 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<Integer> res = Arrays.asList(1, 2, 3, 4, 5).stream().collect(Collectors.toList(30));
+    List<Integer> 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<Integer> res = Arrays.asList(1, 2, 3, 4, 5).stream().collect(Collectors.toSet());
+    Set<Integer> 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<Integer> res = Arrays.asList(1, 2, 3, 4, 5).stream().collect(Collectors.toSet(30));
+    Set<Integer> 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<Integer> res = Arrays.asList(1, 2, 3, 4, 5).stream().collect(Collectors.toArrayList());
+    List<Integer> 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<Integer> res = Arrays.asList(1, 2, 3, 4, 5).stream().collect(Collectors.toArrayList(30));
+    List<Integer> 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<Integer> res = Arrays.asList(1, 2, 3, 4, 5).stream().collect(Collectors.toHashSet());
+    Set<Integer> 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<Integer> res = Arrays.asList(1, 2, 3, 4, 5).stream().collect(Collectors.toHashSet(30));
+    Set<Integer> 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.<MyObj>emptyList().stream().collect(uniqueIndex(MyObj::getId))).isEmpty();
+    assertThat(Collections.<MyObj>emptyList().stream().collect(uniqueIndex(MyObj::getId, 6))).isEmpty();
+    assertThat(Collections.<MyObj>emptyList().stream().collect(uniqueIndex(MyObj::getId, MyObj::getText))).isEmpty();
+    assertThat(Collections.<MyObj>emptyList().stream().collect(uniqueIndex(MyObj::getId, MyObj::getText, 10))).isEmpty();
+  }
+
+  @Test
+  public void uniqueIndex_fails_when_there_is_duplicate_keys() {
+    Stream<MyObj> 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<MyObj> 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<MyObj> 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<MyObj> 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.<MyObj>emptyList().stream().collect(index(MyObj::getId))).isEmpty();
+    assertThat(Collections.<MyObj>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<Integer, MyObj> 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<Integer, String> 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<Integer, MyObj> 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<Integer, String> 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;
+    }
+  }
 }