Emulate Java8 Collectors

Change-Id: I948aad38f4250d27e80c172fd56bb44d50cf10af
diff --git a/user/super/com/google/gwt/emul/java/util/stream/Collector.java b/user/super/com/google/gwt/emul/java/util/stream/Collector.java
new file mode 100644
index 0000000..ea96bb5
--- /dev/null
+++ b/user/super/com/google/gwt/emul/java/util/stream/Collector.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2016 Google Inc.
+ *
+ * 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 java.util.stream;
+
+import static javaemul.internal.InternalPreconditions.checkNotNull;
+
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.Set;
+import java.util.function.BiConsumer;
+import java.util.function.BinaryOperator;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+/**
+ * See <a
+ * href="https://docs.oracle.com/javase/8/docs/api/java/util/stream/Collector.html">the
+ * official Java API doc</a> for details.
+ * @param <T> the type of data to be collected
+ * @param <A> the type of accumulator used to track results
+ * @param <R> the final output data type
+ */
+public interface Collector<T,A,R> {
+
+  /**
+   * See <a
+   * href="https://docs.oracle.com/javase/8/docs/api/java/util/stream/Collector.Characteristics.html">the
+   * official Java API doc</a> for details.
+   */
+  enum Characteristics { CONCURRENT, IDENTITY_FINISH, UNORDERED }
+
+  static <T,A,R> Collector<T,A,R> of(Supplier<A> supplier, BiConsumer<A,T> accumulator, BinaryOperator<A> combiner, Function<A,R> finisher, Collector.Characteristics... characteristics) {
+    checkNotNull(supplier);
+    checkNotNull(accumulator);
+    checkNotNull(combiner);
+    checkNotNull(finisher);
+    checkNotNull(characteristics);
+    return new CollectorImpl<>(
+        supplier,
+        accumulator,
+        combiner,
+        finisher,
+        characteristics
+    );
+  }
+
+  static <T,R> Collector<T,R,R> of(Supplier<R> supplier, BiConsumer<R,T> accumulator, BinaryOperator<R> combiner, Collector.Characteristics... characteristics) {
+    checkNotNull(supplier);
+    checkNotNull(accumulator);
+    checkNotNull(combiner);
+    checkNotNull(characteristics);
+    return new CollectorImpl<>(
+        supplier,
+        accumulator,
+        combiner,
+        Function.identity(),
+        Collections.unmodifiableSet(EnumSet.of(Characteristics.IDENTITY_FINISH, characteristics))
+    );
+  }
+
+  Supplier<A> supplier();
+
+  BiConsumer<A,T> accumulator();
+
+  Set<Collector.Characteristics> characteristics();
+
+  BinaryOperator<A> combiner();
+
+  Function<A,R> finisher();
+
+  /**
+   * Simple internal implementation of a collector, holding each of the functions in a field.
+   */
+  static final class CollectorImpl<T, A, R> implements Collector<T, A, R> {
+    private final Supplier<A> supplier;
+    private final BiConsumer<A, T> accumulator;
+    private final Set<Collector.Characteristics> characteristics;
+    private final BinaryOperator<A> combiner;
+    private final Function<A, R> finisher;
+
+    public CollectorImpl(Supplier<A> supplier, BiConsumer<A, T> accumulator, BinaryOperator<A> combiner, Function<A, R> finisher, Characteristics... characteristics) {
+      this.supplier = supplier;
+      this.accumulator = accumulator;
+      if (characteristics.length == 0) {
+        this.characteristics = Collections.emptySet();
+      } else if (characteristics.length == 1) {
+        this.characteristics = Collections.singleton(characteristics[0]);
+      } else {
+        this.characteristics = Collections.unmodifiableSet(EnumSet.of(characteristics[0], characteristics));
+      }
+      this.combiner = combiner;
+      this.finisher = finisher;
+    }
+
+    public CollectorImpl(Supplier<A> supplier, BiConsumer<A, T> accumulator, BinaryOperator<A> combiner, Function<A, R> finisher, Set<Characteristics> characteristics) {
+      this.supplier = supplier;
+      this.accumulator = accumulator;
+      this.combiner = combiner;
+      this.finisher = finisher;
+      this.characteristics = characteristics;
+    }
+
+    @Override
+    public Supplier<A> supplier() {
+      return supplier;
+    }
+
+    @Override
+    public BiConsumer<A, T> accumulator() {
+      return accumulator;
+    }
+
+    @Override
+    public BinaryOperator<A> combiner() {
+      return combiner;
+    }
+
+    @Override
+    public Function<A, R> finisher() {
+      return finisher;
+    }
+
+    @Override
+    public Set<Characteristics> characteristics() {
+      return characteristics;
+    }
+  }
+}
\ No newline at end of file
diff --git a/user/super/com/google/gwt/emul/java/util/stream/Collectors.java b/user/super/com/google/gwt/emul/java/util/stream/Collectors.java
new file mode 100644
index 0000000..620e0ed
--- /dev/null
+++ b/user/super/com/google/gwt/emul/java/util/stream/Collectors.java
@@ -0,0 +1,339 @@
+/*
+ * Copyright 2016 Google Inc.
+ *
+ * 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 java.util.stream;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.DoubleSummaryStatistics;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.IntSummaryStatistics;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.LongSummaryStatistics;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.StringJoiner;
+import java.util.function.BiConsumer;
+import java.util.function.BinaryOperator;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+import java.util.function.ToDoubleFunction;
+import java.util.function.ToIntFunction;
+import java.util.function.ToLongFunction;
+
+/**
+ * See <a
+ * href="https://docs.oracle.com/javase/8/docs/api/java/util/stream/Collectors.html">the
+ * official Java API doc</a> for details.
+ */
+public final class Collectors {
+  public static <T> Collector<T,?,Double> averagingDouble(ToDoubleFunction<? super T> mapper) {
+    // TODO simplify to only collect average if possible
+    return collectingAndThen(summarizingDouble(mapper), DoubleSummaryStatistics::getAverage);
+  }
+
+  public static <T> Collector<T,?,Double> averagingInt(ToIntFunction<? super T> mapper) {
+    // TODO simplify to only collect average if possible
+    return collectingAndThen(summarizingInt(mapper), IntSummaryStatistics::getAverage);
+  }
+
+  public static <T> Collector<T,?,Double> averagingLong(ToLongFunction<? super T> mapper) {
+    // TODO simplify to only collect average if possible
+    return collectingAndThen(summarizingLong(mapper), LongSummaryStatistics::getAverage);
+  }
+
+  public static <T,A,R,RR> Collector<T,A,RR> collectingAndThen(Collector<T,A,R> downstream, Function<R,RR> finisher) {
+    return new Collector.CollectorImpl<>(
+        downstream.supplier(),
+        downstream.accumulator(),
+        downstream.combiner(),
+        downstream.finisher().andThen(finisher),
+        removeIdentFinisher(downstream.characteristics())
+    );
+  }
+
+  public static <T> Collector<T,?,Long> counting() {
+    return reducing(0L, item -> 1L, Long::sum);
+  }
+
+  public static <T,K> Collector<T,?,Map<K,List<T>>> groupingBy(Function<? super T,? extends K> classifier) {
+    // TODO inline this and avoid the finisher extra work of copying from a map to another map
+    //      kept separate for now to unify implementations and reduce testing required
+    return groupingBy(classifier, toList());
+  }
+
+  public static <T,K,A,D> Collector<T,?,Map<K,D>> groupingBy(Function<? super T,? extends K> classifier, Collector<? super T,A,D> downstream) {
+    return groupingBy(classifier, HashMap::new, downstream);
+  }
+
+  public static <T,K,D,A,M extends Map<K,D>> Collector<T,?,M> groupingBy(Function<? super T,? extends K> classifier, Supplier<M> mapFactory, Collector<? super T,A,D> downstream) {
+    return Collector.<T, Map<K, List<T>>, M>of(
+        LinkedHashMap::new,
+        (m, o) -> {
+          K k = classifier.apply(o);
+          List<T> l = m.get(k);
+          if (l == null) {
+            l = new ArrayList<>();
+            m.put(k, l);
+          }
+          l.add(o);
+
+        },
+        (m1, m2) -> mergeAll(m1, m2, Collectors::addAll),
+        m -> {
+          M result = mapFactory.get();
+          for (Map.Entry<K, List<T>> entry : m.entrySet()) {
+            result.put(entry.getKey(), streamAndCollect(downstream, entry.getValue()));
+          }
+          return result;
+        });
+  }
+
+//  not supported
+//  public static <T,K> Collector<T,?,ConcurrentMap<K,List<T>>> groupingByConcurrent(Function<? super T,? extends K> classifier)
+//  public static <T,K,A,D> Collector<T,?,ConcurrentMap<K,D>> groupingByConcurrent(Function<? super T,? extends K> classifier, Collector<? super T,A,D> downstream)
+//  public static <T,K,A,D,M extends ConcurrentMap<K,D>> Collector<T,?,M> groupingByConcurrent(Function<? super T,? extends K> classifier, Supplier<M> mapFactory, Collector<? super T,A,D> downstream)
+
+  public static Collector<CharSequence,?,String> joining() {
+    // specific implementation rather than calling joining("") since we don't need to worry about
+    // appending delimiters between empty strings
+    return Collector.of(
+        StringBuilder::new,
+        StringBuilder::append,
+        StringBuilder::append,
+        StringBuilder::toString
+    );
+  }
+
+  public static Collector<CharSequence,?,String> joining(CharSequence delimiter) {
+    return joining(delimiter, "", "");
+  }
+
+  public static Collector<CharSequence,?,String> joining(final CharSequence delimiter, CharSequence prefix, CharSequence suffix) {
+    return Collector.of(
+        () -> new StringJoiner(delimiter, prefix, suffix),
+        StringJoiner::add,
+        StringJoiner::merge,
+        StringJoiner::toString
+    );
+  }
+
+  public static <T,U,A,R> Collector<T,?,R> mapping(final Function<? super T,? extends U> mapper, final Collector<? super U,A,R> downstream) {
+    return new Collector.CollectorImpl<>(
+        downstream.supplier(),
+        (BiConsumer<A, T>) (A a, T t) -> {
+          downstream.accumulator().accept(a, mapper.apply(t));
+        },
+        downstream.combiner(),
+        downstream.finisher(),
+        downstream.characteristics()
+    );
+  }
+
+  public static <T> Collector<T,?,Optional<T>> maxBy(Comparator<? super T> comparator) {
+    return minBy(comparator.reversed());
+  }
+
+  public static <T> Collector<T,?,Optional<T>> minBy(final Comparator<? super T> comparator) {
+    return reducing((a, b) -> comparator.compare(a, b) < 0 ? a : b);
+  }
+
+  public static <T> Collector<T,?,Map<Boolean,List<T>>> partitioningBy(Predicate<? super T> predicate) {
+    // calling groupBy directly rather than partioningBy so that it can be optimized later
+    return groupingBy(predicate::test);
+  }
+
+  public static <T,D,A> Collector<T,?,Map<Boolean,D>> partitioningBy(Predicate<? super T> predicate, Collector<? super T,A,D> downstream) {
+    return groupingBy(predicate::test, downstream);
+  }
+
+  public static <T> Collector<T,?,Optional<T>> reducing(BinaryOperator<T> op) {
+    return reducing(Optional.empty(), Optional::of, (a, b) -> {
+      if (!a.isPresent()) {
+        return b;
+      }
+      if (!b.isPresent()) {
+        return a;
+      }
+      return Optional.of(op.apply(a.get(), b.get()));
+    });
+  }
+
+  public static <T> Collector<T,?,T> reducing(T identity, BinaryOperator<T> op) {
+    return reducing(identity, Function.identity(), op);
+  }
+
+  @SuppressWarnings("unchecked")
+  public static <T,U> Collector<T,?,U> reducing(final U identity, final Function<? super T,? extends U> mapper, BinaryOperator<U> op) {
+    return Collector.of(
+      () -> new Object[]{identity},
+      (u, t) -> u[0] = op.apply((U) u[0], mapper.apply(t)),
+      (Object[] u1, Object[] u2) -> {
+        u1[0] = op.apply((U) u1[0], (U) u2[0]);
+        return u1;
+      },
+      (Object[] a) -> (U) a[0]
+    );
+  }
+
+  public static <T> Collector<T,?,DoubleSummaryStatistics> summarizingDouble(ToDoubleFunction<? super T> mapper) {
+    return Collector.of(
+        DoubleSummaryStatistics::new,
+        (stats, item) -> stats.accept(mapper.applyAsDouble(item)),
+        (t, u) -> {
+          t.combine(u);
+          return t;
+        },
+        Collector.Characteristics.UNORDERED, Collector.Characteristics.IDENTITY_FINISH
+    );
+  }
+
+  public static <T> Collector<T,?,IntSummaryStatistics> summarizingInt(ToIntFunction<? super T> mapper) {
+    return Collector.of(
+        IntSummaryStatistics::new,
+        (stats, item) -> stats.accept(mapper.applyAsInt(item)),
+        (t, u) -> {
+          t.combine(u);
+          return t;
+        },
+        Collector.Characteristics.UNORDERED, Collector.Characteristics.IDENTITY_FINISH
+    );
+  }
+
+  public static <T> Collector<T,?,LongSummaryStatistics> summarizingLong(ToLongFunction<? super T> mapper) {
+    return Collector.of(
+        LongSummaryStatistics::new,
+        (stats, item) -> stats.accept(mapper.applyAsLong(item)),
+        (t, u) -> {
+          t.combine(u);
+          return t;
+        },
+        Collector.Characteristics.UNORDERED, Collector.Characteristics.IDENTITY_FINISH
+    );
+  }
+
+  public static <T> Collector<T,?,Double> summingDouble(final ToDoubleFunction<? super T> mapper) {
+    // TODO simplify to only collect sum if possible
+    return collectingAndThen(summarizingDouble(mapper), DoubleSummaryStatistics::getSum);
+  }
+
+  public static <T> Collector<T,?,Integer> summingInt(ToIntFunction<? super T> mapper) {
+    // TODO simplify to only collect sum if possible
+    return collectingAndThen(summarizingInt(mapper), intSummaryStatistics -> (int) intSummaryStatistics.getSum());
+  }
+
+  public static <T> Collector<T,?,Long> summingLong(ToLongFunction<? super T> mapper) {
+    // TODO simplify to only collect sum if possible
+    return collectingAndThen(summarizingLong(mapper), LongSummaryStatistics::getSum);
+  }
+
+  public static <T,C extends Collection<T>> Collector<T,?,C> toCollection(final Supplier<C> collectionFactory) {
+    return Collector.of(
+        collectionFactory,
+        Collection::add,
+        // TODO switch to a lambda reference once #9333 is fixed
+        (c1, c2) -> addAll(c1, c2),
+        Collector.Characteristics.IDENTITY_FINISH
+    );
+  }
+
+//  not supported
+//  public static <T,K,U> Collector<T,?,ConcurrentMap<K,U>> toConcurrentMap(Function<? super T,? extends K> keyMapper, Function<? super T,? extends U> valueMapper)
+//  public static <T,K,U> Collector<T,?,ConcurrentMap<K,U>> toConcurrentMap(Function<? super T,? extends K> keyMapper, Function<? super T,? extends U> valueMapper, BinaryOperator<U> mergeFunction)
+//  public static <T,K,U,M extends ConcurrentMap<K,U>> Collector<T,?,M> toConcurrentMap(Function<? super T,? extends K> keyMapper, Function<? super T,? extends U> valueMapper, BinaryOperator<U> mergeFunction, Supplier<M> mapSupplier)
+
+  public static <T> Collector<T,?,List<T>> toList() {
+    return toCollection(ArrayList::new);
+  }
+
+  public static <T,K,U> Collector<T,?,Map<K,U>> toMap(final Function<? super T,? extends K> keyMapper, final Function<? super T,? extends U> valueMapper) {
+    return toMap(keyMapper, valueMapper, (m1, m2) -> { throw new IllegalStateException("Can't assign multiple values to the same key"); });
+  }
+
+  public static <T,K,U> Collector<T,?,Map<K,U>> toMap(Function<? super T,? extends K> keyMapper, Function<? super T,? extends U> valueMapper, BinaryOperator<U> mergeFunction) {
+    return toMap(keyMapper, valueMapper, mergeFunction, HashMap::new);
+  }
+
+  public static <T,K,U,M extends Map<K,U>> Collector<T,?,M> toMap(final Function<? super T,? extends K> keyMapper, final Function<? super T,? extends U> valueMapper, final BinaryOperator<U> mergeFunction, final Supplier<M> mapSupplier) {
+    return Collector.of(
+        mapSupplier,
+        (map, item) -> {
+          K key = keyMapper.apply(item);
+          U newValue = valueMapper.apply(item);
+          if (map.containsKey(key)) {
+            map.put(key, mergeFunction.apply(map.get(key), newValue));
+          } else {
+            map.put(key, newValue);
+          }
+        },
+        (m1, m2) -> mergeAll(m1, m2, mergeFunction),
+        Collector.Characteristics.IDENTITY_FINISH
+    );
+  }
+
+  public static <T> Collector<T,?,Set<T>> toSet() {
+    return Collector.<T, HashSet<T>, Set<T>>of(
+        HashSet::new,
+        HashSet::add,
+        // TODO switch to a lambda reference once #9333 is fixed
+        (c1, c2) -> addAll(c1, c2),
+        // this is Function.identity, but Java doesn't like it here to change types.
+        s -> s,
+        Collector.Characteristics.UNORDERED, Collector.Characteristics.IDENTITY_FINISH
+    );
+  }
+
+  private static Set<Collector.Characteristics> removeIdentFinisher(Set<Collector.Characteristics> characteristics) {
+    if (!characteristics.contains(Collector.Characteristics.IDENTITY_FINISH)) {
+      return characteristics;
+    }
+
+    if (characteristics.size() == 1) {
+      return Collections.emptySet();
+    }
+
+    EnumSet<Collector.Characteristics> result = EnumSet.copyOf(characteristics);
+    result.remove(Collector.Characteristics.IDENTITY_FINISH);
+    return Collections.unmodifiableSet(result);
+  }
+
+  private static <T, D, A> D streamAndCollect(Collector<? super T, A, D> downstream, List<T> list) {
+    A a = downstream.supplier().get();
+    for (T t : list) {
+      downstream.accumulator().accept(a, t);
+    }
+    return downstream.finisher().apply(a);
+  }
+
+  private static <K, V, M extends Map<K, V>> M mergeAll(M m1, M m2, BinaryOperator<V> mergeFunction) {
+    for (Map.Entry<K, V> entry : m2.entrySet()) {
+      m1.merge(entry.getKey(), entry.getValue(), mergeFunction);
+    }
+    return m1;
+  }
+
+  private static <T, C extends Collection<T>> C addAll(C collection, Collection<T> items) {
+    collection.addAll(items);
+    return collection;
+  }
+}
diff --git a/user/test/com/google/gwt/emultest/EmulJava8Suite.java b/user/test/com/google/gwt/emultest/EmulJava8Suite.java
index 62c6526..fbe0c91 100644
--- a/user/test/com/google/gwt/emultest/EmulJava8Suite.java
+++ b/user/test/com/google/gwt/emultest/EmulJava8Suite.java
@@ -38,6 +38,7 @@
 import com.google.gwt.emultest.java8.util.StringJoinerTest;
 import com.google.gwt.emultest.java8.util.TreeMapTest;
 import com.google.gwt.emultest.java8.util.VectorTest;
+import com.google.gwt.emultest.java8.util.stream.CollectorsTest;
 import com.google.gwt.junit.tools.GWTTestSuite;
 
 import junit.framework.Test;
@@ -76,6 +77,10 @@
     suite.addTestSuite(DoubleSummaryStatisticsTest.class);
     suite.addTestSuite(IntSummaryStatisticsTest.class);
     suite.addTestSuite(LongSummaryStatisticsTest.class);
+
+    //-- java.util.stream
+    suite.addTestSuite(CollectorsTest.class);
+
     return suite;
   }
 }
diff --git a/user/test/com/google/gwt/emultest/java8/util/stream/CollectorsTest.java b/user/test/com/google/gwt/emultest/java8/util/stream/CollectorsTest.java
new file mode 100644
index 0000000..7a14614
--- /dev/null
+++ b/user/test/com/google/gwt/emultest/java8/util/stream/CollectorsTest.java
@@ -0,0 +1,546 @@
+/*
+ * Copyright 2016 Google Inc.
+ *
+ * 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 com.google.gwt.emultest.java8.util.stream;
+
+import static java.util.stream.Collectors.averagingDouble;
+import static java.util.stream.Collectors.averagingInt;
+import static java.util.stream.Collectors.averagingLong;
+import static java.util.stream.Collectors.collectingAndThen;
+import static java.util.stream.Collectors.counting;
+import static java.util.stream.Collectors.groupingBy;
+import static java.util.stream.Collectors.joining;
+import static java.util.stream.Collectors.mapping;
+import static java.util.stream.Collectors.maxBy;
+import static java.util.stream.Collectors.minBy;
+import static java.util.stream.Collectors.partitioningBy;
+import static java.util.stream.Collectors.summarizingDouble;
+import static java.util.stream.Collectors.summarizingInt;
+import static java.util.stream.Collectors.summarizingLong;
+import static java.util.stream.Collectors.summingDouble;
+import static java.util.stream.Collectors.summingInt;
+import static java.util.stream.Collectors.summingLong;
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toMap;
+import static java.util.stream.Collectors.toSet;
+
+import com.google.gwt.emultest.java.util.EmulTestBase;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.DoubleSummaryStatistics;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.IntSummaryStatistics;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.LongSummaryStatistics;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.BiConsumer;
+import java.util.function.BiPredicate;
+import java.util.function.BinaryOperator;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Collector;
+
+/**
+ * Tests {@link java.util.stream.Collectors}.
+ * <p />
+ * Methods that are presently only tested indirectly:
+ * <ul>
+ *   <li>reducing: counting, minBy/maxBy use this</li>
+ *   <li>toCollection tested by toList (toSet now uses its own impl)</li>
+ * </ul>
+ */
+public class CollectorsTest extends EmulTestBase {
+
+  public void testAveragingDouble() {
+    Collector<Double, ?, Double> c = averagingDouble(Double::doubleValue);
+    applyItems((4.0 + 8.0) / 2.0, c, 4.0, 8.0);
+
+    assertZeroItemsCollectedAs(0D, c);
+    assertSingleItemCollectedAs(5D, c, 5D);
+  }
+
+  public void testAveragingInt() {
+    Collector<Integer, ?, Double> c = averagingInt(Integer::intValue);
+    applyItems((4.0 + 8.0) / 2.0, c, 4, 8);
+
+    assertZeroItemsCollectedAs(0D, c);
+    assertSingleItemCollectedAs(5D, c, 5);
+  }
+
+  public void testAveragingLong() {
+    Collector<Long, ?, Double> c = averagingLong(Long::longValue);
+    applyItems((4.0 + 8.0) / 2.0, c, 4L, 8L);
+
+    assertZeroItemsCollectedAs(0D, c);
+    assertSingleItemCollectedAs(5D, c, 5L);
+  }
+
+  public void testCollectingAndThen() {
+    Collector<Object, ?, List<Object>> listIdentityCollector = collectingAndThen(toList(), Function.identity());
+    // same test as toList():
+    // same items (allow dups)
+    applyItems(
+        Arrays.asList("a", "a"),
+        listIdentityCollector,
+        "a", "a"
+    );
+
+    // ordered
+    applyItems(
+        Arrays.asList("a", "b"),
+        listIdentityCollector,
+        "a", "b"
+    );
+    assertZeroItemsCollectedAs(Collections.emptyList(), listIdentityCollector);
+    assertSingleItemCollectedAs(Collections.singletonList("a"), listIdentityCollector, "a");
+
+    Collector<Object, ?, Integer> uglyCount = collectingAndThen(toList(), List::size);
+    // (nearly) same test as counting():
+    applyItems(2, uglyCount, "1", new Object());
+
+    assertZeroItemsCollectedAs(0, uglyCount);
+    assertSingleItemCollectedAs(1, uglyCount, new Object());
+  }
+
+  public void testCounting() {
+    Collector<Object, ?, Long> c = counting();
+    applyItems(2L, c, "1", new Object());
+
+    assertZeroItemsCollectedAs(0L, c);
+    assertSingleItemCollectedAs(1L, c, new Object());
+  }
+
+  public void testGroupingBy() {
+    Collector<String, ?, Map<String, List<String>>> c1 = groupingBy(Function.identity());
+
+    Map<String, List<String>> mapOfLists = new HashMap<>();
+    mapOfLists.put("a", Arrays.asList("a", "a"));
+    applyItems(mapOfLists, c1, "a", "a");
+    mapOfLists.clear();
+    mapOfLists.put("a", Collections.singletonList("a"));
+    mapOfLists.put("b", Collections.singletonList("b"));
+    applyItems(mapOfLists, c1, "a", "b");
+
+    assertZeroItemsCollectedAs(Collections.emptyMap(), c1);
+    assertSingleItemCollectedAs(Collections.singletonMap("a", Collections.singletonList("a")), c1, "a");
+
+    Collector<String, ?, LinkedHashMap<String, Set<String>>> c2 = groupingBy(Function.identity(), LinkedHashMap::new, toSet());
+
+    LinkedHashMap<String, Set<String>> linkedMapOfSets = new LinkedHashMap<>();
+    linkedMapOfSets.put("a", Collections.singleton("a"));
+    applyItems(linkedMapOfSets, c2, "a", "a");
+    linkedMapOfSets.clear();
+    linkedMapOfSets.put("a", Collections.singleton("a"));
+    linkedMapOfSets.put("b", Collections.singleton("b"));
+    applyItems(linkedMapOfSets, c2, "a", "b");
+
+    // check to make sure we actually get the linked results, and that they are ordered how we want them
+    LinkedHashMap<String, Set<String>> out = applyItemsWithoutSplitting(c2, "a", "b");
+    assertEquals(Arrays.asList("a", "b"), new ArrayList<>(out.keySet()));
+    out = applyItemsWithoutSplitting(c2, "b", "a");
+    assertEquals(Arrays.asList("b", "a"), new ArrayList<>(out.keySet()));
+
+    assertZeroItemsCollectedAs(new LinkedHashMap<>(), c2);
+    linkedMapOfSets.clear();
+    linkedMapOfSets.put("a", Collections.singleton("a"));
+    assertSingleItemCollectedAs(linkedMapOfSets, c2, "a");
+  }
+
+  public void testJoining() {
+    Collector<CharSequence, ?, String> c = joining();
+    applyItems("ab", c, "a", "b");
+    applyItems("a,", c, "a", ",");
+    applyItems("", c, "", "");
+    assertZeroItemsCollectedAs("", c);
+    assertSingleItemCollectedAs("a", c, "a");
+    assertSingleItemCollectedAs("", c, "");
+
+    c = joining(",");
+    applyItems("a,b", c, "a", "b");
+    applyItems("a,,", c, "a", ",");
+    applyItems(",", c, "", "");
+    assertZeroItemsCollectedAs("", c);
+    assertSingleItemCollectedAs("a", c, "a");
+    assertSingleItemCollectedAs("", c, "");
+
+    c = joining("-", "{", "}");
+    applyItems("{a-b}", c, "a", "b");
+    assertZeroItemsCollectedAs("{}", c);
+    assertSingleItemCollectedAs("{a}", c, "a");
+    assertSingleItemCollectedAs("{}", c, "");
+  }
+
+  public void testMapping() {
+    Collector<String, ?, List<String>> identityMapping = mapping(Function.identity(), toList());
+    // same test as toList():
+    // same items (allow dups)
+    applyItems(
+        Arrays.asList("a", "a"),
+        identityMapping,
+        "a", "a"
+    );
+
+    // ordered
+    applyItems(
+        Arrays.asList("a", "b"),
+        identityMapping,
+        "a", "b"
+    );
+    assertZeroItemsCollectedAs(Collections.emptyList(), identityMapping);
+    assertSingleItemCollectedAs(Collections.singletonList("a"), identityMapping, "a");
+
+    Collector<Integer, ?, List<String>> numberMapping = mapping(s -> "#" + s, toList());
+    // poke the same tests as list, make sure the mapper is run
+    applyItems(
+        Arrays.asList("#1", "#2"),
+        numberMapping,
+        1, 2
+    );
+
+    // ordered
+    applyItems(
+        Arrays.asList("#1", "#2"),
+        numberMapping,
+        1, 2
+    );
+    assertZeroItemsCollectedAs(Collections.emptyList(), numberMapping);
+    assertSingleItemCollectedAs(Collections.singletonList("#10"), numberMapping, 10);
+  }
+
+  public void testMaxBy() {
+    Collector<String, ?, Optional<String>> c = maxBy(Comparator.naturalOrder());
+    applyItems(Optional.of("z"), c, "a", "z");
+    applyItems(Optional.of("z"), c, "z", "a");
+
+    assertZeroItemsCollectedAs(Optional.empty(), c);
+    assertSingleItemCollectedAs(Optional.of("foo"), c, "foo");
+  }
+
+  public void testMinBy() {
+    Collector<String, ?, Optional<String>> c = minBy(Comparator.naturalOrder());
+    applyItems(Optional.of("a"), c, "a", "z");
+    applyItems(Optional.of("a"), c, "z", "a");
+
+    assertZeroItemsCollectedAs(Optional.empty(), c);
+    assertSingleItemCollectedAs(Optional.of("foo"), c, "foo");
+  }
+
+  public void testPartitioningBy() {
+    Collector<Boolean, ?, Map<Boolean, List<Boolean>>> c1 = partitioningBy(Boolean::valueOf);
+
+    Map<Boolean, List<Boolean>> mapOfLists = new HashMap<>();
+    mapOfLists.put(true, Collections.singletonList(true));
+    mapOfLists.put(false, Collections.singletonList(false));
+    applyItems(mapOfLists, c1, true, false);
+    mapOfLists.clear();
+    mapOfLists.put(true, Arrays.asList(true, true));
+    applyItems(mapOfLists, c1, true, true);
+    mapOfLists.clear();
+    mapOfLists.put(false, Arrays.asList(false, false));
+    applyItems(mapOfLists, c1, false, false);
+
+    assertZeroItemsCollectedAs(Collections.emptyMap(), c1);
+    assertSingleItemCollectedAs(Collections.singletonMap(true, Collections.singletonList(true)), c1, true);
+    assertSingleItemCollectedAs(Collections.singletonMap(false, Collections.singletonList(false)), c1, false);
+
+    Collector<Boolean, ?, Map<Boolean, Set<Boolean>>> c2 = partitioningBy(Boolean::valueOf, toSet());
+
+    Map<Boolean, Set<Boolean>> mapOfSets = new HashMap<>();
+    mapOfSets.put(true, Collections.singleton(true));
+    mapOfSets.put(false, Collections.singleton(false));
+    applyItems(mapOfSets, c2, true, false);
+    mapOfSets.clear();
+    mapOfSets.put(true, Collections.singleton(true));
+    applyItems(mapOfSets, c2, true, true);
+    mapOfSets.clear();
+    mapOfSets.put(false, Collections.singleton(false));
+    applyItems(mapOfSets, c2, false, false);
+  }
+
+  public void testSummarizingDouble() {
+    Collector<Double, ?, DoubleSummaryStatistics> c = summarizingDouble(Double::doubleValue);
+    DoubleSummaryStatistics stats = new DoubleSummaryStatistics();
+    stats.accept(5.1);
+    stats.accept(7);
+    BiPredicate<DoubleSummaryStatistics, DoubleSummaryStatistics> equals = (s1, s2) ->
+        s1.getSum() == s2.getSum()
+            && s1.getAverage() == s2.getAverage()
+            && s1.getCount() == s2.getCount()
+            && s1.getMin() == s2.getMin()
+            && s1.getMax() == s2.getMax();
+    applyItems(stats, c, 5.1, 7.0, equals);
+    applyItems(stats, c, 7.0, 5.1, equals);//probably unnecessary to run these backward
+
+    assertZeroItemsCollectedAs(new DoubleSummaryStatistics(), c, equals);
+    stats = new DoubleSummaryStatistics();
+    stats.accept(7.3);
+    assertSingleItemCollectedAs(stats, c, 7.3, equals);
+  }
+
+  public void testSummarizingInt() {
+    Collector<Integer, ?, IntSummaryStatistics> c = summarizingInt(Integer::intValue);
+    IntSummaryStatistics stats = new IntSummaryStatistics();
+    stats.accept(2);
+    stats.accept(10);
+    BiPredicate<IntSummaryStatistics, IntSummaryStatistics> equals = (s1, s2) ->
+        s1.getSum() == s2.getSum()
+            && s1.getAverage() == s2.getAverage()
+            && s1.getCount() == s2.getCount()
+            && s1.getMin() == s2.getMin()
+            && s1.getMax() == s2.getMax();
+    applyItems(stats, c, 2, 10, equals);
+    applyItems(stats, c, 10, 2, equals);
+
+    assertZeroItemsCollectedAs(new IntSummaryStatistics(), c, equals);
+    stats = new IntSummaryStatistics();
+    stats.accept(7);
+    assertSingleItemCollectedAs(stats, c, 7, equals);
+  }
+
+  public void testSummarizingLong() {
+    Collector<Long, ?, LongSummaryStatistics> c = summarizingLong(Long::longValue);
+    LongSummaryStatistics stats = new LongSummaryStatistics();
+    stats.accept(2);
+    stats.accept(10);
+    BiPredicate<LongSummaryStatistics, LongSummaryStatistics> equals = (s1, s2) ->
+        s1.getSum() == s2.getSum()
+            && s1.getAverage() == s2.getAverage()
+            && s1.getCount() == s2.getCount()
+            && s1.getMin() == s2.getMin()
+            && s1.getMax() == s2.getMax();
+    applyItems(stats, c, 2L, 10L, equals);
+    applyItems(stats, c, 10L, 2L, equals);
+
+    assertZeroItemsCollectedAs(new LongSummaryStatistics(), c, equals);
+    stats = new LongSummaryStatistics();
+    stats.accept(7L);
+    assertSingleItemCollectedAs(stats, c, 7L, equals);
+  }
+
+  public void testSummingDouble() {
+    Collector<Double, ?, Double> c = summingDouble(Double::doubleValue);
+    applyItems(4.1 + 8.2, c, 4.1, 8.2);
+
+    assertZeroItemsCollectedAs(0d, c);
+    assertSingleItemCollectedAs(7.3, c, 7.3);
+  }
+
+  public void testSummingInt() {
+    Collector<Integer, ?, Integer> c = summingInt(Integer::intValue);
+    applyItems(4 + 8, c, 4, 8);
+
+    assertZeroItemsCollectedAs(0, c);
+    assertSingleItemCollectedAs(7, c, 7);
+  }
+
+  public void testSummingLong() {
+    Collector<Long, ?, Long> c = summingLong(Long::longValue);
+    applyItems(4L + 8L, c, 4L, 8L);
+
+    assertZeroItemsCollectedAs(0L, c);
+    assertSingleItemCollectedAs(5L, c, 5L);
+  }
+
+  public void testList() {
+    Collector<String, ?, List<String>> c = toList();
+
+    // same items (allow dups)
+    applyItems(
+        Arrays.asList("a", "a"),
+        c,
+        "a", "a"
+    );
+
+    // ordered
+    applyItems(
+        Arrays.asList("a", "b"),
+        c,
+        "a", "b"
+    );
+    assertZeroItemsCollectedAs(Collections.emptyList(), c);
+    assertSingleItemCollectedAs(Collections.singletonList("a"), c, "a");
+  }
+
+  public void testMap() {
+    Collector<String, ?, Map<String, String>> c = toMap(Function.identity(), Function.identity());
+
+    // two distinct items
+    Map<String, String> map = new HashMap<>();
+    map.put("a", "a");
+    map.put("b", "b");
+    applyItems(map, c, "a", "b");
+
+    // inline applyItems and test each to confirm failure for duplicates
+    try {
+      applyItemsWithoutSplitting(c, "a", "a");
+      fail("expected IllegalStateException");
+    } catch (IllegalStateException expected) {
+    }
+    try {
+      applyItemsWithSplitting(c, "a", "a");
+      fail("expected IllegalStateException");
+    } catch (IllegalStateException expected) {
+    }
+
+    assertZeroItemsCollectedAs(Collections.emptyMap(), c);
+    assertSingleItemCollectedAs(Collections.singletonMap("a", "a"), c, "a");
+
+    List<String> seen = new ArrayList<>();
+    c = toMap(Function.identity(), Function.identity(), (s, s2) -> {
+      seen.add("first: " + s);
+      seen.add("second: " + s2);
+      return s + "," + s2;
+    });
+    map = new HashMap<>();
+    map.put("a", "a,a");
+    applyItems(map, c, "a", "a");
+    assertEquals(Arrays.asList("first: a", "second: a", "first: a", "second: a"), seen);
+  }
+
+  public void testSet() {
+    Collector<String, ?, Set<String>> c = toSet();
+
+    // same items (no dups)
+    applyItems(
+        Collections.singleton("a"),
+        c,
+        "a", "a"
+    );
+
+    // different items
+    applyItems(
+        new HashSet<>(Arrays.asList("a", "b")),
+        c,
+        "a", "b"
+    );
+
+    assertZeroItemsCollectedAs(Collections.emptySet(), c);
+    assertSingleItemCollectedAs(Collections.singleton("a"), c, "a");
+  }
+
+  /**
+   * This method attempts to apply a collector to items as a stream might do, so that we
+   * can simply verify the output. Taken from the Collector class's javadoc.
+   */
+  private static <T, A, R> void applyItems(R expected, Collector<T, A, R> collector, T t1, T t2, BiPredicate<R, R> equals) {
+    assertTrue("failed without splitting", equals.test(expected, applyItemsWithoutSplitting(collector, t1, t2)));
+    assertTrue("failed with splitting", equals.test(expected, applyItemsWithSplitting(collector, t1, t2)));
+  }
+
+  /**
+   * This method attempts to apply a collector to items as a stream might do, so that we
+   * can simply verify the output. Taken from the Collector class's javadoc.
+   */
+  private static <T, A, R> void applyItems(R expected, Collector<T, A, R> collector, T t1, T t2) {
+    applyItems(expected, collector, t1, t2, Object::equals);
+  }
+
+  /**
+   * Helper for applyItems.
+   */
+  private static <T, A, R> R applyItemsWithoutSplitting(Collector<T, A, R> collector, T t1, T t2) {
+    Supplier<A> supplier = collector.supplier();
+    BiConsumer<A, T> accumulator = collector.accumulator();
+    // unused in this impl
+    BinaryOperator<A> combiner = collector.combiner();
+    Function<A, R> finisher = collector.finisher();
+
+    A a1 = supplier.get();
+
+    accumulator.accept(a1, t1);
+    accumulator.accept(a1, t2);
+
+    // result without splitting
+    R r1 = finisher.apply(a1);
+    return r1;
+  }
+
+  /**
+   * Helper for applyItems.
+   */
+  private static <T, A, R> R applyItemsWithSplitting(Collector<T, A, R> collector, T t1, T t2) {
+    Supplier<A> supplier = collector.supplier();
+    BiConsumer<A, T> accumulator = collector.accumulator();
+    // actually used in this impl
+    BinaryOperator<A> combiner = collector.combiner();
+    Function<A, R> finisher = collector.finisher();
+
+    A a2 = supplier.get();
+    accumulator.accept(a2, t1);
+    A a3 = supplier.get();
+    accumulator.accept(a3, t2);
+
+    // result with splitting
+    R r2 = finisher.apply(combiner.apply(a2, a3));
+    return r2;
+  }
+
+  private static <T, A, R> void assertZeroItemsCollectedAs(R expected, Collector<T, A, R> collector) {
+    assertZeroItemsCollectedAs(expected, collector, Object::equals);
+  }
+
+  private static <T, A, R> void assertZeroItemsCollectedAs(R expected, Collector<T, A, R> collector, BiPredicate<R, R> equals) {
+    Supplier<A> supplier = collector.supplier();
+    // unused in this impl
+    BiConsumer<A, T> accumulator = collector.accumulator();
+    // shouldn't really be used, just handy to poke the internals quick
+    BinaryOperator<A> combiner = collector.combiner();
+    Function<A, R> finisher = collector.finisher();
+
+    R actual = finisher.apply(supplier.get());
+    assertTrue(equals, expected, actual);
+    // doesn't actually ever happen, just internal checks
+    actual = finisher.apply(combiner.apply(supplier.get(), supplier.get()));
+    assertTrue(equals, expected, actual);
+  }
+
+  private static <T, A, R> void assertSingleItemCollectedAs(R expected, Collector<T, A, R> collector, T item) {
+    assertSingleItemCollectedAs(expected, collector, item, Object::equals);
+  }
+
+  private static <T, A, R> void assertSingleItemCollectedAs(R expected, Collector<T, A, R> collector, T item, BiPredicate<R, R> equals) {
+    Supplier<A> supplier = collector.supplier();
+    BiConsumer<A, T> accumulator = collector.accumulator();
+    // shouldn't really be used, just handy to poke the internals quick
+    BinaryOperator<A> combiner = collector.combiner();
+    Function<A, R> finisher = collector.finisher();
+
+    A a1 = supplier.get();
+
+    accumulator.accept(a1, item);
+
+    R actual = finisher.apply(a1);
+    // normal test
+    assertTrue(equals, expected, actual);
+    // these shouldn't really be used, just handy to poke the internals quick
+    actual = finisher.apply(combiner.apply(a1, supplier.get()));
+    assertTrue(equals, expected, actual);
+    actual = finisher.apply(combiner.apply(supplier.get(), a1));
+    assertTrue(equals, expected, actual);
+  }
+
+  private static <T, U> void assertTrue(BiPredicate<T, U> predicate, T expected, U actual) {
+    assertTrue("expected= " + expected + ", actual=" + actual, predicate.test(expected, actual));
+  }
+
+}