Emulate Java 8 Streams.

Change-Id: Ifbe0c001f1a53f5cb9c4c1a77ee203b2660d4e23
diff --git a/user/super/com/google/gwt/emul/java/util/Arrays.java b/user/super/com/google/gwt/emul/java/util/Arrays.java
index 72da442..4229fe6 100644
--- a/user/super/com/google/gwt/emul/java/util/Arrays.java
+++ b/user/super/com/google/gwt/emul/java/util/Arrays.java
@@ -35,6 +35,11 @@
 import java.util.function.IntUnaryOperator;
 import java.util.function.LongBinaryOperator;
 import java.util.function.UnaryOperator;
+import java.util.stream.DoubleStream;
+import java.util.stream.IntStream;
+import java.util.stream.LongStream;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
 
 import javaemul.internal.ArrayHelper;
 import javaemul.internal.LongCompareHolder;
@@ -1452,6 +1457,38 @@
         Spliterator.IMMUTABLE | Spliterator.ORDERED);
   }
 
+  public static DoubleStream stream(double[] array) {
+    return stream(array, 0, array.length);
+  }
+
+  public static DoubleStream stream(double[] array, int startInclusive, int endExclusive) {
+    return StreamSupport.doubleStream(spliterator(array, startInclusive, endExclusive), false);
+  }
+
+  public static IntStream stream(int[] array) {
+    return stream(array, 0, array.length);
+  }
+
+  public static IntStream stream(int[] array, int startInclusive, int endExclusive) {
+    return StreamSupport.intStream(spliterator(array, startInclusive, endExclusive), false);
+  }
+
+  public static LongStream stream(long[] array) {
+    return stream(array, 0, array.length);
+  }
+
+  public static LongStream stream(long[] array, int startInclusive, int endExclusive) {
+    return StreamSupport.longStream(spliterator(array, startInclusive, endExclusive), false);
+  }
+
+  public static <T> Stream<T> stream(T[] array) {
+    return stream(array, 0, array.length);
+  }
+
+  public static <T> Stream<T> stream(T[] array, int startInclusive, int endExclusive) {
+    return StreamSupport.stream(spliterator(array, startInclusive, endExclusive), false);
+  }
+
   public static String toString(boolean[] a) {
     if (a == null) {
       return "null";
diff --git a/user/super/com/google/gwt/emul/java/util/Collection.java b/user/super/com/google/gwt/emul/java/util/Collection.java
index 379be6d..d678e05 100644
--- a/user/super/com/google/gwt/emul/java/util/Collection.java
+++ b/user/super/com/google/gwt/emul/java/util/Collection.java
@@ -18,6 +18,8 @@
 import static javaemul.internal.InternalPreconditions.checkNotNull;
 
 import java.util.function.Predicate;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
 
 /**
  * General-purpose interface for storing collections of objects.
@@ -49,6 +51,11 @@
   @Override
   Iterator<E> iterator();
 
+  default Stream<E> parallelStream() {
+    // no parallelism in gwt
+    return stream();
+  }
+
   boolean remove(Object o);
 
   boolean removeAll(Collection<?> c);
@@ -74,6 +81,10 @@
     return Spliterators.spliterator(this, 0);
   }
 
+  default Stream<E> stream() {
+    return StreamSupport.stream(spliterator(), false);
+  }
+
   Object[] toArray();
 
   <T> T[] toArray(T[] a);
diff --git a/user/super/com/google/gwt/emul/java/util/stream/BaseStream.java b/user/super/com/google/gwt/emul/java/util/stream/BaseStream.java
new file mode 100644
index 0000000..3d08e80
--- /dev/null
+++ b/user/super/com/google/gwt/emul/java/util/stream/BaseStream.java
@@ -0,0 +1,45 @@
+/*
+ * 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.Iterator;
+import java.util.Spliterator;
+
+/**
+ * See <a href="https://docs.oracle.com/javase/8/docs/api/java/util/stream/BaseStream.html">
+ * the official Java API doc</a> for details.
+ *
+ * @param <T> the contents of the stream
+ * @param <S> the type of stream implementing this interface
+ */
+public interface BaseStream<T,S extends BaseStream<T,S>> extends AutoCloseable {
+  Iterator<T> iterator();
+
+  Spliterator<T> spliterator();
+
+  boolean isParallel();
+
+  S sequential();
+
+  S parallel();
+
+  S unordered();
+
+  S onClose(Runnable closeHandler);
+
+  void close();
+}
diff --git a/user/super/com/google/gwt/emul/java/util/stream/DoubleStream.java b/user/super/com/google/gwt/emul/java/util/stream/DoubleStream.java
new file mode 100644
index 0000000..492f621
--- /dev/null
+++ b/user/super/com/google/gwt/emul/java/util/stream/DoubleStream.java
@@ -0,0 +1,1028 @@
+/*
+ * 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.checkCriticalState;
+import static javaemul.internal.InternalPreconditions.checkNotNull;
+import static javaemul.internal.InternalPreconditions.checkState;
+
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.DoubleSummaryStatistics;
+import java.util.HashSet;
+import java.util.OptionalDouble;
+import java.util.PrimitiveIterator;
+import java.util.Spliterator;
+import java.util.Spliterators;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.DoubleBinaryOperator;
+import java.util.function.DoubleConsumer;
+import java.util.function.DoubleFunction;
+import java.util.function.DoublePredicate;
+import java.util.function.DoubleSupplier;
+import java.util.function.DoubleToIntFunction;
+import java.util.function.DoubleToLongFunction;
+import java.util.function.DoubleUnaryOperator;
+import java.util.function.IntConsumer;
+import java.util.function.LongConsumer;
+import java.util.function.ObjDoubleConsumer;
+import java.util.function.Supplier;
+
+/**
+ * See <a href="https://docs.oracle.com/javase/8/docs/api/java/util/stream/DoubleStream.html">
+ * the official Java API doc</a> for details.
+ */
+public interface DoubleStream extends BaseStream<Double,DoubleStream> {
+
+  /**
+   * Value holder for various stream operations.
+   */
+  static final class ValueConsumer implements DoubleConsumer {
+    double value;
+
+    @Override
+    public void accept(double value) {
+      this.value = value;
+    }
+  }
+
+  static DoubleStream.Builder builder() {
+    return new Builder() {
+      private double[] items = new double[0];
+
+      @Override
+      public void accept(double t) {
+        checkState(items != null, "Builder already built");
+        items[items.length] = t;
+      }
+
+      @Override
+      public DoubleStream build() {
+        checkCriticalState(items != null, "Builder already built");
+        DoubleStream stream = Arrays.stream(items);
+        items = null;
+        return stream;
+      }
+    };
+  }
+
+  static DoubleStream concat(DoubleStream a, DoubleStream b) {
+    // This is nearly the same as flatMap, but inlined, wrapped around a single spliterator of
+    // these two objects, and without close() called as the stream progresses. Instead, close is
+    // invoked as part of the resulting stream's own onClose, so that either can fail without
+    // affecting the other, and correctly collecting suppressed exceptions.
+
+    // TODO replace this flatMap-ish spliterator with one that directly combines the two root
+    // streams
+    Spliterator<? extends DoubleStream> spliteratorOfStreams =
+        Arrays.asList(a, b).spliterator();
+    DoubleStream result = new DoubleStreamSource(null, new Spliterators.AbstractDoubleSpliterator(
+        Long.MAX_VALUE,
+        0
+    ) {
+      DoubleStream nextStream;
+      Spliterator.OfDouble next;
+
+      @Override
+      public boolean tryAdvance(DoubleConsumer action) {
+        // look for a new spliterator
+        while (advanceToNextSpliterator()) {
+          // if we have one, try to read and use it
+          if (next.tryAdvance(action)) {
+            return true;
+          } else {
+            nextStream = null;
+            // failed, null it out so we can find another
+            next = null;
+          }
+        }
+        return false;
+      }
+
+      private boolean advanceToNextSpliterator() {
+        while (next == null) {
+          if (!spliteratorOfStreams.tryAdvance(n -> {
+            if (n != null) {
+              nextStream = n;
+              next = n.spliterator();
+            }
+          })) {
+            return false;
+          }
+        }
+        return true;
+      }
+    });
+
+    result.onClose(a::close);
+    result.onClose(b::close);
+
+    return result;
+  }
+
+  static DoubleStream empty() {
+    return new EmptyDoubleStreamSource(null);
+  }
+
+  static DoubleStream generate(DoubleSupplier s) {
+    return StreamSupport.doubleStream(new Spliterators.AbstractDoubleSpliterator(
+        Long.MAX_VALUE,
+        Spliterator.IMMUTABLE | Spliterator.ORDERED
+    ) {
+      @Override
+      public boolean tryAdvance(DoubleConsumer action) {
+        action.accept(s.getAsDouble());
+        return true;
+      }
+    }, false);
+  }
+
+  static DoubleStream iterate(double seed, DoubleUnaryOperator f) {
+    return StreamSupport.doubleStream(new Spliterators.AbstractDoubleSpliterator(
+        Long.MAX_VALUE,
+        Spliterator.IMMUTABLE | Spliterator.ORDERED
+    ) {
+      private double next = seed;
+
+      @Override
+      public boolean tryAdvance(DoubleConsumer action) {
+        action.accept(next);
+        next = f.applyAsDouble(next);
+        return true;
+      }
+    }, false);
+  }
+
+  static DoubleStream of(double... values) {
+    return Arrays.stream(values);
+  }
+
+  static DoubleStream of(double t) {
+    // TODO consider a splittable that returns only a single value
+    return of(new double[]{t});
+  }
+
+  /**
+   * See
+   * <a href="https://docs.oracle.com/javase/8/docs/api/java/util/stream/DoubleStream.Builder.html">
+   * the official Java API doc</a> for details.
+   */
+  public interface Builder extends DoubleConsumer {
+    @Override
+    void accept(double t);
+
+    default DoubleStream.Builder add(double t) {
+      accept(t);
+      return this;
+    }
+
+    DoubleStream build();
+  }
+
+  boolean allMatch(DoublePredicate predicate);
+
+  boolean anyMatch(DoublePredicate predicate);
+
+  OptionalDouble average();
+
+  Stream<Double> boxed();
+
+  <R> R collect(Supplier<R> supplier, ObjDoubleConsumer<R> accumulator, BiConsumer<R,R> combiner);
+
+  long count();
+
+  DoubleStream distinct();
+
+  DoubleStream filter(DoublePredicate predicate);
+
+  OptionalDouble findAny();
+
+  OptionalDouble findFirst();
+
+  DoubleStream flatMap(DoubleFunction<? extends DoubleStream> mapper);
+
+  void forEach(DoubleConsumer action);
+
+  void forEachOrdered(DoubleConsumer action);
+
+  PrimitiveIterator.OfDouble iterator();
+
+  DoubleStream limit(long maxSize);
+
+  DoubleStream map(DoubleUnaryOperator mapper);
+
+  IntStream mapToInt(DoubleToIntFunction mapper);
+
+  LongStream mapToLong(DoubleToLongFunction mapper);
+
+  <U> Stream<U> mapToObj(DoubleFunction<? extends U> mapper);
+
+  OptionalDouble max();
+
+  OptionalDouble min();
+
+  boolean noneMatch(DoublePredicate predicate);
+
+  DoubleStream parallel();
+
+  DoubleStream peek(DoubleConsumer action);
+
+  OptionalDouble reduce(DoubleBinaryOperator op);
+
+  double reduce(double identity, DoubleBinaryOperator op);
+
+  DoubleStream sequential();
+
+  DoubleStream skip(long n);
+
+  DoubleStream sorted();
+
+  Spliterator.OfDouble spliterator();
+
+  double sum();
+
+  DoubleSummaryStatistics summaryStatistics();
+
+  double[] toArray();
+
+  /**
+   * Represents an empty stream, doing nothing for all methods.
+   */
+  static class EmptyDoubleStreamSource extends TerminatableStream<EmptyDoubleStreamSource>
+      implements DoubleStream {
+    public EmptyDoubleStreamSource(TerminatableStream<?> previous) {
+      super(previous);
+    }
+
+    @Override
+    public DoubleStream filter(DoublePredicate predicate) {
+      throwIfTerminated();
+      return this;
+    }
+
+    @Override
+    public DoubleStream map(DoubleUnaryOperator mapper) {
+      throwIfTerminated();
+      return this;
+    }
+
+    @Override
+    public <U> Stream<U> mapToObj(DoubleFunction<? extends U> mapper) {
+      throwIfTerminated();
+      return new Stream.EmptyStreamSource<U>(this);
+    }
+
+    @Override
+    public IntStream mapToInt(DoubleToIntFunction mapper) {
+      throwIfTerminated();
+      return new IntStream.EmptyIntStreamSource(this);
+    }
+
+    @Override
+    public LongStream mapToLong(DoubleToLongFunction mapper) {
+      throwIfTerminated();
+      return new LongStream.EmptyLongStreamSource(this);
+    }
+
+    @Override
+    public DoubleStream flatMap(DoubleFunction<? extends DoubleStream> mapper) {
+      throwIfTerminated();
+      return this;
+    }
+
+    @Override
+    public DoubleStream distinct() {
+      throwIfTerminated();
+      return this;
+    }
+
+    @Override
+    public DoubleStream sorted() {
+      throwIfTerminated();
+      return this;
+    }
+
+    @Override
+    public DoubleStream peek(DoubleConsumer action) {
+      throwIfTerminated();
+      return this;
+    }
+
+    @Override
+    public DoubleStream limit(long maxSize) {
+      throwIfTerminated();
+      checkState(maxSize >= 0, "maxSize may not be negative");
+      return this;
+    }
+
+    @Override
+    public DoubleStream skip(long n) {
+      throwIfTerminated();
+      checkState(n >= 0, "n may not be negative");
+      return this;
+    }
+
+    @Override
+    public void forEach(DoubleConsumer action) {
+      terminate();
+      // do nothing
+    }
+
+    @Override
+    public void forEachOrdered(DoubleConsumer action) {
+      terminate();
+      // do nothing
+    }
+
+    @Override
+    public double[] toArray() {
+      terminate();
+      return new double[0];
+    }
+
+    @Override
+    public double reduce(double identity, DoubleBinaryOperator op) {
+      terminate();
+      return identity;
+    }
+
+    @Override
+    public OptionalDouble reduce(DoubleBinaryOperator op) {
+      terminate();
+      return OptionalDouble.empty();
+    }
+
+    @Override
+    public <R> R collect(Supplier<R> supplier,
+                         ObjDoubleConsumer<R> accumulator,
+                         BiConsumer<R, R> combiner) {
+      terminate();
+      return supplier.get();
+    }
+
+    @Override
+    public double sum() {
+      terminate();
+      return 0;
+    }
+
+    @Override
+    public OptionalDouble min() {
+      terminate();
+      return OptionalDouble.empty();
+    }
+
+    @Override
+    public OptionalDouble max() {
+      terminate();
+      return OptionalDouble.empty();
+    }
+
+    @Override
+    public long count() {
+      terminate();
+      return 0;
+    }
+
+    @Override
+    public OptionalDouble average() {
+      terminate();
+      return OptionalDouble.empty();
+    }
+
+    @Override
+    public DoubleSummaryStatistics summaryStatistics() {
+      terminate();
+      return new DoubleSummaryStatistics();
+    }
+
+    @Override
+    public boolean anyMatch(DoublePredicate predicate) {
+      terminate();
+      return false;
+    }
+
+    @Override
+    public boolean allMatch(DoublePredicate predicate) {
+      terminate();
+      return true;
+    }
+
+    @Override
+    public boolean noneMatch(DoublePredicate predicate) {
+      terminate();
+      return true;
+    }
+
+    @Override
+    public OptionalDouble findFirst() {
+      terminate();
+      return OptionalDouble.empty();
+    }
+
+    @Override
+    public OptionalDouble findAny() {
+      terminate();
+      return OptionalDouble.empty();
+    }
+
+    @Override
+    public Stream<Double> boxed() {
+      throwIfTerminated();
+      return new Stream.EmptyStreamSource<Double>(this);
+    }
+
+    @Override
+    public DoubleStream sequential() {
+      throwIfTerminated();
+      return this;
+    }
+
+    @Override
+    public DoubleStream parallel() {
+      throwIfTerminated();
+      return this;
+    }
+
+    @Override
+    public PrimitiveIterator.OfDouble iterator() {
+      return Spliterators.iterator(spliterator());
+    }
+
+    @Override
+    public Spliterator.OfDouble spliterator() {
+      terminate();
+      return Spliterators.emptyDoubleSpliterator();
+    }
+
+    @Override
+    public boolean isParallel() {
+      throwIfTerminated();
+      return false;
+    }
+
+    @Override
+    public DoubleStream unordered() {
+      throwIfTerminated();
+      return this;
+    }
+  }
+
+  /**
+   * Double to Int map spliterator.
+   */
+  static final class MapToIntSpliterator extends Spliterators.AbstractIntSpliterator {
+    private final DoubleToIntFunction map;
+    private final Spliterator.OfDouble original;
+
+    public MapToIntSpliterator(DoubleToIntFunction map, Spliterator.OfDouble original) {
+      super(
+          original.estimateSize(),
+          original.characteristics() & ~(Spliterator.SORTED | Spliterator.DISTINCT)
+      );
+      checkNotNull(map);
+      this.map = map;
+      this.original = original;
+    }
+
+    @Override
+    public boolean tryAdvance(final IntConsumer action) {
+      return original.tryAdvance((double u) -> action.accept(map.applyAsInt(u)));
+    }
+  }
+
+  /**
+   * Double to Object map spliterator.
+   * @param <T> the type of Object in the spliterator
+   */
+  static final class MapToObjSpliterator<T> extends Spliterators.AbstractSpliterator<T> {
+    private final DoubleFunction<? extends T> map;
+    private final Spliterator.OfDouble original;
+
+    public MapToObjSpliterator(DoubleFunction<? extends T> map, Spliterator.OfDouble original) {
+      super(
+          original.estimateSize(),
+          original.characteristics() & ~(Spliterator.SORTED | Spliterator.DISTINCT)
+      );
+      checkNotNull(map);
+      this.map = map;
+      this.original = original;
+    }
+
+    @Override
+    public boolean tryAdvance(final Consumer<? super T> action) {
+      return original.tryAdvance((double u) -> action.accept(map.apply(u)));
+    }
+  }
+
+  /**
+   * Double to Long map spliterator.
+   */
+  static final class MapToLongSpliterator extends Spliterators.AbstractLongSpliterator {
+    private final DoubleToLongFunction map;
+    private final Spliterator.OfDouble original;
+
+    public MapToLongSpliterator(DoubleToLongFunction map, Spliterator.OfDouble original) {
+      super(
+          original.estimateSize(),
+          original.characteristics() & ~(Spliterator.SORTED | Spliterator.DISTINCT)
+      );
+      checkNotNull(map);
+      this.map = map;
+      this.original = original;
+    }
+
+    @Override
+    public boolean tryAdvance(final LongConsumer action) {
+      return original.tryAdvance((double u) -> action.accept(map.applyAsLong(u)));
+    }
+  }
+
+  /**
+   * Double to Double map spliterator.
+   */
+  static final class MapToDoubleSpliterator extends Spliterators.AbstractDoubleSpliterator {
+    private final DoubleUnaryOperator map;
+    private final Spliterator.OfDouble original;
+
+    public MapToDoubleSpliterator(DoubleUnaryOperator map, Spliterator.OfDouble original) {
+      super(
+          original.estimateSize(),
+          original.characteristics() & ~(Spliterator.SORTED | Spliterator.DISTINCT)
+      );
+      checkNotNull(map);
+      this.map = map;
+      this.original = original;
+    }
+
+    @Override
+    public boolean tryAdvance(final DoubleConsumer action) {
+      return original.tryAdvance((double u) -> action.accept(map.applyAsDouble(u)));
+    }
+  }
+
+  /**
+   * Double filter spliterator.
+   */
+  static final class FilterSpliterator extends Spliterators.AbstractDoubleSpliterator {
+    private final DoublePredicate filter;
+    private final Spliterator.OfDouble original;
+
+    private boolean found;
+
+    public FilterSpliterator(DoublePredicate filter, Spliterator.OfDouble original) {
+      super(original.estimateSize(), original.characteristics() & ~Spliterator.SIZED);
+      checkNotNull(filter);
+      this.filter = filter;
+      this.original = original;
+    }
+
+    @Override
+    public Comparator<? super Double> getComparator() {
+      return original.getComparator();
+    }
+
+    @Override
+    public boolean tryAdvance(final DoubleConsumer action) {
+      found = false;
+      while (!found && original.tryAdvance((double item) -> {
+        if (filter.test(item)) {
+          found = true;
+          action.accept(item);
+        }
+      })) {
+        // do nothing, work is done in tryAdvance
+      }
+
+      return found;
+    }
+  }
+
+  /**
+   * Double skip spliterator.
+   */
+  static final class SkipSpliterator extends Spliterators.AbstractDoubleSpliterator {
+    private long skip;
+    private final Spliterator.OfDouble original;
+
+    public SkipSpliterator(long skip, Spliterator.OfDouble original) {
+      super(
+          original.hasCharacteristics(Spliterator.SIZED)
+              ? Math.max(0, original.estimateSize() - skip)
+              : Long.MAX_VALUE,
+          original.characteristics()
+      );
+      this.skip = skip;
+      this.original = original;
+    }
+
+    @Override
+    public Comparator<? super Double> getComparator() {
+      return original.getComparator();
+    }
+
+    @Override
+    public boolean tryAdvance(DoubleConsumer action) {
+      while (skip > 0) {
+        if (!original.tryAdvance((double ignore) -> { })) {
+          return false;
+        }
+        skip--;
+      }
+      return original.tryAdvance(action);
+    }
+  }
+
+  /**
+   * Double limit spliterator.
+   */
+  static final class LimitSpliterator extends Spliterators.AbstractDoubleSpliterator {
+    private final long limit;
+    private final Spliterator.OfDouble original;
+    private int position = 0;
+
+    public LimitSpliterator(long limit, Spliterator.OfDouble original) {
+      super(
+          original.hasCharacteristics(Spliterator.SIZED)
+              ? Math.min(original.estimateSize(), limit)
+              : Long.MAX_VALUE,
+          original.characteristics()
+      );
+      this.limit = limit;
+      this.original = original;
+    }
+
+    @Override
+    public Comparator<? super Double> getComparator() {
+      return original.getComparator();
+    }
+
+    @Override
+    public boolean tryAdvance(DoubleConsumer action) {
+      if (limit <= position) {
+        return false;
+      }
+      boolean result = original.tryAdvance(action);
+      position++;
+      return result;
+    }
+  }
+
+  /**
+   * Main implementation of DoubleStream, wrapping a single spliterator, and an optional parent
+   * stream.
+   */
+  static class DoubleStreamSource extends TerminatableStream<DoubleStreamSource>
+      implements DoubleStream {
+    private final Spliterator.OfDouble spliterator;
+
+    public DoubleStreamSource(TerminatableStream<?> previous, Spliterator.OfDouble spliterator) {
+      super(previous);
+      this.spliterator = spliterator;
+    }
+
+    // terminals
+
+    @Override
+    public void forEach(DoubleConsumer action) {
+      forEachOrdered(action);
+    }
+
+    @Override
+    public void forEachOrdered(DoubleConsumer action) {
+      terminate();
+      spliterator.forEachRemaining(action);
+    }
+
+    @Override
+    public double[] toArray() {
+      terminate();
+      double[] entries = new double[0];
+      // this is legal in js, since the array will be backed by a JS array
+      spliterator.forEachRemaining((double value) -> entries[entries.length] = value);
+
+      return entries;
+    }
+
+    @Override
+    public double reduce(double identity, DoubleBinaryOperator op) {
+      terminate();
+      ValueConsumer holder = new ValueConsumer();
+      holder.value = identity;
+      spliterator.forEachRemaining((double value) -> {
+        holder.accept(op.applyAsDouble(holder.value, value));
+      });
+      return holder.value;
+    }
+
+    @Override
+    public OptionalDouble reduce(DoubleBinaryOperator op) {
+      ValueConsumer holder = new ValueConsumer();
+      if (spliterator.tryAdvance(holder)) {
+        return OptionalDouble.of(reduce(holder.value, op));
+      }
+      terminate();
+      return OptionalDouble.empty();
+    }
+
+    @Override
+    public <R> R collect(Supplier<R> supplier,
+                         ObjDoubleConsumer<R> accumulator,
+                         BiConsumer<R, R> combiner) {
+      terminate();
+      final R acc = supplier.get();
+      spliterator.forEachRemaining((double value) -> accumulator.accept(acc, value));
+      return acc;
+    }
+
+    @Override
+    public double sum() {
+      return summaryStatistics().getSum();
+    }
+
+    @Override
+    public OptionalDouble min() {
+      terminate();
+      final ValueConsumer holder = new ValueConsumer();
+      if (spliterator.tryAdvance((double value) -> holder.value = value)) {
+        spliterator.forEachRemaining((double value) -> {
+          holder.value = Math.min(holder.value, value);
+        });
+        return OptionalDouble.of(holder.value);
+      }
+      return OptionalDouble.empty();
+    }
+
+    @Override
+    public OptionalDouble max() {
+      terminate();
+      final ValueConsumer holder = new ValueConsumer();
+      if (spliterator.tryAdvance((double value) -> holder.value = value)) {
+        spliterator.forEachRemaining((double value) -> {
+          holder.value = Math.max(holder.value, value);
+        });
+        return OptionalDouble.of(holder.value);
+      }
+      return OptionalDouble.empty();
+    }
+
+    @Override
+    public long count() {
+      terminate();
+      long count = 0;
+      while (spliterator.tryAdvance((double value) -> { })) {
+        count++;
+      }
+      return count;
+    }
+
+    @Override
+    public OptionalDouble average() {
+      DoubleSummaryStatistics stats = summaryStatistics();
+      if (stats.getCount() == 0) {
+        return OptionalDouble.empty();
+      }
+      return OptionalDouble.of(stats.getAverage());
+    }
+
+    @Override
+    public DoubleSummaryStatistics summaryStatistics() {
+      return collect(
+          DoubleSummaryStatistics::new,
+          // TODO switch to a lambda reference once #9340 is fixed
+          (doubleSummaryStatistics, value) -> doubleSummaryStatistics.accept(value),
+          DoubleSummaryStatistics::combine
+      );
+    }
+
+    @Override
+    public boolean anyMatch(DoublePredicate predicate) {
+      return filter(predicate).findFirst().isPresent();
+    }
+
+    @Override
+    public boolean allMatch(DoublePredicate predicate) {
+      return !anyMatch(predicate.negate());
+    }
+
+    @Override
+    public boolean noneMatch(DoublePredicate predicate) {
+      return !anyMatch(predicate);
+    }
+
+    @Override
+    public OptionalDouble findFirst() {
+      terminate();
+      ValueConsumer holder = new ValueConsumer();
+      if (spliterator.tryAdvance(holder)) {
+        return OptionalDouble.of(holder.value);
+      }
+      return OptionalDouble.empty();
+    }
+
+    @Override
+    public OptionalDouble findAny() {
+      return findFirst();
+    }
+
+    @Override
+    public PrimitiveIterator.OfDouble iterator() {
+      return Spliterators.iterator(spliterator());
+    }
+
+    @Override
+    public Spliterator.OfDouble spliterator() {
+      terminate();
+      return spliterator;
+    }
+
+    // end terminals
+
+    // intermediates
+
+    @Override
+    public DoubleStream filter(DoublePredicate predicate) {
+      throwIfTerminated();
+      return new DoubleStreamSource(this, new FilterSpliterator(predicate, spliterator));
+    }
+
+    @Override
+    public DoubleStream map(DoubleUnaryOperator mapper) {
+      throwIfTerminated();
+      return new DoubleStreamSource(this, new MapToDoubleSpliterator(mapper, spliterator));
+    }
+
+    @Override
+    public <U> Stream<U> mapToObj(DoubleFunction<? extends U> mapper) {
+      throwIfTerminated();
+      return new Stream.StreamSource(this, new MapToObjSpliterator<U>(mapper, spliterator));
+    }
+
+    @Override
+    public IntStream mapToInt(DoubleToIntFunction mapper) {
+      throwIfTerminated();
+      return new IntStream.IntStreamSource(this, new MapToIntSpliterator(mapper, spliterator));
+    }
+
+    @Override
+    public LongStream mapToLong(DoubleToLongFunction mapper) {
+      throwIfTerminated();
+      return new LongStream.LongStreamSource(this, new MapToLongSpliterator(mapper, spliterator));
+    }
+
+    @Override
+    public DoubleStream flatMap(DoubleFunction<? extends DoubleStream> mapper) {
+      throwIfTerminated();
+      final Spliterator<? extends DoubleStream> spliteratorOfStreams =
+          new MapToObjSpliterator<DoubleStream>(mapper, spliterator);
+      return new DoubleStreamSource(this, new Spliterators.AbstractDoubleSpliterator(
+          Long.MAX_VALUE,
+          0
+      ) {
+        DoubleStream nextStream;
+        Spliterator.OfDouble next;
+
+        @Override
+        public boolean tryAdvance(DoubleConsumer action) {
+          // look for a new spliterator
+          while (advanceToNextSpliterator()) {
+            // if we have one, try to read and use it
+            if (next.tryAdvance(action)) {
+              return true;
+            } else {
+              nextStream.close();
+              nextStream = null;
+              // failed, null it out so we can find another
+              next = null;
+            }
+          }
+          return false;
+        }
+
+        private boolean advanceToNextSpliterator() {
+          while (next == null) {
+            if (!spliteratorOfStreams.tryAdvance(n -> {
+              if (n != null) {
+                nextStream = n;
+                next = n.spliterator();
+              }
+            })) {
+              return false;
+            }
+          }
+          return true;
+        }
+      });
+    }
+
+    @Override
+    public DoubleStream distinct() {
+      throwIfTerminated();
+      HashSet<Double> seen = new HashSet<>();
+      return filter(seen::add);
+    }
+
+    @Override
+    public DoubleStream sorted() {
+      throwIfTerminated();
+      return new DoubleStreamSource(this, new Spliterators.AbstractDoubleSpliterator(
+          spliterator.estimateSize(),
+          spliterator.characteristics() | Spliterator.SORTED
+      ) {
+        Spliterator.OfDouble ordered = null;
+
+        @Override
+        public Comparator<? super Double> getComparator() {
+          return null;
+        }
+
+        @Override
+        public boolean tryAdvance(DoubleConsumer action) {
+          if (ordered == null) {
+            double[] list = new double[0];
+            spliterator.forEachRemaining((double item) -> list[list.length] = item);
+            Arrays.sort(list);
+            ordered = Spliterators.spliterator(list, characteristics());
+          }
+          return ordered.tryAdvance(action);
+        }
+      });
+    }
+
+    @Override
+    public DoubleStream peek(DoubleConsumer action) {
+      checkNotNull(action);
+      throwIfTerminated();
+      return new DoubleStreamSource(this, new Spliterators.AbstractDoubleSpliterator(
+          spliterator.estimateSize(),
+          spliterator.characteristics()
+      ) {
+        @Override
+        public boolean tryAdvance(final DoubleConsumer innerAction) {
+          return spliterator.tryAdvance(action.andThen(innerAction));
+        }
+      });
+    }
+
+    @Override
+    public DoubleStream limit(long maxSize) {
+      throwIfTerminated();
+      checkState(maxSize >= 0, "maxSize may not be negative");
+      return new DoubleStreamSource(this, new LimitSpliterator(maxSize, spliterator));
+    }
+
+    @Override
+    public DoubleStream skip(long n) {
+      throwIfTerminated();
+      checkState(n >= 0, "n may not be negative");
+      if (n == 0) {
+        return this;
+      }
+      return new DoubleStreamSource(this, new SkipSpliterator(n, spliterator));
+    }
+
+    @Override
+    public Stream<Double> boxed() {
+      return mapToObj(Double::valueOf);
+    }
+
+    @Override
+    public DoubleStream sequential() {
+      throwIfTerminated();
+      return this;
+    }
+
+    @Override
+    public DoubleStream parallel() {
+      throwIfTerminated();
+      return this;
+    }
+
+    @Override
+    public boolean isParallel() {
+      throwIfTerminated();
+      return false;
+    }
+
+    @Override
+    public DoubleStream unordered() {
+      throwIfTerminated();
+      return this;
+    }
+  }
+}
diff --git a/user/super/com/google/gwt/emul/java/util/stream/IntStream.java b/user/super/com/google/gwt/emul/java/util/stream/IntStream.java
new file mode 100644
index 0000000..8a0816e
--- /dev/null
+++ b/user/super/com/google/gwt/emul/java/util/stream/IntStream.java
@@ -0,0 +1,1081 @@
+/*
+ * 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.checkCriticalState;
+import static javaemul.internal.InternalPreconditions.checkNotNull;
+import static javaemul.internal.InternalPreconditions.checkState;
+
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.IntSummaryStatistics;
+import java.util.OptionalDouble;
+import java.util.OptionalInt;
+import java.util.PrimitiveIterator;
+import java.util.Spliterator;
+import java.util.Spliterators;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.DoubleConsumer;
+import java.util.function.IntBinaryOperator;
+import java.util.function.IntConsumer;
+import java.util.function.IntFunction;
+import java.util.function.IntPredicate;
+import java.util.function.IntSupplier;
+import java.util.function.IntToDoubleFunction;
+import java.util.function.IntToLongFunction;
+import java.util.function.IntUnaryOperator;
+import java.util.function.LongConsumer;
+import java.util.function.ObjIntConsumer;
+import java.util.function.Supplier;
+
+/**
+ * See <a href="https://docs.oracle.com/javase/8/docs/api/java/util/stream/IntStream.html">
+ * the official Java API doc</a> for details.
+ */
+public interface IntStream extends BaseStream<Integer,IntStream> {
+
+  /**
+   * Value holder for various stream operations.
+   */
+  static final class ValueConsumer implements IntConsumer {
+    int value;
+
+    @Override
+    public void accept(int value) {
+      this.value = value;
+    }
+  }
+
+  static IntStream.Builder builder() {
+    return new Builder() {
+      private int[] items = new int[0];
+
+      @Override
+      public void accept(int t) {
+        checkState(items != null, "Builder already built");
+        items[items.length] = t;
+      }
+
+      @Override
+      public IntStream build() {
+        checkCriticalState(items != null, "Builder already built");
+        IntStream stream = Arrays.stream(items);
+        items = null;
+        return stream;
+      }
+    };
+  }
+
+  static IntStream concat(IntStream a, IntStream b) {
+    // This is nearly the same as flatMap, but inlined, wrapped around a single spliterator of
+    // these two objects, and without close() called as the stream progresses. Instead, close is
+    // invoked as part of the resulting stream's own onClose, so that either can fail without
+    // affecting the other, and correctly collecting suppressed exceptions.
+
+    // TODO replace this flatMap-ish spliterator with one that directly combines the two root
+    // streams
+    Spliterator<? extends IntStream> spliteratorOfStreams =
+        Arrays.asList(a, b).spliterator();
+    IntStream result = new IntStreamSource(null, new Spliterators.AbstractIntSpliterator(Long
+        .MAX_VALUE, 0) {
+      IntStream nextStream;
+      Spliterator.OfInt next;
+
+      @Override
+      public boolean tryAdvance(IntConsumer action) {
+        // look for a new spliterator
+        while (advanceToNextSpliterator()) {
+          // if we have one, try to read and use it
+          if (next.tryAdvance(action)) {
+            return true;
+          } else {
+            nextStream = null;
+            // failed, null it out so we can find another
+            next = null;
+          }
+        }
+        return false;
+      }
+
+      private boolean advanceToNextSpliterator() {
+        while (next == null) {
+          if (!spliteratorOfStreams.tryAdvance(n -> {
+            if (n != null) {
+              nextStream = n;
+              next = n.spliterator();
+            }
+          })) {
+            return false;
+          }
+        }
+        return true;
+      }
+    });
+
+    result.onClose(a::close);
+    result.onClose(b::close);
+
+    return result;
+  }
+
+  static IntStream empty() {
+    return new EmptyIntStreamSource(null);
+  }
+
+  static IntStream generate(final IntSupplier s) {
+    return StreamSupport.intStream(new Spliterators.AbstractIntSpliterator(
+        Long.MAX_VALUE,
+        Spliterator.IMMUTABLE | Spliterator.ORDERED
+    ) {
+      @Override
+      public boolean tryAdvance(IntConsumer action) {
+        action.accept(s.getAsInt());
+        return true;
+      }
+    }, false);
+  }
+
+  static IntStream iterate(int seed, IntUnaryOperator f) {
+    return StreamSupport.intStream(new Spliterators.AbstractIntSpliterator(
+        Long.MAX_VALUE,
+        Spliterator.IMMUTABLE | Spliterator.ORDERED
+    ) {
+      private int next = seed;
+
+      @Override
+      public boolean tryAdvance(IntConsumer action) {
+        action.accept(next);
+        next = f.applyAsInt(next);
+        return true;
+      }
+    }, false);
+  }
+
+  static IntStream of(int... values) {
+    return Arrays.stream(values);
+  }
+
+  static IntStream of(int t) {
+    // TODO consider a splittable that returns only a single value
+    return of(new int[]{t});
+  }
+
+  static IntStream range(int startInclusive, int endExclusive) {
+    if (startInclusive >= endExclusive) {
+      return empty();
+    }
+    return rangeClosed(startInclusive, endExclusive - 1);
+  }
+
+  static IntStream rangeClosed(int startInclusive, int endInclusive) {
+    if (startInclusive > endInclusive) {
+      return empty();
+    }
+    int count = endInclusive - startInclusive + 1;
+
+    return StreamSupport.intStream(new Spliterators.AbstractIntSpliterator(
+        count,
+        Spliterator.IMMUTABLE | Spliterator.SIZED | Spliterator.SUBSIZED | Spliterator.ORDERED
+            | Spliterator.SORTED | Spliterator.DISTINCT
+    ) {
+      private int next = startInclusive;
+
+      @Override
+      public Comparator<? super Integer> getComparator() {
+        return null;
+      }
+
+      @Override
+      public boolean tryAdvance(IntConsumer action) {
+        if (next <= endInclusive) {
+          action.accept(next++);
+          return true;
+        }
+        return false;
+      }
+    }, false);
+  }
+
+  /**
+   * See
+   * <a href="https://docs.oracle.com/javase/8/docs/api/java/util/stream/IntStream.Builder.html">
+   * the official Java API doc</a> for details.
+   */
+  public interface Builder extends IntConsumer {
+    @Override
+    void accept(int t);
+
+    default IntStream.Builder add(int t) {
+      accept(t);
+      return this;
+    }
+
+    IntStream build();
+  }
+
+  boolean allMatch(IntPredicate predicate);
+
+  boolean anyMatch(IntPredicate predicate);
+
+  DoubleStream asDoubleStream();
+
+  LongStream asLongStream();
+
+  OptionalDouble average();
+
+  Stream<Integer> boxed();
+
+  <R> R collect(Supplier<R> supplier, ObjIntConsumer<R> accumulator, BiConsumer<R,R> combiner);
+
+  long count();
+
+  IntStream distinct();
+
+  IntStream filter(IntPredicate predicate);
+
+  OptionalInt findAny();
+
+  OptionalInt findFirst();
+
+  IntStream flatMap(IntFunction<? extends IntStream> mapper);
+
+  void forEach(IntConsumer action);
+
+  void forEachOrdered(IntConsumer action);
+
+  PrimitiveIterator.OfInt iterator();
+
+  IntStream limit(long maxSize);
+
+  IntStream map(IntUnaryOperator mapper);
+
+  DoubleStream mapToDouble(IntToDoubleFunction mapper);
+
+  LongStream mapToLong(IntToLongFunction mapper);
+
+  <U> Stream<U> mapToObj(IntFunction<? extends U> mapper);
+
+  OptionalInt max();
+
+  OptionalInt min();
+
+  boolean noneMatch(IntPredicate predicate);
+
+  IntStream parallel();
+
+  IntStream peek(IntConsumer action);
+
+  OptionalInt reduce(IntBinaryOperator op);
+
+  int reduce(int identity, IntBinaryOperator op);
+
+  IntStream sequential();
+
+  IntStream skip(long n);
+
+  IntStream sorted();
+
+  Spliterator.OfInt spliterator();
+
+  int sum();
+
+  IntSummaryStatistics summaryStatistics();
+
+  int[] toArray();
+
+  /**
+   * Represents an empty stream, doing nothing for all methods.
+   */
+  static class EmptyIntStreamSource extends TerminatableStream<EmptyIntStreamSource>
+      implements IntStream {
+    public EmptyIntStreamSource(TerminatableStream<?> previous) {
+      super(previous);
+    }
+
+    @Override
+    public IntStream filter(IntPredicate predicate) {
+      throwIfTerminated();
+      return this;
+    }
+
+    @Override
+    public IntStream map(IntUnaryOperator mapper) {
+      throwIfTerminated();
+      return this;
+    }
+
+    @Override
+    public <U> Stream<U> mapToObj(IntFunction<? extends U> mapper) {
+      throwIfTerminated();
+      return new Stream.EmptyStreamSource<U>(this);
+    }
+
+    @Override
+    public LongStream mapToLong(IntToLongFunction mapper) {
+      throwIfTerminated();
+      return new LongStream.EmptyLongStreamSource(this);
+    }
+
+    @Override
+    public DoubleStream mapToDouble(IntToDoubleFunction mapper) {
+      throwIfTerminated();
+      return new DoubleStream.EmptyDoubleStreamSource(this);
+    }
+
+    @Override
+    public IntStream flatMap(IntFunction<? extends IntStream> mapper) {
+      throwIfTerminated();
+      return this;
+    }
+
+    @Override
+    public IntStream distinct() {
+      throwIfTerminated();
+      return this;
+    }
+
+    @Override
+    public IntStream sorted() {
+      throwIfTerminated();
+      return this;
+    }
+
+    @Override
+    public IntStream peek(IntConsumer action) {
+      throwIfTerminated();
+      return this;
+    }
+
+    @Override
+    public IntStream limit(long maxSize) {
+      throwIfTerminated();
+      checkState(maxSize >= 0, "maxSize may not be negative");
+      return this;
+    }
+
+    @Override
+    public IntStream skip(long n) {
+      throwIfTerminated();
+      checkState(n >= 0, "n may not be negative");
+      return this;
+    }
+
+    @Override
+    public void forEach(IntConsumer action) {
+      terminate();
+      // do nothing
+    }
+
+    @Override
+    public void forEachOrdered(IntConsumer action) {
+      terminate();
+      // do nothing
+    }
+
+    @Override
+    public int[] toArray() {
+      terminate();
+      return new int[0];
+    }
+
+    @Override
+    public int reduce(int identity, IntBinaryOperator op) {
+      terminate();
+      return identity;
+    }
+
+    @Override
+    public OptionalInt reduce(IntBinaryOperator op) {
+      terminate();
+      return OptionalInt.empty();
+    }
+
+    @Override
+    public <R> R collect(Supplier<R> supplier,
+                         ObjIntConsumer<R> accumulator,
+                         BiConsumer<R, R> combiner) {
+      terminate();
+      return supplier.get();
+    }
+
+    @Override
+    public int sum() {
+      terminate();
+      return 0;
+    }
+
+    @Override
+    public OptionalInt min() {
+      terminate();
+      return OptionalInt.empty();
+    }
+
+    @Override
+    public OptionalInt max() {
+      terminate();
+      return OptionalInt.empty();
+    }
+
+    @Override
+    public long count() {
+      terminate();
+      return 0;
+    }
+
+    @Override
+    public OptionalDouble average() {
+      terminate();
+      return OptionalDouble.empty();
+    }
+
+    @Override
+    public IntSummaryStatistics summaryStatistics() {
+      terminate();
+      return new IntSummaryStatistics();
+    }
+
+    @Override
+    public boolean anyMatch(IntPredicate predicate) {
+      terminate();
+      return false;
+    }
+
+    @Override
+    public boolean allMatch(IntPredicate predicate) {
+      terminate();
+      return true;
+    }
+
+    @Override
+    public boolean noneMatch(IntPredicate predicate) {
+      terminate();
+      return true;
+    }
+
+    @Override
+    public OptionalInt findFirst() {
+      terminate();
+      return OptionalInt.empty();
+    }
+
+    @Override
+    public OptionalInt findAny() {
+      terminate();
+      return OptionalInt.empty();
+    }
+
+    @Override
+    public LongStream asLongStream() {
+      throwIfTerminated();
+      return new LongStream.EmptyLongStreamSource(this);
+    }
+
+    @Override
+    public DoubleStream asDoubleStream() {
+      throwIfTerminated();
+      return new DoubleStream.EmptyDoubleStreamSource(this);
+    }
+
+    @Override
+    public Stream<Integer> boxed() {
+      throwIfTerminated();
+      return new Stream.EmptyStreamSource<Integer>(this);
+    }
+
+    @Override
+    public IntStream sequential() {
+      throwIfTerminated();
+      return this;
+    }
+
+    @Override
+    public IntStream parallel() {
+      throwIfTerminated();
+      return this;
+    }
+
+    @Override
+    public PrimitiveIterator.OfInt iterator() {
+      return Spliterators.iterator(spliterator());
+    }
+
+    @Override
+    public Spliterator.OfInt spliterator() {
+      terminate();
+      return Spliterators.emptyIntSpliterator();
+    }
+
+    @Override
+    public boolean isParallel() {
+      throwIfTerminated();
+      return false;
+    }
+
+    @Override
+    public IntStream unordered() {
+      throwIfTerminated();
+      return this;
+    }
+  }
+
+  /**
+   * Int to Int map spliterator.
+   */
+  static final class MapToIntSpliterator extends Spliterators.AbstractIntSpliterator {
+    private final IntUnaryOperator map;
+    private final Spliterator.OfInt original;
+
+    public MapToIntSpliterator(IntUnaryOperator map, Spliterator.OfInt original) {
+      super(
+          original.estimateSize(),
+          original.characteristics() & ~(Spliterator.SORTED | Spliterator.DISTINCT)
+      );
+      checkNotNull(map);
+      this.map = map;
+      this.original = original;
+    }
+
+    @Override
+    public boolean tryAdvance(final IntConsumer action) {
+      return original.tryAdvance((int u) -> action.accept(map.applyAsInt(u)));
+    }
+  }
+
+  /**
+   * Int to Object map spliterator.
+   * @param <T> the type of data in the object spliterator
+   */
+  static final class MapToObjSpliterator<T> extends Spliterators.AbstractSpliterator<T> {
+    private final IntFunction<? extends T> map;
+    private final Spliterator.OfInt original;
+
+    public MapToObjSpliterator(IntFunction<? extends T> map, Spliterator.OfInt original) {
+      super(
+          original.estimateSize(),
+          original.characteristics() & ~(Spliterator.SORTED | Spliterator.DISTINCT)
+      );
+      checkNotNull(map);
+      this.map = map;
+      this.original = original;
+    }
+
+    @Override
+    public boolean tryAdvance(final Consumer<? super T> action) {
+      return original.tryAdvance((int u) -> action.accept(map.apply(u)));
+    }
+  }
+
+  /**
+   * Int to Long map spliterator.
+   */
+  static final class MapToLongSpliterator extends Spliterators.AbstractLongSpliterator {
+    private final IntToLongFunction map;
+    private final Spliterator.OfInt original;
+
+    public MapToLongSpliterator(IntToLongFunction map, Spliterator.OfInt original) {
+      super(
+          original.estimateSize(),
+          original.characteristics() & ~(Spliterator.SORTED | Spliterator.DISTINCT)
+      );
+      checkNotNull(map);
+      this.map = map;
+      this.original = original;
+    }
+
+    @Override
+    public boolean tryAdvance(final LongConsumer action) {
+      return original.tryAdvance((int u) -> action.accept(map.applyAsLong(u)));
+    }
+  }
+
+  /**
+   * Int to Double map spliterator.
+   */
+  static final class MapToDoubleSpliterator extends Spliterators.AbstractDoubleSpliterator {
+    private final IntToDoubleFunction map;
+    private final Spliterator.OfInt original;
+
+    public MapToDoubleSpliterator(IntToDoubleFunction map, Spliterator.OfInt original) {
+      super(
+          original.estimateSize(),
+          original.characteristics() & ~(Spliterator.SORTED | Spliterator.DISTINCT)
+      );
+      checkNotNull(map);
+      this.map = map;
+      this.original = original;
+    }
+
+    @Override
+    public boolean tryAdvance(final DoubleConsumer action) {
+      return original.tryAdvance((int u) -> action.accept(map.applyAsDouble(u)));
+    }
+  }
+
+  /**
+   * Int filter spliterator.
+   */
+  static final class FilterSpliterator extends Spliterators.AbstractIntSpliterator {
+    private final IntPredicate filter;
+    private final Spliterator.OfInt original;
+
+    private boolean found;
+
+    public FilterSpliterator(IntPredicate filter, Spliterator.OfInt original) {
+      super(original.estimateSize(), original.characteristics() & ~Spliterator.SIZED);
+      checkNotNull(filter);
+      this.filter = filter;
+      this.original = original;
+    }
+
+    @Override
+    public Comparator<? super Integer> getComparator() {
+      return original.getComparator();
+    }
+
+    @Override
+    public boolean tryAdvance(final IntConsumer action) {
+      found = false;
+      while (!found && original.tryAdvance((int item) -> {
+        if (filter.test(item)) {
+          found = true;
+          action.accept(item);
+        }
+      })) {
+        // do nothing, work is done in tryAdvance
+      }
+
+      return found;
+    }
+  }
+
+  /**
+   * Int skip spliterator.
+   */
+  static final class SkipSpliterator extends Spliterators.AbstractIntSpliterator {
+    private long skip;
+    private final Spliterator.OfInt original;
+
+    public SkipSpliterator(long skip, Spliterator.OfInt original) {
+      super(
+          original.hasCharacteristics(Spliterator.SIZED)
+              ? Math.max(0, original.estimateSize() - skip)
+              : Long.MAX_VALUE,
+          original.characteristics()
+      );
+      this.skip = skip;
+      this.original = original;
+    }
+
+    @Override
+    public Comparator<? super Integer> getComparator() {
+      return original.getComparator();
+    }
+
+    @Override
+    public boolean tryAdvance(IntConsumer action) {
+      while (skip > 0) {
+        if (!original.tryAdvance((int ignore) -> { })) {
+          return false;
+        }
+        skip--;
+      }
+      return original.tryAdvance(action);
+    }
+  }
+
+  /**
+   * Int limit spliterator.
+   */
+  static final class LimitSpliterator extends Spliterators.AbstractIntSpliterator {
+    private final long limit;
+    private final Spliterator.OfInt original;
+    private int position = 0;
+
+    public LimitSpliterator(long limit, Spliterator.OfInt original) {
+      super(
+          original.hasCharacteristics(Spliterator.SIZED)
+              ? Math.min(original.estimateSize(), limit)
+              : Long.MAX_VALUE,
+          original.characteristics()
+      );
+      this.limit = limit;
+      this.original = original;
+    }
+
+    @Override
+    public Comparator<? super Integer> getComparator() {
+      return original.getComparator();
+    }
+
+    @Override
+    public boolean tryAdvance(IntConsumer action) {
+      if (limit <= position) {
+        return false;
+      }
+      boolean result = original.tryAdvance(action);
+      position++;
+      return result;
+    }
+  }
+
+  /**
+   * Main implementation of IntStream, wrapping a single spliterator, and an optional parent stream.
+   */
+  static class IntStreamSource extends TerminatableStream<IntStreamSource> implements IntStream {
+    private final Spliterator.OfInt spliterator;
+
+    public IntStreamSource(TerminatableStream<?> previous, Spliterator.OfInt spliterator) {
+      super(previous);
+      this.spliterator = spliterator;
+    }
+
+    // terminals
+    @Override
+    public Spliterator.OfInt spliterator() {
+      terminate();
+      return spliterator;
+    }
+
+    @Override
+    public PrimitiveIterator.OfInt iterator() {
+      return Spliterators.iterator(spliterator());
+    }
+
+    @Override
+    public OptionalInt findFirst() {
+      terminate();
+      ValueConsumer holder = new ValueConsumer();
+      if (spliterator.tryAdvance(holder)) {
+        return OptionalInt.of(holder.value);
+      }
+      return OptionalInt.empty();
+    }
+
+    @Override
+    public OptionalInt findAny() {
+      return findFirst();
+    }
+
+    @Override
+    public boolean noneMatch(IntPredicate predicate) {
+      return !anyMatch(predicate);
+    }
+
+    @Override
+    public boolean allMatch(IntPredicate predicate) {
+      return !anyMatch(predicate.negate());
+    }
+
+    @Override
+    public boolean anyMatch(IntPredicate predicate) {
+      return filter(predicate).findFirst().isPresent();
+    }
+
+    @Override
+    public IntSummaryStatistics summaryStatistics() {
+      return collect(
+          IntSummaryStatistics::new,
+          // TODO switch to a lambda reference once #9340 is fixed
+          (intSummaryStatistics, value) -> intSummaryStatistics.accept(value),
+          IntSummaryStatistics::combine
+      );
+    }
+
+    @Override
+    public OptionalDouble average() {
+      IntSummaryStatistics stats = summaryStatistics();
+      if (stats.getCount() == 0) {
+        return OptionalDouble.empty();
+      }
+      return OptionalDouble.of(stats.getAverage());
+    }
+
+    @Override
+    public long count() {
+      terminate();
+      long count = 0;
+      while (spliterator.tryAdvance((int value) -> { })) {
+        count++;
+      }
+      return count;
+    }
+
+    @Override
+    public OptionalInt max() {
+      terminate();
+      final ValueConsumer holder = new ValueConsumer();
+      if (spliterator.tryAdvance((int value) -> holder.value = value)) {
+        spliterator.forEachRemaining((int value) -> holder.value = Math.max(holder.value, value));
+        return OptionalInt.of(holder.value);
+      }
+      return OptionalInt.empty();
+    }
+
+    @Override
+    public OptionalInt min() {
+      terminate();
+      final ValueConsumer holder = new ValueConsumer();
+      if (spliterator.tryAdvance((int value) -> holder.value = value)) {
+        spliterator.forEachRemaining((int value) -> holder.value = Math.min(holder.value, value));
+        return OptionalInt.of(holder.value);
+      }
+      return OptionalInt.empty();
+    }
+
+    @Override
+    public int sum() {
+      return (int) summaryStatistics().getSum();
+    }
+
+    @Override
+    public <R> R collect(Supplier<R> supplier,
+                         final ObjIntConsumer<R> accumulator,
+                         BiConsumer<R, R> combiner) {
+      terminate();
+      final R acc = supplier.get();
+      spliterator.forEachRemaining((int value) -> accumulator.accept(acc, value));
+      return acc;
+    }
+
+    @Override
+    public OptionalInt reduce(IntBinaryOperator op) {
+      ValueConsumer holder = new ValueConsumer();
+      if (spliterator.tryAdvance(holder)) {
+        return OptionalInt.of(reduce(holder.value, op));
+      }
+      terminate();
+      return OptionalInt.empty();
+    }
+
+    @Override
+    public int reduce(int identity, IntBinaryOperator op) {
+      terminate();
+      ValueConsumer holder = new ValueConsumer();
+      holder.value = identity;
+      spliterator.forEachRemaining((int value) -> {
+        holder.accept(op.applyAsInt(holder.value, value));
+      });
+      return holder.value;
+    }
+
+    @Override
+    public int[] toArray() {
+      terminate();
+      int[] entries = new int[0];
+      // this is legal in js, since the array will be backed by a JS array
+      spliterator.forEachRemaining((int value) -> entries[entries.length] = value);
+
+      return entries;
+    }
+
+    @Override
+    public void forEachOrdered(IntConsumer action) {
+      terminate();
+      spliterator.forEachRemaining(action);
+    }
+
+    @Override
+    public void forEach(IntConsumer action) {
+      forEachOrdered(action);
+    }
+    // end terminals
+
+    // intermediates
+
+    @Override
+    public IntStream filter(IntPredicate predicate) {
+      throwIfTerminated();
+      return new IntStreamSource(this, new FilterSpliterator(predicate, spliterator));
+    }
+
+    @Override
+    public IntStream map(IntUnaryOperator mapper) {
+      throwIfTerminated();
+      return new IntStreamSource(this, new MapToIntSpliterator(mapper, spliterator));
+    }
+
+    @Override
+    public <U> Stream<U> mapToObj(IntFunction<? extends U> mapper) {
+      throwIfTerminated();
+      return new Stream.StreamSource(this, new MapToObjSpliterator<U>(mapper, spliterator));
+    }
+
+    @Override
+    public LongStream mapToLong(IntToLongFunction mapper) {
+      throwIfTerminated();
+      return new LongStream.LongStreamSource(this, new MapToLongSpliterator(mapper, spliterator));
+    }
+
+    @Override
+    public DoubleStream mapToDouble(IntToDoubleFunction mapper) {
+      throwIfTerminated();
+      return new DoubleStream.DoubleStreamSource(
+          this,
+          new MapToDoubleSpliterator(mapper, spliterator)
+      );
+    }
+
+    @Override
+    public IntStream flatMap(IntFunction<? extends IntStream> mapper) {
+      throwIfTerminated();
+      final Spliterator<? extends IntStream> spliteratorOfStreams =
+          new MapToObjSpliterator<>(mapper, spliterator);
+      return new IntStreamSource(this, new Spliterators.AbstractIntSpliterator(Long.MAX_VALUE, 0) {
+        IntStream nextStream;
+        Spliterator.OfInt next;
+
+        @Override
+        public boolean tryAdvance(IntConsumer action) {
+          // look for a new spliterator
+          while (advanceToNextSpliterator()) {
+            // if we have one, try to read and use it
+            if (next.tryAdvance(action)) {
+              return true;
+            } else {
+              nextStream.close();
+              nextStream = null;
+              // failed, null it out so we can find another
+              next = null;
+            }
+          }
+          return false;
+        }
+
+        private boolean advanceToNextSpliterator() {
+          while (next == null) {
+            if (!spliteratorOfStreams.tryAdvance(n -> {
+              if (n != null) {
+                nextStream = n;
+                next = n.spliterator();
+              }
+            })) {
+              return false;
+            }
+          }
+          return true;
+        }
+      });
+    }
+
+    @Override
+    public IntStream distinct() {
+      throwIfTerminated();
+      HashSet<Integer> seen = new HashSet<>();
+      return filter(seen::add);
+    }
+
+    @Override
+    public IntStream sorted() {
+      throwIfTerminated();
+      return new IntStreamSource(this, new Spliterators.AbstractIntSpliterator(
+          spliterator.estimateSize(),
+          spliterator.characteristics() | Spliterator.SORTED
+      ) {
+        Spliterator.OfInt ordered = null;
+
+        @Override
+        public Comparator<? super Integer> getComparator() {
+          return null;
+        }
+
+        @Override
+        public boolean tryAdvance(IntConsumer action) {
+          if (ordered == null) {
+            int[] list = new int[0];
+            spliterator.forEachRemaining((int item) -> list[list.length] = item);
+            Arrays.sort(list);
+            ordered = Spliterators.spliterator(list, characteristics());
+          }
+          return ordered.tryAdvance(action);
+        }
+      });
+    }
+
+    @Override
+    public IntStream peek(IntConsumer action) {
+      checkNotNull(action);
+      throwIfTerminated();
+      return new IntStreamSource(this, new Spliterators.AbstractIntSpliterator(
+          spliterator.estimateSize(),
+          spliterator.characteristics()
+      ) {
+        @Override
+        public boolean tryAdvance(final IntConsumer innerAction) {
+          return spliterator.tryAdvance(action.andThen(innerAction));
+        }
+      });
+    }
+
+    @Override
+    public IntStream limit(long maxSize) {
+      throwIfTerminated();
+      checkState(maxSize >= 0, "maxSize may not be negative");
+      return new IntStreamSource(this, new LimitSpliterator(maxSize, spliterator));
+    }
+
+    @Override
+    public IntStream skip(long n) {
+      throwIfTerminated();
+      checkState(n >= 0, "n may not be negative");
+      if (n == 0) {
+        return this;
+      }
+      return new IntStreamSource(this, new SkipSpliterator(n, spliterator));
+    }
+
+    @Override
+    public LongStream asLongStream() {
+      return mapToLong(i -> (long) i);
+    }
+
+    @Override
+    public DoubleStream asDoubleStream() {
+      return mapToDouble(i -> (double) i);
+    }
+
+    @Override
+    public Stream<Integer> boxed() {
+      return mapToObj(Integer::valueOf);
+    }
+
+    @Override
+    public IntStream sequential() {
+      throwIfTerminated();
+      return this;
+    }
+
+    @Override
+    public IntStream parallel() {
+      throwIfTerminated();
+      return this;
+    }
+
+    @Override
+    public boolean isParallel() {
+      throwIfTerminated();
+      return false;
+    }
+
+    @Override
+    public IntStream unordered() {
+      throwIfTerminated();
+      return this;
+    }
+  }
+}
diff --git a/user/super/com/google/gwt/emul/java/util/stream/LongStream.java b/user/super/com/google/gwt/emul/java/util/stream/LongStream.java
new file mode 100644
index 0000000..f2b6ba1
--- /dev/null
+++ b/user/super/com/google/gwt/emul/java/util/stream/LongStream.java
@@ -0,0 +1,1072 @@
+/*
+ * 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.checkCriticalState;
+import static javaemul.internal.InternalPreconditions.checkNotNull;
+import static javaemul.internal.InternalPreconditions.checkState;
+
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.LongSummaryStatistics;
+import java.util.OptionalDouble;
+import java.util.OptionalLong;
+import java.util.PrimitiveIterator;
+import java.util.Spliterator;
+import java.util.Spliterators;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.DoubleConsumer;
+import java.util.function.IntConsumer;
+import java.util.function.LongBinaryOperator;
+import java.util.function.LongConsumer;
+import java.util.function.LongFunction;
+import java.util.function.LongPredicate;
+import java.util.function.LongSupplier;
+import java.util.function.LongToDoubleFunction;
+import java.util.function.LongToIntFunction;
+import java.util.function.LongUnaryOperator;
+import java.util.function.ObjLongConsumer;
+import java.util.function.Supplier;
+
+/**
+ * See <a href="https://docs.oracle.com/javase/8/docs/api/java/util/stream/LongStream.html">
+ * the official Java API doc</a> for details.
+ */
+public interface LongStream extends BaseStream<Long,LongStream> {
+
+  /**
+   * Value holder for various stream operations.
+   */
+  static final class ValueConsumer implements LongConsumer {
+    long value;
+
+    @Override
+    public void accept(long value) {
+      this.value = value;
+    }
+  }
+
+  static LongStream.Builder builder() {
+    return new Builder() {
+      private long[] items = new long[0];
+
+      @Override
+      public void accept(long t) {
+        checkState(items != null, "Builder already built");
+        items[items.length] = t;
+      }
+
+      @Override
+      public LongStream build() {
+        checkCriticalState(items != null, "Builder already built");
+        LongStream stream = Arrays.stream(items);
+        items = null;
+        return stream;
+      }
+    };
+  }
+
+  static LongStream concat(LongStream a, LongStream b) {
+    // This is nearly the same as flatMap, but inlined, wrapped around a single spliterator of
+    // these two objects, and without close() called as the stream progresses. Instead, close is
+    // invoked as part of the resulting stream's own onClose, so that either can fail without
+    // affecting the other, and correctly collecting suppressed exceptions.
+
+    // TODO replace this flatMap-ish spliterator with one that directly combines the two root
+    // streams
+    Spliterator<? extends LongStream> spliteratorOfStreams =
+        Arrays.asList(a, b).spliterator();
+    LongStream result = new LongStreamSource(null, new Spliterators.AbstractLongSpliterator(Long
+        .MAX_VALUE, 0) {
+      LongStream nextStream;
+      Spliterator.OfLong next;
+
+      @Override
+      public boolean tryAdvance(LongConsumer action) {
+        // look for a new spliterator
+        while (advanceToNextSpliterator()) {
+          // if we have one, try to read and use it
+          if (next.tryAdvance(action)) {
+            return true;
+          } else {
+            nextStream = null;
+            // failed, null it out so we can find another
+            next = null;
+          }
+        }
+        return false;
+      }
+
+      private boolean advanceToNextSpliterator() {
+        while (next == null) {
+          if (!spliteratorOfStreams.tryAdvance(n -> {
+            if (n != null) {
+              nextStream = n;
+              next = n.spliterator();
+            }
+          })) {
+            return false;
+          }
+        }
+        return true;
+      }
+    });
+
+    result.onClose(a::close);
+    result.onClose(b::close);
+
+    return result;
+  }
+
+  static LongStream empty() {
+    return new EmptyLongStreamSource(null);
+  }
+
+  static LongStream generate(LongSupplier s) {
+    return StreamSupport.longStream(new Spliterators.AbstractLongSpliterator(
+        Long.MAX_VALUE,
+        Spliterator.IMMUTABLE | Spliterator.ORDERED
+    ) {
+      @Override
+      public boolean tryAdvance(LongConsumer action) {
+        action.accept(s.getAsLong());
+        return true;
+      }
+    }, false);
+  }
+
+  static LongStream iterate(long seed, LongUnaryOperator f) {
+    return StreamSupport.longStream(new Spliterators.AbstractLongSpliterator(
+        Long.MAX_VALUE,
+        Spliterator.IMMUTABLE | Spliterator.ORDERED
+    ) {
+      private long next = seed;
+
+      @Override
+      public boolean tryAdvance(LongConsumer action) {
+        action.accept(next);
+        next = f.applyAsLong(next);
+        return true;
+      }
+    }, false);
+  }
+
+  static LongStream of(long... values) {
+    return Arrays.stream(values);
+  }
+
+  static LongStream of(long t) {
+    // TODO consider a splittable that returns only a single value
+    return of(new long[]{t});
+  }
+
+  static LongStream range(long startInclusive, long endExclusive) {
+    if (startInclusive >= endExclusive) {
+      return empty();
+    }
+    return rangeClosed(startInclusive, endExclusive - 1);
+  }
+
+  static LongStream rangeClosed(long startInclusive, long endInclusive) {
+    if (startInclusive > endInclusive) {
+      return empty();
+    }
+    long count = endInclusive - startInclusive + 1;
+
+    return StreamSupport.longStream(new Spliterators.AbstractLongSpliterator(
+        count,
+        Spliterator.IMMUTABLE | Spliterator.SIZED | Spliterator.SUBSIZED | Spliterator.ORDERED
+            | Spliterator.SORTED | Spliterator.DISTINCT
+    ) {
+      private long next = startInclusive;
+
+      @Override
+      public Comparator<? super Long> getComparator() {
+        return null;
+      }
+
+      @Override
+      public boolean tryAdvance(LongConsumer action) {
+        if (next <= endInclusive) {
+          action.accept(next++);
+          return true;
+        }
+        return false;
+      }
+    }, false);
+  }
+
+  /**
+   * See
+   * <a href="https://docs.oracle.com/javase/8/docs/api/java/util/stream/LongStream.Builder.html">
+   * the official Java API doc</a> for details.
+   */
+  public interface Builder extends LongConsumer {
+    @Override
+    void accept(long t);
+
+    default LongStream.Builder add(long t) {
+      accept(t);
+      return this;
+    }
+
+    LongStream build();
+  }
+
+  boolean allMatch(LongPredicate predicate);
+
+  boolean anyMatch(LongPredicate predicate);
+
+  DoubleStream asDoubleStream();
+
+  OptionalDouble average();
+
+  Stream<Long> boxed();
+
+  <R> R collect(Supplier<R> supplier, ObjLongConsumer<R> accumulator, BiConsumer<R, R> combiner);
+
+  long count();
+
+  LongStream distinct();
+
+  LongStream filter(LongPredicate predicate);
+
+  OptionalLong findAny();
+
+  OptionalLong findFirst();
+
+  LongStream flatMap(LongFunction<? extends LongStream> mapper);
+
+  void forEach(LongConsumer action);
+
+  void forEachOrdered(LongConsumer action);
+
+  PrimitiveIterator.OfLong iterator();
+
+  LongStream limit(long maxSize);
+
+  LongStream map(LongUnaryOperator mapper);
+
+  DoubleStream mapToDouble(LongToDoubleFunction mapper);
+
+  IntStream mapToInt(LongToIntFunction mapper);
+
+  <U> Stream<U> mapToObj(LongFunction<? extends U> mapper);
+
+  OptionalLong max();
+
+  OptionalLong min();
+
+  boolean noneMatch(LongPredicate predicate);
+
+  LongStream parallel();
+
+  LongStream peek(LongConsumer action);
+
+  OptionalLong reduce(LongBinaryOperator op);
+
+  long reduce(long identity, LongBinaryOperator op);
+
+  LongStream sequential();
+
+  LongStream skip(long n);
+
+  LongStream sorted();
+
+  Spliterator.OfLong spliterator();
+
+  long sum();
+
+  LongSummaryStatistics summaryStatistics();
+
+  long[] toArray();
+
+  /**
+   * Represents an empty stream, doing nothing for all methods.
+   */
+  static class EmptyLongStreamSource extends TerminatableStream<EmptyLongStreamSource>
+      implements LongStream {
+    public EmptyLongStreamSource(TerminatableStream<?> previous) {
+      super(previous);
+    }
+
+    @Override
+    public LongStream filter(LongPredicate predicate) {
+      throwIfTerminated();
+      return this;
+    }
+
+    @Override
+    public LongStream map(LongUnaryOperator mapper) {
+      throwIfTerminated();
+      return this;
+    }
+
+    @Override
+    public <U> Stream<U> mapToObj(LongFunction<? extends U> mapper) {
+      throwIfTerminated();
+      return new Stream.EmptyStreamSource<U>(this);
+    }
+
+    @Override
+    public IntStream mapToInt(LongToIntFunction mapper) {
+      throwIfTerminated();
+      return new IntStream.EmptyIntStreamSource(this);
+    }
+
+    @Override
+    public DoubleStream mapToDouble(LongToDoubleFunction mapper) {
+      throwIfTerminated();
+      return new DoubleStream.EmptyDoubleStreamSource(this);
+    }
+
+    @Override
+    public LongStream flatMap(LongFunction<? extends LongStream> mapper) {
+      throwIfTerminated();
+      return this;
+    }
+
+    @Override
+    public LongStream distinct() {
+      throwIfTerminated();
+      return this;
+    }
+
+    @Override
+    public LongStream sorted() {
+      throwIfTerminated();
+      return this;
+    }
+
+    @Override
+    public LongStream peek(LongConsumer action) {
+      throwIfTerminated();
+      return this;
+    }
+
+    @Override
+    public LongStream limit(long maxSize) {
+      throwIfTerminated();
+      checkState(maxSize >= 0, "maxSize may not be negative");
+      return this;
+    }
+
+    @Override
+    public LongStream skip(long n) {
+      throwIfTerminated();
+      checkState(n >= 0, "n may not be negative");
+      return this;
+    }
+
+    @Override
+    public void forEach(LongConsumer action) {
+      terminate();
+    }
+
+    @Override
+    public void forEachOrdered(LongConsumer action) {
+      terminate();
+    }
+
+    @Override
+    public long[] toArray() {
+      terminate();
+      return new long[0];
+    }
+
+    @Override
+    public long reduce(long identity, LongBinaryOperator op) {
+      terminate();
+      return identity;
+    }
+
+    @Override
+    public OptionalLong reduce(LongBinaryOperator op) {
+      terminate();
+      return OptionalLong.empty();
+    }
+
+    @Override
+    public <R> R collect(Supplier<R> supplier,
+                         ObjLongConsumer<R> accumulator,
+                         BiConsumer<R, R> combiner) {
+      terminate();
+      return supplier.get();
+    }
+
+    @Override
+    public long sum() {
+      terminate();
+      return 0;
+    }
+
+    @Override
+    public OptionalLong min() {
+      terminate();
+      return OptionalLong.empty();
+    }
+
+    @Override
+    public OptionalLong max() {
+      terminate();
+      return OptionalLong.empty();
+    }
+
+    @Override
+    public long count() {
+      terminate();
+      return 0;
+    }
+
+    @Override
+    public OptionalDouble average() {
+      terminate();
+      return OptionalDouble.empty();
+    }
+
+    @Override
+    public LongSummaryStatistics summaryStatistics() {
+      terminate();
+      return new LongSummaryStatistics();
+    }
+
+    @Override
+    public boolean anyMatch(LongPredicate predicate) {
+      terminate();
+      return false;
+    }
+
+    @Override
+    public boolean allMatch(LongPredicate predicate) {
+      terminate();
+      return true;
+    }
+
+    @Override
+    public boolean noneMatch(LongPredicate predicate) {
+      terminate();
+      return true;
+    }
+
+    @Override
+    public OptionalLong findFirst() {
+      terminate();
+      return OptionalLong.empty();
+    }
+
+    @Override
+    public OptionalLong findAny() {
+      terminate();
+      return OptionalLong.empty();
+    }
+
+    @Override
+    public DoubleStream asDoubleStream() {
+      throwIfTerminated();
+      return new DoubleStream.EmptyDoubleStreamSource(this);
+    }
+
+    @Override
+    public Stream<Long> boxed() {
+      throwIfTerminated();
+      return new Stream.EmptyStreamSource<Long>(this);
+    }
+
+    @Override
+    public LongStream sequential() {
+      throwIfTerminated();
+      return this;
+    }
+
+    @Override
+    public LongStream parallel() {
+      throwIfTerminated();
+      return this;
+    }
+
+    @Override
+    public PrimitiveIterator.OfLong iterator() {
+      return Spliterators.iterator(spliterator());
+    }
+
+    @Override
+    public Spliterator.OfLong spliterator() {
+      terminate();
+      return Spliterators.emptyLongSpliterator();
+    }
+
+    @Override
+    public boolean isParallel() {
+      throwIfTerminated();
+      return false;
+    }
+
+    @Override
+    public LongStream unordered() {
+      throwIfTerminated();
+      return this;
+    }
+  }
+
+  /**
+   * Long to Int map spliterator.
+   */
+  static final class MapToIntSpliterator extends Spliterators.AbstractIntSpliterator {
+    private final LongToIntFunction map;
+    private final Spliterator.OfLong original;
+
+    public MapToIntSpliterator(LongToIntFunction map, Spliterator.OfLong original) {
+      super(
+          original.estimateSize(),
+          original.characteristics() & ~(Spliterator.SORTED | Spliterator.DISTINCT)
+      );
+      checkNotNull(map);
+      this.map = map;
+      this.original = original;
+    }
+
+    @Override
+    public boolean tryAdvance(final IntConsumer action) {
+      return original.tryAdvance((long u) -> action.accept(map.applyAsInt(u)));
+    }
+  }
+
+  /**
+   * Long to Object map spliterator.
+   * @param <T> the type of data in the object spliterator
+   */
+  static final class MapToObjSpliterator<T> extends Spliterators.AbstractSpliterator<T> {
+    private final LongFunction<? extends T> map;
+    private final Spliterator.OfLong original;
+
+    public MapToObjSpliterator(LongFunction<? extends T> map, Spliterator.OfLong original) {
+      super(
+          original.estimateSize(),
+          original.characteristics() & ~(Spliterator.SORTED | Spliterator.DISTINCT)
+      );
+      checkNotNull(map);
+      this.map = map;
+      this.original = original;
+    }
+
+    @Override
+    public boolean tryAdvance(final Consumer<? super T> action) {
+      return original.tryAdvance((long u) -> action.accept(map.apply(u)));
+    }
+  }
+
+  /**
+   * Long to Long map spliterator.
+   */
+  static final class MapToLongSpliterator extends Spliterators.AbstractLongSpliterator {
+    private final LongUnaryOperator map;
+    private final Spliterator.OfLong original;
+
+    public MapToLongSpliterator(LongUnaryOperator map, Spliterator.OfLong original) {
+      super(
+          original.estimateSize(),
+          original.characteristics() & ~(Spliterator.SORTED | Spliterator.DISTINCT)
+      );
+      checkNotNull(map);
+      this.map = map;
+      this.original = original;
+    }
+
+    @Override
+    public boolean tryAdvance(final LongConsumer action) {
+      return original.tryAdvance((long u) -> action.accept(map.applyAsLong(u)));
+    }
+  }
+
+  /**
+   * Long to Double map Spliterator.
+   */
+  static final class MapToDoubleSpliterator extends Spliterators.AbstractDoubleSpliterator {
+    private final LongToDoubleFunction map;
+    private final Spliterator.OfLong original;
+
+    public MapToDoubleSpliterator(LongToDoubleFunction map, Spliterator.OfLong original) {
+      super(
+          original.estimateSize(),
+          original.characteristics() & ~(Spliterator.SORTED | Spliterator.DISTINCT)
+      );
+      checkNotNull(map);
+      this.map = map;
+      this.original = original;
+    }
+
+    @Override
+    public boolean tryAdvance(final DoubleConsumer action) {
+      return original.tryAdvance((long u) -> action.accept(map.applyAsDouble(u)));
+    }
+  }
+
+  /**
+   * Long filter spliterator.
+   */
+  static final class FilterSpliterator extends Spliterators.AbstractLongSpliterator {
+    private final LongPredicate filter;
+    private final Spliterator.OfLong original;
+
+    private boolean found;
+
+    public FilterSpliterator(LongPredicate filter, Spliterator.OfLong original) {
+      super(original.estimateSize(), original.characteristics() & ~Spliterator.SIZED);
+      checkNotNull(filter);
+      this.filter = filter;
+      this.original = original;
+    }
+
+    @Override
+    public Comparator<? super Long> getComparator() {
+      return original.getComparator();
+    }
+
+    @Override
+    public boolean tryAdvance(final LongConsumer action) {
+      found = false;
+      while (!found && original.tryAdvance((long item) -> {
+        if (filter.test(item)) {
+          found = true;
+          action.accept(item);
+        }
+      })) {
+        // do nothing, work is done in tryAdvance
+      }
+
+      return found;
+    }
+  }
+
+  /**
+   * Long skip spliterator.
+   */
+  static final class SkipSpliterator extends Spliterators.AbstractLongSpliterator {
+    private long skip;
+    private final Spliterator.OfLong original;
+
+    public SkipSpliterator(long skip, Spliterator.OfLong original) {
+      super(
+          original.hasCharacteristics(Spliterator.SIZED)
+              ? Math.max(0, original.estimateSize() - skip)
+              : Long.MAX_VALUE,
+          original.characteristics()
+      );
+      this.skip = skip;
+      this.original = original;
+    }
+
+    @Override
+    public Comparator<? super Long> getComparator() {
+      return original.getComparator();
+    }
+
+    @Override
+    public boolean tryAdvance(LongConsumer action) {
+      while (skip > 0) {
+        if (!original.tryAdvance((long ignore) -> { })) {
+          return false;
+        }
+        skip--;
+      }
+      return original.tryAdvance(action);
+    }
+  }
+
+  /**
+   * Long limit spliterator.
+   */
+  static final class LimitSpliterator extends Spliterators.AbstractLongSpliterator {
+    private final long limit;
+    private final Spliterator.OfLong original;
+    private int position = 0;
+
+    public LimitSpliterator(long limit, Spliterator.OfLong original) {
+      super(
+          original.hasCharacteristics(Spliterator.SIZED)
+              ? Math.min(original.estimateSize(), limit)
+              : Long.MAX_VALUE,
+          original.characteristics()
+      );
+      this.limit = limit;
+      this.original = original;
+    }
+
+    @Override
+    public Comparator<? super Long> getComparator() {
+      return original.getComparator();
+    }
+
+    @Override
+    public boolean tryAdvance(LongConsumer action) {
+      if (limit <= position) {
+        return false;
+      }
+      boolean result = original.tryAdvance(action);
+      position++;
+      return result;
+    }
+  }
+
+  /**
+   * Main implementation of LongStream, wrapping a single spliterator, and an optional parent
+   * stream.
+   */
+  static class LongStreamSource extends TerminatableStream<LongStreamSource> implements LongStream {
+    private final Spliterator.OfLong spliterator;
+
+    public LongStreamSource(TerminatableStream<?> previous, Spliterator.OfLong spliterator) {
+      super(previous);
+      this.spliterator = spliterator;
+    }
+
+    // terminals
+
+    @Override
+    public void forEach(LongConsumer action) {
+      forEachOrdered(action);
+    }
+
+    @Override
+    public void forEachOrdered(LongConsumer action) {
+      terminate();
+      spliterator.forEachRemaining(action);
+    }
+
+    @Override
+    public long[] toArray() {
+      terminate();
+      long[] entries = new long[0];
+      // this is legal in js, since the array will be backed by a JS array
+      spliterator.forEachRemaining((long value) -> entries[entries.length] = value);
+
+      return entries;
+    }
+
+    @Override
+    public long reduce(long identity, LongBinaryOperator op) {
+      terminate();
+      ValueConsumer holder = new ValueConsumer();
+      holder.value = identity;
+      spliterator.forEachRemaining((long value) -> {
+        holder.accept(op.applyAsLong(holder.value, value));
+      });
+      return holder.value;
+    }
+
+    @Override
+    public OptionalLong reduce(LongBinaryOperator op) {
+      ValueConsumer holder = new ValueConsumer();
+      if (spliterator.tryAdvance(holder)) {
+        return OptionalLong.of(reduce(holder.value, op));
+      }
+      terminate();
+      return OptionalLong.empty();
+    }
+
+    @Override
+    public <R> R collect(Supplier<R> supplier,
+                         ObjLongConsumer<R> accumulator,
+                         BiConsumer<R, R> combiner) {
+      terminate();
+      final R acc = supplier.get();
+      spliterator.forEachRemaining((long value) -> accumulator.accept(acc, value));
+      return acc;
+    }
+
+    @Override
+    public long sum() {
+      return summaryStatistics().getSum();
+    }
+
+    @Override
+    public OptionalLong min() {
+      terminate();
+      final ValueConsumer holder = new ValueConsumer();
+      if (spliterator.tryAdvance((long value) -> holder.value = value)) {
+        spliterator.forEachRemaining((long value) -> holder.value = Math.min(holder.value, value));
+        return OptionalLong.of(holder.value);
+      }
+      return OptionalLong.empty();
+    }
+
+    @Override
+    public OptionalLong max() {
+      terminate();
+      final ValueConsumer holder = new ValueConsumer();
+      if (spliterator.tryAdvance((long value) -> holder.value = value)) {
+        spliterator.forEachRemaining((long value) -> holder.value = Math.max(holder.value, value));
+        return OptionalLong.of(holder.value);
+      }
+      return OptionalLong.empty();
+    }
+
+    @Override
+    public long count() {
+      terminate();
+      long count = 0;
+      while (spliterator.tryAdvance((long value) -> { })) {
+        count++;
+      }
+      return count;
+    }
+
+    @Override
+    public OptionalDouble average() {
+      LongSummaryStatistics stats = summaryStatistics();
+      if (stats.getCount() == 0) {
+        return OptionalDouble.empty();
+      }
+      return OptionalDouble.of(stats.getAverage());
+    }
+
+    @Override
+    public LongSummaryStatistics summaryStatistics() {
+      return collect(
+          LongSummaryStatistics::new,
+          // TODO switch to a lambda reference once #9340 is fixed
+          (longSummaryStatistics, value) -> longSummaryStatistics.accept(value),
+          LongSummaryStatistics::combine
+      );
+    }
+
+    @Override
+    public boolean anyMatch(LongPredicate predicate) {
+      return filter(predicate).findFirst().isPresent();
+    }
+
+    @Override
+    public boolean allMatch(LongPredicate predicate) {
+      return !anyMatch(predicate.negate());
+    }
+
+    @Override
+    public boolean noneMatch(LongPredicate predicate) {
+      return !anyMatch(predicate);
+    }
+
+    @Override
+    public OptionalLong findFirst() {
+      terminate();
+      ValueConsumer holder = new ValueConsumer();
+      if (spliterator.tryAdvance(holder)) {
+        return OptionalLong.of(holder.value);
+      }
+      return OptionalLong.empty();
+    }
+
+    @Override
+    public OptionalLong findAny() {
+      return findFirst();
+    }
+
+    @Override
+    public PrimitiveIterator.OfLong iterator() {
+      return Spliterators.iterator(spliterator());
+    }
+
+    @Override
+    public Spliterator.OfLong spliterator() {
+      terminate();
+      return spliterator;
+    }
+    // end terminals
+
+    // intermediates
+    @Override
+    public LongStream filter(LongPredicate predicate) {
+      throwIfTerminated();
+      return new LongStreamSource(this, new FilterSpliterator(predicate, spliterator));
+    }
+
+    @Override
+    public LongStream map(LongUnaryOperator mapper) {
+      throwIfTerminated();
+      return new LongStream.LongStreamSource(this, new MapToLongSpliterator(mapper, spliterator));
+    }
+
+    @Override
+    public <U> Stream<U> mapToObj(LongFunction<? extends U> mapper) {
+      throwIfTerminated();
+      return new Stream.StreamSource(this, new MapToObjSpliterator<U>(mapper, spliterator));
+    }
+
+    @Override
+    public IntStream mapToInt(LongToIntFunction mapper) {
+      throwIfTerminated();
+      return new IntStream.IntStreamSource(this, new MapToIntSpliterator(mapper, spliterator));
+    }
+
+    @Override
+    public DoubleStream mapToDouble(LongToDoubleFunction mapper) {
+      throwIfTerminated();
+      return new DoubleStream.DoubleStreamSource(this, new MapToDoubleSpliterator(
+          mapper,
+          spliterator)
+      );
+    }
+
+    @Override
+    public LongStream flatMap(LongFunction<? extends LongStream> mapper) {
+      throwIfTerminated();
+      final Spliterator<? extends LongStream> spliteratorOfStreams = new MapToObjSpliterator<>(
+          mapper,
+          spliterator
+      );
+      return new LongStreamSource(
+          this,
+          new Spliterators.AbstractLongSpliterator(Long.MAX_VALUE, 0
+          ) {
+            LongStream nextStream;
+            Spliterator.OfLong next;
+
+            @Override
+            public boolean tryAdvance(LongConsumer action) {
+              // look for a new spliterator
+              while (advanceToNextSpliterator()) {
+                // if we have one, try to read and use it
+                if (next.tryAdvance(action)) {
+                  return true;
+                } else {
+                  nextStream.close();
+                  nextStream = null;
+                  // failed, null it out so we can find another
+                  next = null;
+                }
+              }
+              return false;
+            }
+
+            private boolean advanceToNextSpliterator() {
+              while (next == null) {
+                if (!spliteratorOfStreams.tryAdvance(n -> {
+                  if (n != null) {
+                    nextStream = n;
+                    next = n.spliterator();
+                  }
+                })) {
+                  return false;
+                }
+              }
+              return true;
+            }
+          });
+    }
+
+    @Override
+    public LongStream distinct() {
+      throwIfTerminated();
+      HashSet<Long> seen = new HashSet<>();
+      return filter(seen::add);
+    }
+
+    @Override
+    public LongStream sorted() {
+      throwIfTerminated();
+      return new LongStreamSource(this, new Spliterators.AbstractLongSpliterator(
+          spliterator.estimateSize(),
+          spliterator.characteristics() | Spliterator.SORTED
+      ) {
+        Spliterator.OfLong ordered = null;
+
+        @Override
+        public Comparator<? super Long> getComparator() {
+          return null;
+        }
+
+        @Override
+        public boolean tryAdvance(LongConsumer action) {
+          if (ordered == null) {
+            long[] list = new long[0];
+            spliterator.forEachRemaining((long item) -> list[list.length] = item);
+            Arrays.sort(list);
+            ordered = Spliterators.spliterator(list, characteristics());
+          }
+          return ordered.tryAdvance(action);
+        }
+      });
+    }
+
+    @Override
+    public LongStream peek(LongConsumer action) {
+      checkNotNull(action);
+      throwIfTerminated();
+      return new LongStreamSource(this, new Spliterators.AbstractLongSpliterator(
+          spliterator.estimateSize(),
+          spliterator.characteristics()
+      ) {
+        @Override
+        public boolean tryAdvance(final LongConsumer innerAction) {
+          return spliterator.tryAdvance(action.andThen(innerAction));
+        }
+      });
+    }
+
+    @Override
+    public LongStream limit(long maxSize) {
+      throwIfTerminated();
+      checkState(maxSize >= 0, "maxSize may not be negative");
+      return new LongStreamSource(this, new LimitSpliterator(maxSize, spliterator));
+    }
+
+    @Override
+    public LongStream skip(long n) {
+      throwIfTerminated();
+      checkState(n >= 0, "n may not be negative");
+      if (n == 0) {
+        return this;
+      }
+      return new LongStreamSource(this, new SkipSpliterator(n, spliterator));
+    }
+
+    @Override
+    public DoubleStream asDoubleStream() {
+      return mapToDouble(x -> (double) x);
+    }
+
+    @Override
+    public Stream<Long> boxed() {
+      return mapToObj(Long::valueOf);
+    }
+
+    @Override
+    public LongStream sequential() {
+      throwIfTerminated();
+      return this;
+    }
+
+    @Override
+    public LongStream parallel() {
+      throwIfTerminated();
+      return this;
+    }
+
+    @Override
+    public boolean isParallel() {
+      throwIfTerminated();
+      return false;
+    }
+
+    @Override
+    public LongStream unordered() {
+      throwIfTerminated();
+      return this;
+    }
+  }
+}
diff --git a/user/super/com/google/gwt/emul/java/util/stream/Stream.java b/user/super/com/google/gwt/emul/java/util/stream/Stream.java
new file mode 100644
index 0000000..b61486d
--- /dev/null
+++ b/user/super/com/google/gwt/emul/java/util/stream/Stream.java
@@ -0,0 +1,1180 @@
+/*
+ * 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.checkCriticalState;
+import static javaemul.internal.InternalPreconditions.checkNotNull;
+import static javaemul.internal.InternalPreconditions.checkState;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Optional;
+import java.util.Spliterator;
+import java.util.Spliterators;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+import java.util.function.BinaryOperator;
+import java.util.function.Consumer;
+import java.util.function.DoubleConsumer;
+import java.util.function.Function;
+import java.util.function.IntConsumer;
+import java.util.function.IntFunction;
+import java.util.function.LongConsumer;
+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;
+import java.util.function.UnaryOperator;
+
+/**
+ * See <a href="https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html">
+ * the official Java API doc</a> for details.
+ *
+ * @param <T> the type of data being streamed
+ */
+public interface Stream<T> extends BaseStream<T, Stream<T>> {
+  /**
+   * Value holder for various stream operations.
+   */
+  static final class ValueConsumer<T> implements Consumer<T> {
+    T value;
+
+    @Override
+    public void accept(T value) {
+      this.value = value;
+    }
+  }
+
+  static <T> Stream.Builder<T> builder() {
+    return new Builder<T>() {
+      private Object[] items = new Object[0];
+
+      @Override
+      public void accept(T t) {
+        checkState(items != null, "Builder already built");
+        items[items.length] = t;
+      }
+
+      @Override
+      @SuppressWarnings("unchecked")
+      public Stream<T> build() {
+        checkCriticalState(items != null, "Builder already built");
+        Stream<T> stream = (Stream<T>) Arrays.stream(items);
+        items = null;
+        return stream;
+      }
+    };
+  }
+
+  static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b) {
+    // This is nearly the same as flatMap, but inlined, wrapped around a single spliterator of
+    // these two objects, and without close() called as the stream progresses. Instead, close is
+    // invoked as part of the resulting stream's own onClose, so that either can fail without
+    // affecting the other, and correctly collecting suppressed exceptions.
+
+    // TODO replace this flatMap-ish spliterator with one that directly combines the two root
+    // streams
+    Spliterator<? extends Stream<? extends T>> spliteratorOfStreams =
+        Arrays.asList(a, b).spliterator();
+    Stream<T> result = new StreamSource<T>(null, new Spliterators.AbstractSpliterator<T>(Long
+        .MAX_VALUE, 0) {
+      Stream<? extends T> nextStream;
+      Spliterator<? extends T> next;
+
+      @Override
+      public boolean tryAdvance(Consumer<? super T> action) {
+        // look for a new spliterator
+        while (advanceToNextSpliterator()) {
+          // if we have one, try to read and use it
+          if (next.tryAdvance(action)) {
+            return true;
+          } else {
+            nextStream = null;
+            // failed, null it out so we can find another
+            next = null;
+          }
+        }
+        return false;
+      }
+
+      private boolean advanceToNextSpliterator() {
+        while (next == null) {
+          if (!spliteratorOfStreams.tryAdvance(n -> {
+            if (n != null) {
+              nextStream = n;
+              next = n.spliterator();
+            }
+          })) {
+            return false;
+          }
+        }
+        return true;
+      }
+    });
+
+    result.onClose(a::close);
+    result.onClose(b::close);
+
+    return result;
+  }
+
+  static <T> Stream<T> empty() {
+    return new EmptyStreamSource<T>(null);
+  }
+
+  static <T> Stream<T> generate(Supplier<T> s) {
+    return StreamSupport.stream(new Spliterators.AbstractSpliterator<T>(Long.MAX_VALUE,
+        Spliterator.IMMUTABLE | Spliterator.ORDERED) {
+      @Override
+      public boolean tryAdvance(Consumer<? super T> action) {
+        action.accept(s.get());
+        return true;
+      }
+    }, false);
+  }
+
+  static <T> Stream<T> iterate(T seed, UnaryOperator<T> f) {
+    return StreamSupport.stream(new Spliterators.AbstractSpliterator<T>(Long.MAX_VALUE,
+        Spliterator.IMMUTABLE | Spliterator.ORDERED) {
+      private T next = seed;
+
+      @Override
+      public boolean tryAdvance(Consumer<? super T> action) {
+        action.accept(next);
+        next = f.apply(next);
+        return true;
+      }
+    }, false);
+  }
+
+  static <T> Stream<T> of(T t) {
+    // TODO consider a splittable that returns only a single value, either for use here or in the
+    //      singleton collection types
+    return Collections.singleton(t).stream();
+  }
+
+  static <T> Stream<T> of(T... values) {
+    return Arrays.stream(values);
+  }
+
+  /**
+   * See <a href="https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.Builder.html">
+   * the official Java API doc</a> for details.
+   */
+  public interface Builder<T> extends Consumer<T> {
+    @Override
+    void accept(T t);
+
+    default Stream.Builder<T> add(T t) {
+      accept(t);
+      return this;
+    }
+
+    Stream<T> build();
+  }
+
+  boolean allMatch(Predicate<? super T> predicate);
+
+  boolean anyMatch(Predicate<? super T> predicate);
+
+  <R,A> R collect(Collector<? super T,A,R> collector);
+
+  <R> R collect(Supplier<R> supplier,
+                BiConsumer<R,? super T> accumulator,
+                BiConsumer<R,R> combiner);
+
+  long count();
+
+  Stream<T> distinct();
+
+  Stream<T> filter(Predicate<? super T> predicate);
+
+  Optional<T> findAny();
+
+  Optional<T> findFirst();
+
+  <R> Stream<R> flatMap(Function<? super T,? extends Stream<? extends R>> mapper);
+
+  DoubleStream flatMapToDouble(Function<? super T,? extends DoubleStream> mapper);
+
+  IntStream flatMapToInt(Function<? super T,? extends IntStream> mapper);
+
+  LongStream flatMapToLong(Function<? super T,? extends LongStream> mapper);
+
+  void forEach(Consumer<? super T> action);
+
+  void forEachOrdered(Consumer<? super T> action);
+
+  Stream<T> limit(long maxSize);
+
+  <R> Stream<R> map(Function<? super T,? extends R> mapper);
+
+  DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper);
+
+  IntStream mapToInt(ToIntFunction<? super T> mapper);
+
+  LongStream mapToLong(ToLongFunction<? super T> mapper);
+
+  Optional<T> max(Comparator<? super T> comparator);
+
+  Optional<T> min(Comparator<? super T> comparator);
+
+  boolean noneMatch(Predicate<? super T> predicate);
+
+  Stream<T> peek(Consumer<? super T> action);
+
+  Optional<T> reduce(BinaryOperator<T> accumulator);
+
+  T reduce(T identity, BinaryOperator<T> accumulator);
+
+  <U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner);
+
+  Stream<T> skip(long n);
+
+  Stream<T> sorted();
+
+  Stream<T> sorted(Comparator<? super T> comparator);
+
+  Object[] toArray();
+
+  <A> A[] toArray(IntFunction<A[]> generator);
+
+  /**
+   * Represents an empty stream, doing nothing for all methods.
+   */
+  static class EmptyStreamSource<T> extends TerminatableStream<EmptyStreamSource<T>>
+      implements Stream<T> {
+
+    public EmptyStreamSource(TerminatableStream<?> previous) {
+      super(previous);
+    }
+
+    @Override
+    public  Stream<T> filter(Predicate<? super T> predicate) {
+      throwIfTerminated();
+      return this;
+    }
+
+    @Override
+    public <R>  Stream<R> map(Function<? super T, ? extends R> mapper) {
+      throwIfTerminated();
+      return (Stream) this;
+    }
+
+    @Override
+    public IntStream mapToInt(ToIntFunction<? super T> mapper) {
+      throwIfTerminated();
+      return new IntStream.EmptyIntStreamSource(this);
+    }
+
+    @Override
+    public LongStream mapToLong(ToLongFunction<? super T> mapper) {
+      throwIfTerminated();
+      return new LongStream.EmptyLongStreamSource(this);
+    }
+
+    @Override
+    public DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper) {
+      throwIfTerminated();
+      return new DoubleStream.EmptyDoubleStreamSource(this);
+    }
+
+    @Override
+    public <R> Stream<R> flatMap(Function<? super T, ? extends  Stream<? extends R>> mapper) {
+      throwIfTerminated();
+      return (Stream) this;
+    }
+
+    @Override
+    public IntStream flatMapToInt(Function<? super T, ? extends IntStream> mapper) {
+      throwIfTerminated();
+      return new IntStream.EmptyIntStreamSource(this);
+    }
+
+    @Override
+    public LongStream flatMapToLong(Function<? super T, ? extends LongStream> mapper) {
+      throwIfTerminated();
+      return new LongStream.EmptyLongStreamSource(this);
+    }
+
+    @Override
+    public DoubleStream flatMapToDouble(Function<? super T, ? extends DoubleStream> mapper) {
+      throwIfTerminated();
+      return new DoubleStream.EmptyDoubleStreamSource(this);
+    }
+
+    @Override
+    public Stream<T> distinct() {
+      throwIfTerminated();
+      return this;
+    }
+
+    @Override
+    public Stream<T> sorted() {
+      throwIfTerminated();
+      return this;
+    }
+
+    @Override
+    public Stream<T> sorted(Comparator<? super T> comparator) {
+      throwIfTerminated();
+      return this;
+    }
+
+    @Override
+    public Stream<T> peek(Consumer<? super T> action) {
+      throwIfTerminated();
+      return this;
+    }
+
+    @Override
+    public Stream<T> limit(long maxSize) {
+      throwIfTerminated();
+      checkState(maxSize >= 0, "maxSize may not be negative");
+      return this;
+    }
+
+    @Override
+    public Stream<T> skip(long n) {
+      throwIfTerminated();
+      checkState(n >= 0, "n may not be negative");
+      return this;
+    }
+
+    @Override
+    public void forEach(Consumer<? super T> action) {
+      terminate();
+      // nothing to do
+    }
+
+    @Override
+    public void forEachOrdered(Consumer<? super T> action) {
+      terminate();
+      // nothing to do
+    }
+
+    @Override
+    public Object[] toArray() {
+      terminate();
+      return new Object[0];
+    }
+
+    @Override
+    public <A> A[] toArray(IntFunction<A[]> generator) {
+      terminate();
+      return generator.apply(0);
+    }
+
+    @Override
+    public T reduce(T identity, BinaryOperator<T> accumulator) {
+      terminate();
+      return identity;
+    }
+
+    @Override
+    public Optional<T> reduce(BinaryOperator<T> accumulator) {
+      terminate();
+      return Optional.empty();
+    }
+
+    @Override
+    public <U> U reduce(U identity,
+                        BiFunction<U, ? super T, U> accumulator,
+                        BinaryOperator<U> combiner) {
+      terminate();
+      return identity;
+    }
+
+    @Override
+    public <R> R collect(Supplier<R> supplier,
+                         BiConsumer<R, ? super T> accumulator,
+                         BiConsumer<R, R> combiner) {
+      terminate();
+      return supplier.get();
+    }
+
+    @Override
+    public <R, A> R collect(Collector<? super T, A, R> collector) {
+      terminate();
+      return collector.finisher().apply(collector.supplier().get());
+    }
+
+    @Override
+    public Optional<T> min(Comparator<? super T> comparator) {
+      terminate();
+      return Optional.empty();
+    }
+
+    @Override
+    public Optional<T> max(Comparator<? super T> comparator) {
+      terminate();
+      return Optional.empty();
+    }
+
+    @Override
+    public long count() {
+      terminate();
+      return 0;
+    }
+
+    @Override
+    public boolean anyMatch(Predicate<? super T> predicate) {
+      terminate();
+      return false;
+    }
+
+    @Override
+    public boolean allMatch(Predicate<? super T> predicate) {
+      terminate();
+      return true;
+    }
+
+    @Override
+    public boolean noneMatch(Predicate<? super T> predicate) {
+      terminate();
+      return true;
+    }
+
+    @Override
+    public Optional<T> findFirst() {
+      terminate();
+      return Optional.empty();
+    }
+
+    @Override
+    public Optional<T> findAny() {
+      terminate();
+      return Optional.empty();
+    }
+
+    @Override
+    public Iterator<T> iterator() {
+      terminate();
+      return Collections.emptyIterator();
+    }
+
+    @Override
+    public Spliterator<T> spliterator() {
+      terminate();
+      return Spliterators.emptySpliterator();
+    }
+
+    @Override
+    public boolean isParallel() {
+      throwIfTerminated();
+      return false;
+    }
+
+    @Override
+    public Stream<T> sequential() {
+      throwIfTerminated();
+      return this;
+    }
+
+    @Override
+    public Stream<T> parallel() {
+      throwIfTerminated();
+      return this;
+    }
+
+    @Override
+    public Stream<T> unordered() {
+      throwIfTerminated();
+      return this;
+    }
+  }
+
+  /**
+   * Object to Object map spliterator.
+   * @param <U> the input type
+   * @param <T> the output type
+   */
+  static final class MapToObjSpliterator<U, T> extends Spliterators.AbstractSpliterator<T> {
+    private final Function<? super U, ? extends T> map;
+    private final Spliterator<U> original;
+
+    public MapToObjSpliterator(Function<? super U, ? extends T> map, Spliterator<U> original) {
+      super(
+          original.estimateSize(),
+          original.characteristics() & ~(Spliterator.SORTED | Spliterator.DISTINCT)
+      );
+      checkNotNull(map);
+      this.map = map;
+      this.original = original;
+    }
+
+    @Override
+    public boolean tryAdvance(final Consumer<? super T> action) {
+      return original.tryAdvance(u -> action.accept(map.apply(u)));
+    }
+  }
+
+  /**
+   * Object to Int map spliterator.
+   * @param <T> the input type
+   */
+  static final class MapToIntSpliterator<T> extends Spliterators.AbstractIntSpliterator {
+    private final ToIntFunction<? super T> map;
+    private final Spliterator<T> original;
+
+    public MapToIntSpliterator(ToIntFunction<? super T> map, Spliterator<T> original) {
+      super(
+          original.estimateSize(),
+          original.characteristics() & ~(Spliterator.SORTED | Spliterator.DISTINCT)
+      );
+      checkNotNull(map);
+      this.map = map;
+      this.original = original;
+    }
+
+    @Override
+    public boolean tryAdvance(final IntConsumer action) {
+      return original.tryAdvance(u -> action.accept(map.applyAsInt(u)));
+    }
+  }
+
+  /**
+   * Object to Long map spliterator.
+   * @param <T> the input type
+   */
+  static final class MapToLongSpliterator<T> extends Spliterators.AbstractLongSpliterator {
+    private final ToLongFunction<? super T> map;
+    private final Spliterator<T> original;
+
+    public MapToLongSpliterator(ToLongFunction<? super T> map, Spliterator<T> original) {
+      super(
+          original.estimateSize(),
+          original.characteristics() & ~(Spliterator.SORTED | Spliterator.DISTINCT)
+      );
+      checkNotNull(map);
+      this.map = map;
+      this.original = original;
+    }
+
+    @Override
+    public boolean tryAdvance(final LongConsumer action) {
+      return original.tryAdvance(u -> action.accept(map.applyAsLong(u)));
+    }
+  }
+
+  /**
+   * Object to Double map spliterator.
+   * @param <T> the input type
+   */
+  static final class MapToDoubleSpliterator<T> extends Spliterators.AbstractDoubleSpliterator {
+    private final ToDoubleFunction<? super T> map;
+    private final Spliterator<T> original;
+
+    public MapToDoubleSpliterator(ToDoubleFunction<? super T> map, Spliterator<T> original) {
+      super(
+          original.estimateSize(),
+          original.characteristics() & ~(Spliterator.SORTED | Spliterator.DISTINCT)
+      );
+      checkNotNull(map);
+      this.map = map;
+      this.original = original;
+    }
+
+    @Override
+    public boolean tryAdvance(final DoubleConsumer action) {
+      return original.tryAdvance(u -> action.accept(map.applyAsDouble(u)));
+    }
+  }
+
+  /**
+   * Object filter spliterator.
+   * @param <T> the type of data to iterate over
+   */
+  static final class FilterSpliterator<T> extends Spliterators.AbstractSpliterator<T> {
+    private final Predicate<? super T> filter;
+    private final Spliterator<T> original;
+
+    private boolean found;
+
+    public FilterSpliterator(Predicate<? super T> filter, Spliterator<T> original) {
+      super(original.estimateSize(), original.characteristics() & ~Spliterator.SIZED);
+      checkNotNull(filter);
+      this.filter = filter;
+      this.original = original;
+    }
+
+    @Override
+    public Comparator<? super T> getComparator() {
+      return original.getComparator();
+    }
+
+    @Override
+    public boolean tryAdvance(final Consumer<? super T> action) {
+      found = false;
+      while (!found && original.tryAdvance(item -> {
+        if (filter.test(item)) {
+          found = true;
+          action.accept(item);
+        }
+      })) {
+        // do nothing, work is done in tryAdvance
+      }
+
+      return found;
+    }
+  }
+
+  /**
+   * Object skip spliterator.
+   * @param <T> the type of data to iterate over
+   */
+  static final class SkipSpliterator<T> extends Spliterators.AbstractSpliterator<T> {
+    private long skip;
+    private final Spliterator<T> original;
+
+    public SkipSpliterator(long skip, Spliterator<T> original) {
+      super(
+          original.hasCharacteristics(Spliterator.SIZED)
+              ? Math.max(0, original.estimateSize() - skip)
+              : Long.MAX_VALUE,
+          original.characteristics()
+      );
+      this.skip = skip;
+      this.original = original;
+    }
+
+    @Override
+    public Comparator<? super T> getComparator() {
+      return original.getComparator();
+    }
+
+    @Override
+    public boolean tryAdvance(Consumer<? super T> action) {
+      while (skip > 0) {
+        if (!original.tryAdvance(ignore -> { })) {
+          return false;
+        }
+        skip--;
+      }
+      return original.tryAdvance(action);
+    }
+  }
+
+  /**
+   * Object limit spliterator.
+   * @param <T> the type of data to iterate over
+   */
+  static final class LimitSpliterator<T> extends Spliterators.AbstractSpliterator<T> {
+    private final long limit;
+    private final Spliterator<T> original;
+    private int position = 0;
+
+    public LimitSpliterator(long limit, Spliterator<T> original) {
+      super(
+          original.hasCharacteristics(Spliterator.SIZED)
+              ? Math.min(original.estimateSize(), limit)
+              : Long.MAX_VALUE,
+          original.characteristics()
+      );
+      this.limit = limit;
+      this.original = original;
+    }
+
+    @Override
+    public Comparator<? super T> getComparator() {
+      return original.getComparator();
+    }
+
+    @Override
+    public boolean tryAdvance(Consumer<? super T> action) {
+      if (limit <= position) {
+        return false;
+      }
+      boolean result = original.tryAdvance(action);
+      position++;
+      return result;
+    }
+  }
+
+  /**
+   * Main implementation of Stream, wrapping a single spliterator and an optional parent stream.
+   * @param <T>
+   */
+  static class StreamSource<T> extends TerminatableStream<StreamSource<T>> implements Stream<T> {
+    private final Spliterator<T> spliterator;
+
+    public StreamSource(TerminatableStream<?> prev, Spliterator<T> spliterator) {
+      super(prev);
+      this.spliterator = spliterator;
+    }
+
+    // terminal
+    @Override
+    public Spliterator<T> spliterator() {
+      terminate();
+      return spliterator;
+    }
+
+    @Override
+    public Iterator<T> iterator() {
+      return Spliterators.iterator(spliterator());
+    }
+
+    @Override
+    public long count() {
+      terminate();
+      long count = 0;
+      while (spliterator.tryAdvance(a -> { })) {
+        count++;
+      }
+      return count;
+    }
+
+    @Override
+    public void forEach(Consumer<? super T> action) {
+      forEachOrdered(action);
+    }
+
+    @Override
+    public void forEachOrdered(Consumer<? super T> action) {
+      terminate();
+      spliterator.forEachRemaining(action);
+    }
+
+    @Override
+    public Object[] toArray() {
+      return toArray(Object[]::new);
+    }
+
+    @Override
+    public <A> A[] toArray(IntFunction<A[]> generator) {
+      List<T> collected = collect(Collectors.toList());
+      return collected.toArray(generator.apply(collected.size()));
+    }
+
+    @Override
+    public <R> R collect(Supplier<R> supplier,
+                         BiConsumer<R, ? super T> accumulator,
+                         BiConsumer<R, R> combiner) {
+      return collect(Collector.of(supplier, accumulator, (a, b) -> {
+        combiner.accept(a, b);
+        return a;
+      }));
+    }
+
+    @Override
+    public <R, A> R collect(final Collector<? super T, A, R> collector) {
+      return collector.finisher().apply(reduce(
+          collector.supplier().get(),
+          (a, t) -> {
+            collector.accumulator().accept(a, t);
+            return a;
+          },
+          collector.combiner()
+      ));
+    }
+
+    @Override
+    public Optional<T> findFirst() {
+      terminate();
+      ValueConsumer<T> holder = new ValueConsumer<T>();
+      if (spliterator.tryAdvance(holder)) {
+        return Optional.of(holder.value);
+      }
+      return Optional.empty();
+    }
+
+    @Override
+    public Optional<T> findAny() {
+      return findFirst();
+    }
+
+    @Override
+    public boolean anyMatch(Predicate<? super T> predicate) {
+      return filter(predicate).findFirst().isPresent();
+    }
+
+    @Override
+    public boolean allMatch(final Predicate<? super T> predicate) {
+      return !anyMatch(predicate.negate());
+    }
+
+    @Override
+    public boolean noneMatch(final Predicate<? super T> predicate) {
+      return !anyMatch(predicate);
+    }
+
+    @Override
+    public Optional<T> min(final Comparator<? super T> comparator) {
+      return reduce(BinaryOperator.minBy(comparator));
+    }
+
+    @Override
+    public Optional<T> max(final Comparator<? super T> comparator) {
+      return reduce(BinaryOperator.maxBy(comparator));
+    }
+
+    @Override
+    public T reduce(T identity, BinaryOperator<T> accumulator) {
+      return reduce(identity, accumulator, accumulator);
+    }
+
+    @Override
+    public Optional<T> reduce(BinaryOperator<T> accumulator) {
+      ValueConsumer<T> consumer = new ValueConsumer<T>();
+      if (!spliterator.tryAdvance(consumer)) {
+        terminate();
+        return Optional.empty();
+      }
+      return Optional.of(reduce(consumer.value, accumulator));
+    }
+
+    // combiner is ignored, since we don't parallelize
+    @Override
+    public <U> U reduce(U identity,
+                        BiFunction<U, ? super T, U> accumulator,
+                        BinaryOperator<U> combiner) {
+      terminate();
+      final ValueConsumer<U> consumer = new ValueConsumer<U>();
+      consumer.value = identity;
+      spliterator.forEachRemaining(item -> {
+        consumer.accept(accumulator.apply(consumer.value, item));
+      });
+      return consumer.value;
+    }
+    // end terminal
+
+    // intermediate
+    @Override
+    public Stream<T> filter(Predicate<? super T> predicate) {
+      throwIfTerminated();
+      return new StreamSource<>(this, new FilterSpliterator<>(predicate, spliterator));
+    }
+
+    @Override
+    public <R> Stream<R> map(Function<? super T, ? extends R> mapper) {
+      throwIfTerminated();
+      return new StreamSource<>(this, new MapToObjSpliterator<>(mapper, spliterator));
+    }
+
+    @Override
+    public IntStream mapToInt(ToIntFunction<? super T> mapper) {
+      throwIfTerminated();
+      return new IntStream.IntStreamSource(this, new MapToIntSpliterator<>(mapper, spliterator));
+    }
+
+    @Override
+    public LongStream mapToLong(ToLongFunction<? super T> mapper) {
+      throwIfTerminated();
+      return new LongStream.LongStreamSource(this, new MapToLongSpliterator<>(mapper, spliterator));
+    }
+
+    @Override
+    public DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper) {
+      throwIfTerminated();
+      return new DoubleStream.DoubleStreamSource(
+          this,
+          new MapToDoubleSpliterator<>(mapper, spliterator)
+      );
+    }
+
+    @Override
+    public <R> Stream<R> flatMap(final Function<? super T, ? extends Stream<? extends R>> mapper) {
+      throwIfTerminated();
+      final Spliterator<? extends Stream<? extends R>> spliteratorOfStreams =
+          new MapToObjSpliterator<>(mapper, spliterator);
+      return new StreamSource<R>(this, new Spliterators.AbstractSpliterator<R>(Long.MAX_VALUE, 0) {
+        Stream<? extends R> nextStream;
+        Spliterator<? extends R> next;
+
+        @Override
+        public boolean tryAdvance(Consumer<? super R> action) {
+          // look for a new spliterator
+          while (advanceToNextSpliterator()) {
+            // if we have one, try to read and use it
+            if (next.tryAdvance(action)) {
+              return true;
+            } else {
+              nextStream.close();
+              nextStream = null;
+              // failed, null it out so we can find another
+              next = null;
+            }
+          }
+          return false;
+        }
+
+        private boolean advanceToNextSpliterator() {
+          while (next == null) {
+            if (!spliteratorOfStreams.tryAdvance(n -> {
+              if (n != null) {
+                nextStream = n;
+                next = n.spliterator();
+              }
+            })) {
+              return false;
+            }
+          }
+          return true;
+        }
+      });
+    }
+
+    @Override
+    public IntStream flatMapToInt(Function<? super T, ? extends IntStream> mapper) {
+      throwIfTerminated();
+      final Spliterator<? extends IntStream> spliteratorOfStreams =
+          new MapToObjSpliterator<>(mapper, spliterator);
+      return new IntStream.IntStreamSource(
+          this,
+          new Spliterators.AbstractIntSpliterator(Long.MAX_VALUE, 0) {
+            IntStream nextStream;
+            Spliterator.OfInt next;
+
+            @Override
+            public boolean tryAdvance(IntConsumer action) {
+              // look for a new spliterator
+              while (advanceToNextSpliterator()) {
+                // if we have one, try to read and use it
+                if (next.tryAdvance(action)) {
+                  return true;
+                } else {
+                  nextStream.close();
+                  nextStream = null;
+                  // failed, null it out so we can find another
+                  next = null;
+                }
+              }
+              return false;
+            }
+
+            private boolean advanceToNextSpliterator() {
+              while (next == null) {
+                if (!spliteratorOfStreams.tryAdvance(n -> {
+                  if (n != null) {
+                    nextStream = n;
+                    next = n.spliterator();
+                  }
+                })) {
+                  return false;
+                }
+              }
+              return true;
+            }
+          }
+      );
+    }
+
+    @Override
+    public LongStream flatMapToLong(Function<? super T, ? extends LongStream> mapper) {
+      throwIfTerminated();
+      final Spliterator<? extends LongStream> spliteratorOfStreams =
+          new MapToObjSpliterator<>(mapper, spliterator);
+      return new LongStream.LongStreamSource(
+          this,
+          new Spliterators.AbstractLongSpliterator(Long.MAX_VALUE, 0) {
+            LongStream nextStream;
+            Spliterator.OfLong next;
+
+            @Override
+            public boolean tryAdvance(LongConsumer action) {
+              // look for a new spliterator
+              while (advanceToNextSpliterator()) {
+                // if we have one, try to read and use it
+                if (next.tryAdvance(action)) {
+                  return true;
+                } else {
+                  nextStream.close();
+                  nextStream = null;
+                  // failed, null it out so we can find another
+                  next = null;
+                }
+              }
+              return false;
+            }
+
+            private boolean advanceToNextSpliterator() {
+              while (next == null) {
+                if (!spliteratorOfStreams.tryAdvance(n -> {
+                  if (n != null) {
+                    nextStream = n;
+                    next = n.spliterator();
+                  }
+                })) {
+                  return false;
+                }
+              }
+              return true;
+            }
+          }
+      );
+    }
+
+    @Override
+    public DoubleStream flatMapToDouble(Function<? super T, ? extends DoubleStream> mapper) {
+      throwIfTerminated();
+      final Spliterator<? extends DoubleStream> spliteratorOfStreams =
+          new MapToObjSpliterator<>(mapper, spliterator);
+      return new DoubleStream.DoubleStreamSource(
+          this,
+          new Spliterators.AbstractDoubleSpliterator(Long.MAX_VALUE, 0) {
+            DoubleStream nextStream;
+            Spliterator.OfDouble next;
+
+            @Override
+            public boolean tryAdvance(DoubleConsumer action) {
+              // look for a new spliterator
+              while (advanceToNextSpliterator()) {
+                // if we have one, try to read and use it
+                if (next.tryAdvance(action)) {
+                  return true;
+                } else {
+                  nextStream.close();
+                  nextStream = null;
+                  // failed, null it out so we can find another
+                  next = null;
+                }
+              }
+              return false;
+            }
+
+            private boolean advanceToNextSpliterator() {
+              while (next == null) {
+                if (!spliteratorOfStreams.tryAdvance(n -> {
+                  if (n != null) {
+                    nextStream = n;
+                    next = n.spliterator();
+                  }
+                })) {
+                  return false;
+                }
+              }
+              return true;
+            }
+          }
+      );
+    }
+
+    @Override
+    public Stream<T> distinct() {
+      throwIfTerminated();
+      HashSet<T> seen = new HashSet<>();
+      return filter(seen::add);
+    }
+
+    @Override
+    public Stream<T> sorted() {
+      throwIfTerminated();
+      Comparator<T> c = (Comparator) Comparator.naturalOrder();
+      return sorted(c);
+    }
+
+    @Override
+    public Stream<T> sorted(final Comparator<? super T> comparator) {
+      throwIfTerminated();
+      return new StreamSource<>(
+          this,
+          new Spliterators.AbstractSpliterator<T>(
+              spliterator.estimateSize(),
+              spliterator.characteristics() | Spliterator.SORTED
+          ) {
+            Spliterator<T> ordered = null;
+
+            @Override
+            public Comparator<? super T> getComparator() {
+              return comparator == Comparator.naturalOrder() ? null : comparator;
+            }
+
+            @Override
+            public boolean tryAdvance(Consumer<? super T> action) {
+              if (ordered == null) {
+                List<T> list = new ArrayList<>();
+                spliterator.forEachRemaining(list::add);
+                Collections.sort(list, comparator);
+                ordered = list.spliterator();
+              }
+              return ordered.tryAdvance(action);
+            }
+          }
+      );
+    }
+
+    @Override
+    public Stream<T> peek(final Consumer<? super T> action) {
+      checkNotNull(action);
+      throwIfTerminated();
+      return new StreamSource<>(
+          this,
+          new Spliterators.AbstractSpliterator<T>(
+              spliterator.estimateSize(),
+              spliterator.characteristics()
+          ) {
+            @Override
+            public boolean tryAdvance(final Consumer<? super T> innerAction) {
+              return spliterator.tryAdvance(item -> {
+                action.accept(item);
+                innerAction.accept(item);
+              });
+            }
+          }
+      );
+    }
+
+    @Override
+    public Stream<T> limit(long maxSize) {
+      throwIfTerminated();
+      checkState(maxSize >= 0, "maxSize may not be negative");
+      return new StreamSource<>(this, new LimitSpliterator<>(maxSize, spliterator));
+    }
+
+    @Override
+    public Stream<T> skip(long n) {
+      throwIfTerminated();
+      checkState(n >= 0, "n may not be negative");
+      if (n == 0) {
+        return this;
+      }
+      return new StreamSource<>(this, new SkipSpliterator<>(n, spliterator));
+    }
+
+    @Override
+    public boolean isParallel() {
+      throwIfTerminated();
+      return false;
+    }
+
+    @Override
+    public Stream<T> sequential() {
+      throwIfTerminated();
+      return this;
+    }
+
+    @Override
+    public Stream<T> parallel() {
+      throwIfTerminated();
+      // do nothing, no such thing as gwt+parallel
+      return this;
+    }
+
+    @Override
+    public Stream<T> unordered() {
+      throwIfTerminated();
+      return this;
+    }
+  }
+}
diff --git a/user/super/com/google/gwt/emul/java/util/stream/StreamSupport.java b/user/super/com/google/gwt/emul/java/util/stream/StreamSupport.java
new file mode 100644
index 0000000..12b34e3
--- /dev/null
+++ b/user/super/com/google/gwt/emul/java/util/stream/StreamSupport.java
@@ -0,0 +1,84 @@
+/*
+ * 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.Spliterator;
+import java.util.function.Supplier;
+
+/**
+ * See <a href="https://docs.oracle.com/javase/8/docs/api/java/util/stream/StreamSupport.html">
+ * the official Java API doc</a> for details.
+ */
+public final class StreamSupport {
+
+  public static DoubleStream doubleStream(Spliterator.OfDouble spliterator, boolean parallel) {
+    return new DoubleStream.DoubleStreamSource(null, spliterator);
+  }
+
+  public static DoubleStream doubleStream(Supplier<? extends Spliterator.OfDouble> supplier,
+                                          int characteristics,
+                                          boolean parallel) {
+    // TODO this is somewhat convoluted, and would be better served by a lazy singleton spliterator
+    return Stream.of(supplier).map(Supplier::get).flatMapToDouble(doubleSpliterator -> {
+      return doubleStream(doubleSpliterator, parallel);
+    });
+  }
+
+  public static IntStream intStream(Spliterator.OfInt spliterator, boolean parallel) {
+    return new IntStream.IntStreamSource(null, spliterator);
+  }
+
+  public static IntStream intStream(Supplier<? extends Spliterator.OfInt> supplier,
+                                    int characteristics,
+                                    boolean parallel) {
+    // TODO this is somewhat convoluted, and would be better served by a lazy singleton spliterator
+    return Stream.of(supplier).map(Supplier::get).flatMapToInt(intSpliterator -> {
+      return intStream(intSpliterator, parallel);
+    });
+  }
+
+  public static LongStream longStream(Spliterator.OfLong spliterator, boolean parallel) {
+    return new LongStream.LongStreamSource(null, spliterator);
+  }
+
+  public static LongStream longStream(Supplier<? extends Spliterator.OfLong> supplier,
+                                      int characteristics,
+                                      final boolean parallel) {
+    // TODO this is somewhat convoluted, and would be better served by a lazy singleton spliterator
+    return Stream.of(supplier).map(Supplier::get).flatMapToLong(longSpliterator -> {
+      return longStream(longSpliterator, parallel);
+    });
+  }
+
+  public static <T> Stream<T> stream(Spliterator<T> spliterator, boolean parallel) {
+    return new Stream.StreamSource<T>(null, spliterator);
+  }
+
+  public static <T> Stream<T> stream(Supplier<? extends Spliterator<T>> supplier,
+                                     int characteristics,
+                                     final boolean parallel) {
+    // TODO this is somewhat convoluted, and would be better served by a lazy singleton spliterator
+    return Stream.of(supplier).map(Supplier::get).flatMap(spliterator -> {
+      return stream(spliterator, parallel);
+    });
+  }
+
+  private StreamSupport() {
+    // prevent instantiation
+  }
+
+}
diff --git a/user/super/com/google/gwt/emul/java/util/stream/TerminatableStream.java b/user/super/com/google/gwt/emul/java/util/stream/TerminatableStream.java
new file mode 100644
index 0000000..018331f
--- /dev/null
+++ b/user/super/com/google/gwt/emul/java/util/stream/TerminatableStream.java
@@ -0,0 +1,102 @@
+/*
+ * 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.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+
+// package protected, as not part of jre
+class TerminatableStream<T extends TerminatableStream<T>> {
+  // root-only fields, ignored for non-root instances
+  private boolean terminated = false;
+  private final List<Runnable> onClose;
+
+  private final TerminatableStream<?> root;
+
+  public TerminatableStream(TerminatableStream<?> previous) {
+    if (previous == null) {
+      root = null;
+      onClose = new ArrayList<>();
+    } else {
+      root = previous;
+      onClose = null;
+    }
+  }
+
+  void throwIfTerminated() {
+    if (root != null) {
+      root.throwIfTerminated();
+    } else if (terminated) {
+      throw new IllegalStateException("Stream already terminated, can't be modified or used");
+    }
+  }
+
+  // note that not all terminals directly call this, but they must use it indirectly
+  void terminate() {
+    if (root == null) {
+      throwIfTerminated();
+      terminated = true;
+    } else {
+      root.terminate();
+    }
+  }
+
+  public T onClose(Runnable closeHandler) {
+    if (root == null) {
+      onClose.add(closeHandler);
+    } else {
+      root.onClose(closeHandler);
+    }
+
+    return (T) this;
+  }
+
+  public void close() {
+    if (root == null) {
+      terminated = true;
+
+      // TODO this might not be quite right, make sure that it behaves the same way when throwing
+      //      the same exception multiple times as JDK does.
+      LinkedHashSet<Throwable> throwables = new LinkedHashSet<>();
+      onClose.forEach((runnable) -> {
+        try {
+          runnable.run();
+        } catch (Throwable e) {
+          throwables.add(e);
+        }
+      });
+      onClose.clear();
+      if (!throwables.isEmpty()) {
+        Iterator<Throwable> iterator = throwables.iterator();
+        Throwable outer = iterator.next();
+        iterator.forEachRemaining(outer::addSuppressed);
+
+        if (outer instanceof RuntimeException) {
+          throw (RuntimeException) outer;
+        }
+        if (outer instanceof Error) {
+          throw (Error) outer;
+        }
+        assert false : "Couldn't have caught this exception from a Runnable! " + outer;
+      }
+    } else {
+      root.close();
+    }
+  }
+}
\ No newline at end of file
diff --git a/user/test/com/google/gwt/emultest/EmulJava8Suite.java b/user/test/com/google/gwt/emultest/EmulJava8Suite.java
index e898b30..572eb86 100644
--- a/user/test/com/google/gwt/emultest/EmulJava8Suite.java
+++ b/user/test/com/google/gwt/emultest/EmulJava8Suite.java
@@ -43,6 +43,11 @@
 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.emultest.java8.util.stream.DoubleStreamTest;
+import com.google.gwt.emultest.java8.util.stream.IntStreamTest;
+import com.google.gwt.emultest.java8.util.stream.LongStreamTest;
+import com.google.gwt.emultest.java8.util.stream.StreamSupportTest;
+import com.google.gwt.emultest.java8.util.stream.StreamTest;
 import com.google.gwt.junit.tools.GWTTestSuite;
 
 import junit.framework.Test;
@@ -90,6 +95,11 @@
 
     //-- java.util.stream
     suite.addTestSuite(CollectorsTest.class);
+    suite.addTestSuite(DoubleStreamTest.class);
+    suite.addTestSuite(IntStreamTest.class);
+    suite.addTestSuite(LongStreamTest.class);
+    suite.addTestSuite(StreamTest.class);
+    suite.addTestSuite(StreamSupportTest.class);
 
     return suite;
   }
diff --git a/user/test/com/google/gwt/emultest/java/util/EmulTestBase.java b/user/test/com/google/gwt/emultest/java/util/EmulTestBase.java
index a3babcc..c1fc5de 100644
--- a/user/test/com/google/gwt/emultest/java/util/EmulTestBase.java
+++ b/user/test/com/google/gwt/emultest/java/util/EmulTestBase.java
@@ -17,6 +17,7 @@
 
 import com.google.gwt.junit.client.GWTTestCase;
 
+import java.util.Arrays;
 import java.util.List;
 
 /**
@@ -41,6 +42,21 @@
     }
   }
 
+  public static void assertEquals(int[] expected, int[] actual) {
+    assertTrue("expected: " + Arrays.toString(expected) + ", actual: " + Arrays.toString(actual),
+        Arrays.equals(expected, actual));
+  }
+
+  public static void assertEquals(long[] expected, long[] actual) {
+    assertTrue("expected: " + Arrays.toString(expected) + ", actual: " + Arrays.toString(actual),
+        Arrays.equals(expected, actual));
+  }
+
+  public static void assertEquals(double[] expected, double[] actual) {
+    assertTrue("expected: " + Arrays.toString(expected) + ", actual: " + Arrays.toString(actual),
+        Arrays.equals(expected, actual));
+  }
+
   @Override
   public String getModuleName() {
     return "com.google.gwt.emultest.EmulSuite";
diff --git a/user/test/com/google/gwt/emultest/java8/util/ArraysTest.java b/user/test/com/google/gwt/emultest/java8/util/ArraysTest.java
index 3a4d42b..ffb42dd 100644
--- a/user/test/com/google/gwt/emultest/java8/util/ArraysTest.java
+++ b/user/test/com/google/gwt/emultest/java8/util/ArraysTest.java
@@ -135,4 +135,156 @@
     Arrays.setAll(array, i -> "" + (i + 1));
     assertEquals(new String[]{"1", "2", "3"}, array);
   }
+
+  public void testArrayStreamInt() {
+    int[] six = {1, 2, 3, 4, 5, 6};
+
+    // zero entries
+    assertEquals(new int[]{}, Arrays.stream(six, 0, 0).toArray());
+    assertEquals(new int[]{}, Arrays.stream(six, 1, 1).toArray());
+
+    // single entry
+    assertEquals(new int[]{1}, Arrays.stream(six, 0, 1).toArray());
+    assertEquals(new int[]{2}, Arrays.stream(six, 1, 2).toArray());
+
+    // multiple entries
+    assertEquals(new int[]{1, 2, 3}, Arrays.stream(six, 0, 3).toArray());
+    assertEquals(new int[]{4, 5, 6}, Arrays.stream(six, 3, 6).toArray());
+
+    try {
+      // start < 0
+      Arrays.stream(six, -1, 1);
+      fail("expected aioobe");
+    } catch (ArrayIndexOutOfBoundsException expected) {
+      // expected
+    }
+    try {
+      // end < start
+      Arrays.stream(six, 2, 1);
+      fail("expected aioobe");
+    } catch (ArrayIndexOutOfBoundsException expected) {
+      // expected
+    }
+    try {
+      // end > size
+      Arrays.stream(six, 0, 7);
+      fail("expected aioobe");
+    } catch (ArrayIndexOutOfBoundsException expected) {
+      // expected
+    }
+  }
+
+  public void testArrayStreamLong() {
+    long[] six = {1, 2, 3, 4, 5, 6};
+
+    // zero entries
+    assertEquals(new long[]{}, Arrays.stream(six, 0, 0).toArray());
+    assertEquals(new long[]{}, Arrays.stream(six, 1, 1).toArray());
+
+    // single entry
+    assertEquals(new long[]{1}, Arrays.stream(six, 0, 1).toArray());
+    assertEquals(new long[]{2}, Arrays.stream(six, 1, 2).toArray());
+
+    // multiple entries
+    assertEquals(new long[]{1, 2, 3}, Arrays.stream(six, 0, 3).toArray());
+    assertEquals(new long[]{4, 5, 6}, Arrays.stream(six, 3, 6).toArray());
+
+    try {
+      // start < 0
+      Arrays.stream(six, -1, 1);
+      fail("expected aioobe");
+    } catch (ArrayIndexOutOfBoundsException expected) {
+      // expected
+    }
+    try {
+      // end < start
+      Arrays.stream(six, 2, 1);
+      fail("expected aioobe");
+    } catch (ArrayIndexOutOfBoundsException expected) {
+      // expected
+    }
+    try {
+      // end > size
+      Arrays.stream(six, 0, 7);
+      fail("expected aioobe");
+    } catch (ArrayIndexOutOfBoundsException expected) {
+      // expected
+    }
+  }
+
+  public void testArrayStreamDouble() {
+    double[] six = {1, 2, 3, 4, 5, 6};
+
+    // zero entries
+    assertEquals(new double[]{}, Arrays.stream(six, 0, 0).toArray());
+    assertEquals(new double[]{}, Arrays.stream(six, 1, 1).toArray());
+
+    // single entry
+    assertEquals(new double[]{1}, Arrays.stream(six, 0, 1).toArray());
+    assertEquals(new double[]{2}, Arrays.stream(six, 1, 2).toArray());
+
+    // multiple entries
+    assertEquals(new double[]{1, 2, 3}, Arrays.stream(six, 0, 3).toArray());
+    assertEquals(new double[]{4, 5, 6}, Arrays.stream(six, 3, 6).toArray());
+
+    try {
+      // start < 0
+      Arrays.stream(six, -1, 1);
+      fail("expected aioobe");
+    } catch (ArrayIndexOutOfBoundsException expected) {
+      // expected
+    }
+    try {
+      // end < start
+      Arrays.stream(six, 2, 1);
+      fail("expected aioobe");
+    } catch (ArrayIndexOutOfBoundsException expected) {
+      // expected
+    }
+    try {
+      // end > size
+      Arrays.stream(six, 0, 7);
+      fail("expected aioobe");
+    } catch (ArrayIndexOutOfBoundsException expected) {
+      // expected
+    }
+  }
+
+  public void testArrayStreamObject() {
+    String[] six = {"1", "2", "3", "4", "5", "6"};
+
+    // zero entries
+    assertEquals(new String[]{}, Arrays.stream(six, 0, 0).toArray());
+    assertEquals(new String[]{}, Arrays.stream(six, 1, 1).toArray());
+
+    // single entry
+    assertEquals(new String[]{"1"}, Arrays.stream(six, 0, 1).toArray());
+    assertEquals(new String[]{"2"}, Arrays.stream(six, 1, 2).toArray());
+
+    // multiple entries
+    assertEquals(new String[]{"1", "2", "3"}, Arrays.stream(six, 0, 3).toArray());
+    assertEquals(new String[]{"4", "5", "6"}, Arrays.stream(six, 3, 6).toArray());
+
+    try {
+      // start < 0
+      Arrays.stream(six, -1, 1);
+      fail("expected aioobe");
+    } catch (ArrayIndexOutOfBoundsException expected) {
+      // expected
+    }
+    try {
+      // end < start
+      Arrays.stream(six, 2, 1);
+      fail("expected aioobe");
+    } catch (ArrayIndexOutOfBoundsException expected) {
+      // expected
+    }
+    try {
+      // end > size
+      Arrays.stream(six, 0, 7);
+      fail("expected aioobe");
+    } catch (ArrayIndexOutOfBoundsException expected) {
+      // expected
+    }
+  }
 }
diff --git a/user/test/com/google/gwt/emultest/java8/util/stream/DoubleStreamTest.java b/user/test/com/google/gwt/emultest/java8/util/stream/DoubleStreamTest.java
new file mode 100644
index 0000000..fb0cc17
--- /dev/null
+++ b/user/test/com/google/gwt/emultest/java8/util/stream/DoubleStreamTest.java
@@ -0,0 +1,443 @@
+/*
+ * 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 com.google.gwt.emultest.java.util.EmulTestBase;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.DoubleSummaryStatistics;
+import java.util.Iterator;
+import java.util.List;
+import java.util.OptionalDouble;
+import java.util.Spliterator;
+import java.util.function.DoubleSupplier;
+import java.util.function.Supplier;
+import java.util.stream.DoubleStream;
+import java.util.stream.Stream;
+
+/**
+ * Tests {@link DoubleStream}.
+ */
+public class DoubleStreamTest extends EmulTestBase {
+
+  public void testEmptyStream() {
+    DoubleStream empty = DoubleStream.empty();
+    assertEquals(0, empty.count());
+    try {
+      empty.count();
+      fail("second terminal operation should have thrown IllegalStateEx");
+    } catch (IllegalStateException expected) {
+      // expected
+    }
+
+    assertEquals(0, DoubleStream.empty().limit(2).toArray().length);
+    assertEquals(0L, DoubleStream.empty().count());
+    assertEquals(0L, DoubleStream.empty().limit(2).count());
+
+    assertFalse(DoubleStream.empty().findFirst().isPresent());
+    assertFalse(DoubleStream.empty().findAny().isPresent());
+    assertFalse(DoubleStream.empty().max().isPresent());
+    assertFalse(DoubleStream.empty().min().isPresent());
+    assertTrue(DoubleStream.empty().noneMatch(item -> true));
+    assertTrue(DoubleStream.empty().allMatch(item -> true));
+    assertFalse(DoubleStream.empty().anyMatch(item -> true));
+    assertEquals(new double[0], DoubleStream.empty().toArray());
+  }
+
+  public void testStreamOfOne() {
+    Supplier<DoubleStream> one = () -> DoubleStream.of(1);
+    assertEquals(new double[]{1d}, one.get().toArray());
+    assertEquals(1L, one.get().count());
+    assertEquals(1d, one.get().findFirst().getAsDouble(), 0d);
+    assertEquals(1d, one.get().findAny().getAsDouble(), 0d);
+  }
+
+  public void testBuilder() {
+    DoubleStream s = DoubleStream.builder()
+        .add(1d)
+        .add(3d)
+        .add(2d)
+        .build();
+
+    assertEquals(
+        new double[] {1d, 3d, 2d},
+        s.toArray()
+    );
+
+    DoubleStream.Builder builder = DoubleStream.builder();
+    DoubleStream built = builder.build();
+    assertEquals(0, built.count());
+    try {
+      builder.build();
+      fail("build() after build() should fail");
+    } catch (IllegalStateException expected) {
+      // expected
+    }
+    try {
+      builder.add(10d);
+      fail("add() after build() should fail");
+    } catch (IllegalStateException expected) {
+      // expected
+    }
+  }
+
+  public void testConcat() {
+    Supplier<DoubleStream> adbc = () -> {
+      return DoubleStream.concat(DoubleStream.of(1, 4), DoubleStream.of(2, 3));
+    };
+
+    assertEquals(new double[]{1d, 4d, 2d, 3d}, adbc.get().toArray());
+    assertEquals(new double[]{1d, 2d, 3d, 4d}, adbc.get().sorted().toArray());
+
+    List<String> closed = new ArrayList<>();
+    DoubleStream first = DoubleStream.of(1d).onClose(() -> closed.add("first"));
+    DoubleStream second = DoubleStream.of(2d).onClose(() -> closed.add("second"));
+
+    DoubleStream concat = DoubleStream.concat(first, second);
+
+    // read everything, make sure we saw it all and didn't close automatically
+    double collectedAll = concat.sum();
+    assertEquals(3d, collectedAll);
+    assertEquals(0, closed.size());
+
+    concat.close();
+    assertEquals(Arrays.asList("first", "second"), closed);
+  }
+
+  public void testIterate() {
+    assertEquals(
+        new double[]{10d, 11d, 12d, 13d, 14d},
+        DoubleStream.iterate(0d, l -> l + 1d)
+            .skip(10)
+            .limit(5)
+            .toArray()
+    );
+  }
+
+  public void testGenerate() {
+    // infinite, but if you limit it is already too short to skip much
+    assertEquals(new double[0], DoubleStream.generate(makeGenerator()).limit(4).skip(5).toArray());
+
+    assertEquals(
+        new double[]{10d, 11d, 12d, 13d, 14d},
+        DoubleStream.generate(makeGenerator())
+            .skip(10)
+            .limit(5)
+            .toArray()
+    );
+  }
+
+  private DoubleSupplier makeGenerator() {
+    return new DoubleSupplier() {
+      double next = 0d;
+
+      @Override
+      public double getAsDouble() {
+        return next++;
+      }
+    };
+  }
+
+  public void testToArray() {
+    assertEquals(new double[0], DoubleStream.of().toArray());
+    assertEquals(new double[] {1d}, DoubleStream.of(1d).toArray());
+    assertEquals(new double[] {3d, 2d, 0d}, DoubleStream.of(3d, 2d, 0d).toArray());
+  }
+
+  public void testReduce() {
+    double reduced = DoubleStream.of(1d, 2d, 4d).reduce(0, Double::sum);
+    assertEquals(7d, reduced, 0d);
+
+    reduced = DoubleStream.of().reduce(0, Double::sum);
+    assertEquals(0d, reduced, 0d);
+
+    OptionalDouble maybe = DoubleStream.of(1d, 4d, 8d).reduce(Double::sum);
+    assertTrue(maybe.isPresent());
+    assertEquals(13, maybe.getAsDouble(), 0d);
+    maybe = DoubleStream.of().reduce(Double::sum);
+    assertFalse(maybe.isPresent());
+  }
+
+  public void testFilter() {
+    // unconsumed stream never runs filter
+    boolean[] data = {false};
+    DoubleStream.of(1d, 2d, 3d).filter(i -> data[0] |= true);
+    assertFalse(data[0]);
+
+    // one result
+    assertEquals(
+        new double[]{1d},
+        DoubleStream.of(1d, 2d, 3d, 4d, 3d).filter(a -> a == 1).toArray()
+    );
+    // zero results
+    assertEquals(
+        new double[0],
+        DoubleStream.of(1d, 2d, 3d, 4d, 3d).filter(a -> false).toArray()
+    );
+    // two results
+    assertEquals(
+        new double[] {2d, 4d},
+        DoubleStream.of(1d, 2d, 3d, 4d, 3d).filter(a -> a % 2 == 0).toArray()
+    );
+    // all
+    assertEquals(
+        new double[] {1d, 2d, 3d, 4d, 3d},
+        DoubleStream.of(1d, 2d, 3d, 4d, 3d).filter(a -> true).toArray()
+    );
+  }
+
+  public void testMap() {
+    // unconsumed stream never runs map
+    int[] data = {0};
+    DoubleStream.of(1d, 2d, 3d).map(i -> data[0]++);
+    assertEquals(0, data[0]);
+
+    assertEquals(
+        new double[] {2d, 4d, 6d},
+        DoubleStream.of(1d, 2d, 3d).map(i -> i * 2).toArray()
+    );
+  }
+
+  public void testPeek() {
+    // unconsumed stream never peeks
+    boolean[] data = {false};
+    DoubleStream.of(1d, 2d, 3d).peek(i -> data[0] |= true);
+    assertFalse(data[0]);
+
+    // make sure we saw it all in order
+    double[] items = new double[] {1d, 2d, 3d};
+    List<Double> peeked = new ArrayList<>();
+    DoubleStream.of(items).peek(peeked::add).forEach(item -> {
+      // do nothing, just run
+    });
+    assertEquals(items.length, peeked.size());
+    for (int i = 0; i < items.length; i++) {
+      assertEquals(items[i], peeked.get(i), 0d);
+    }
+  }
+
+  // same impl, no parallel in browser
+  public void testFindFirstOrAny() {
+    OptionalDouble any = DoubleStream.of(1d, 2d).findAny();
+    assertTrue(any.isPresent());
+    assertEquals(1d, any.getAsDouble(), 0d);
+  }
+
+  public void testAnyMatch() {
+    // all
+    assertTrue(DoubleStream.of(1d, 2d).anyMatch(s -> true));
+
+    // some
+    assertTrue(DoubleStream.of(1d, 2d).anyMatch(s -> s == 1d));
+
+    // none
+    assertFalse(DoubleStream.of(1d, 2d).anyMatch(s -> false));
+  }
+
+  public void testAllMatch() {
+    // all
+    assertTrue(DoubleStream.of(1d, 2d).allMatch(s -> true));
+
+    // some
+    assertFalse(DoubleStream.of(1d, 2d).allMatch(s -> s == 1d));
+
+    // none
+    assertFalse(DoubleStream.of(1d, 2d).allMatch(s -> false));
+  }
+
+  public void testNoneMatch() {
+    // all
+    assertFalse(DoubleStream.of(1d, 2d).noneMatch(s -> true));
+
+    // some
+    assertFalse(DoubleStream.of(1d, 2d).noneMatch(s -> s == 1d));
+
+    // none
+    assertTrue(DoubleStream.of(1d, 2d).noneMatch(s -> false));
+  }
+
+  public void testFlatMap() {
+    assertEquals(0L, DoubleStream.empty().flatMap(value -> DoubleStream.of(1d)).count());
+    assertEquals(0L, DoubleStream.of(1d).flatMap(value -> DoubleStream.empty()).count());
+    assertEquals(0L, DoubleStream.of(1d).flatMap(value -> DoubleStream.of()).count());
+    assertEquals(0L, DoubleStream.of().flatMap(value -> DoubleStream.of(1d)).count());
+    assertEquals(1L, DoubleStream.of(1d).flatMap(value -> DoubleStream.of(1d)).count());
+
+    DoubleStream values = DoubleStream.of(1d, 2d, 3d);
+
+    assertEquals(
+        new double[] {1d, 2d, 2d, 4d, 3d, 6d},
+        values.flatMap(i -> DoubleStream.of(i, i * 2d)).toArray()
+    );
+  }
+
+  public void testMapToOthers() {
+    Supplier<DoubleStream> s = () -> DoubleStream.of(1d, 2d, 10d);
+
+    assertEquals(
+        new String[]{"1", "2", "10"},
+        s.get().mapToObj(DoubleStreamTest::toIntegralString).toArray(String[]::new)
+    );
+
+    assertEquals(
+        new long[]{1L, 2L, 10L},
+        s.get().mapToLong(i -> (long) i).toArray()
+    );
+
+    assertEquals(
+        new int[] {1, 2, 10},
+        s.get().mapToInt(i -> (int) i).toArray()
+    );
+  }
+
+  public void testDistinct() {
+    double[] distinct = DoubleStream.of(1d, 2d, 3d, 2d).distinct().toArray();
+    assertEquals(3, distinct.length);
+    assertEquals(1d + 2d + 3d, distinct[0] + distinct[1] + distinct[2], 0d);
+  }
+
+  public void testSorted() {
+    double[] sorted = DoubleStream.of(3d, 1d, 2d).sorted().toArray();
+    assertEquals(new double[] {1d, 2d, 3d}, sorted);
+  }
+
+  public void testMinMax() {
+    Supplier<DoubleStream> stream = () -> DoubleStream.of(2d, 3d, 4d, 1d);
+
+    assertEquals(1d, stream.get().min().orElse(0), 0d);
+    assertEquals(4d, stream.get().max().orElse(0), 0d);
+
+    assertFalse(stream.get().filter(a -> false).max().isPresent());
+    assertFalse(stream.get().filter(a -> false).min().isPresent());
+  }
+
+  public void testCountLimitSkip() {
+    Supplier<DoubleStream> stream = () -> DoubleStream.of(1d, 2d, 3d, 4d);
+
+    assertEquals(4L, stream.get().count());
+
+    assertEquals(4L, stream.get().limit(4).count());
+    assertEquals(4L, stream.get().limit(5).count());
+    assertEquals(3L, stream.get().limit(3).count());
+
+    assertEquals(3L, stream.get().skip(1).limit(3).count());
+
+    assertEquals(2L, stream.get().limit(3).skip(1).count());
+
+    assertEquals(1L, stream.get().skip(3).count());
+
+    assertEquals(new double[] {3d, 4d}, stream.get().skip(2).limit(3).toArray());
+    assertEquals(new double[] {3d}, stream.get().skip(2).limit(1).toArray());
+
+    assertEquals(new double[] {4d}, stream.get().skip(3).toArray());
+    assertEquals(new double[] {}, stream.get().skip(5).toArray());
+
+    assertEquals(new double[] {1d, 2d}, stream.get().limit(2).toArray());
+
+    assertEquals(new double[] {2d}, stream.get().limit(2).skip(1).toArray());
+  }
+
+  public void testBoxed() {
+    Supplier<DoubleStream> stream = () -> DoubleStream.of(1d, 2d);
+    Stream<Double> expected = stream.get().mapToObj(Double::valueOf);
+    assertEquals(expected.toArray(), stream.get().boxed().toArray());
+  }
+
+  public void testSummaryStats() {
+    Supplier<DoubleStream> stream = () -> DoubleStream.of(1d, 2d, 3d);
+    DoubleSummaryStatistics summaryStats = stream.get().summaryStatistics();
+    assertEquals(3L, summaryStats.getCount());
+    assertEquals(1d, summaryStats.getMin(), 0d);
+    assertEquals(2d, summaryStats.getAverage(), 0d);
+    assertEquals(3d, summaryStats.getMax(), 0d);
+    assertEquals(6d, summaryStats.getSum(), 0d);
+
+    summaryStats.accept(6L);
+    assertEquals(4L, summaryStats.getCount());
+    assertEquals(1d, summaryStats.getMin(), 0d);
+    assertEquals(3d, summaryStats.getAverage(), 0d);
+    assertEquals(6d, summaryStats.getMax(), 0d);
+    assertEquals(12d, summaryStats.getSum(), 0d);
+
+    DoubleSummaryStatistics combinedSumStats = stream.get().summaryStatistics();
+    combinedSumStats.combine(DoubleStream.of(4d, 5d, 6d, 0d).summaryStatistics());
+    assertEquals(7L, combinedSumStats.getCount());
+    assertEquals(0d, combinedSumStats.getMin(), 0d);
+    assertEquals(3d, combinedSumStats.getAverage(), 0d);
+    assertEquals(6d, combinedSumStats.getMax(), 0d);
+    assertEquals(21d, combinedSumStats.getSum(), 0d);
+  }
+
+  public void testAverage() {
+    assertFalse(DoubleStream.empty().average().isPresent());
+    assertEquals(2.0d, DoubleStream.of(1d, 2d, 3d).average().getAsDouble(), 0d);
+    assertEquals(0d, DoubleStream.of(1d, 2d, -3d).average().getAsDouble(), 0d);
+    assertEquals(-2.0d, DoubleStream.of(-1d, -2d, -3d).average().getAsDouble(), 0d);
+  }
+
+  public void testSum() {
+    assertEquals(6d, DoubleStream.of(1d, 2d, 3d).sum(), 0d);
+    assertEquals(0d, DoubleStream.of(1d, 2d, -3d).sum(), 0d);
+    assertEquals(-6d, DoubleStream.of(-1d, -2d, -3d).sum(), 0d);
+  }
+
+  public void testCollect() {
+    // noinspection StringConcatenationInsideStringBufferAppend
+    String val = DoubleStream.of(1d, 2d, 3d, 4d, 5d).collect(StringBuilder::new,
+        (stringBuilder, d) -> stringBuilder.append(toIntegralString(d)),
+        StringBuilder::append).toString();
+
+    assertEquals("12345", val);
+  }
+
+  public void testForEach() {
+    List<Double> vals = new ArrayList<>();
+    DoubleStream.of(1d, 2d, 3d, 4d, 5d).forEach(vals::add);
+    assertEquals(5, vals.size());
+    assertEquals(new Double[] {1d, 2d, 3d, 4d, 5d}, vals.toArray(new Double[vals.size()]));
+  }
+
+  public void testIterator() {
+    List<Double> vals = new ArrayList<>();
+    Iterator<Double> iterator = DoubleStream.of(1d, 2d, 3d, 4d, 5d).iterator();
+    while (iterator.hasNext()) {
+      vals.add(iterator.next());
+    }
+    assertEquals(5, vals.size());
+    assertEquals(new Double[] {1d, 2d, 3d, 4d, 5d}, vals.toArray(new Double[vals.size()]));
+  }
+
+  public void testSpliterator() {
+    Spliterator<Double> spliterator = DoubleStream.of(1d, 2d, 3d, 4d, 5d).spliterator();
+    assertEquals(5, spliterator.estimateSize());
+    assertEquals(5, spliterator.getExactSizeIfKnown());
+
+    List<Double> vals = new ArrayList<>();
+    while (spliterator.tryAdvance(vals::add)) {
+      // work is all done in the condition
+    }
+
+    assertEquals(5, vals.size());
+    assertEquals(new Double[] {1d, 2d, 3d, 4d, 5d}, vals.toArray(new Double[vals.size()]));
+  }
+
+  // See https://github.com/gwtproject/gwt/issues/8615
+  private static String toIntegralString(double value) {
+    return "" + (int) value;
+  }
+}
diff --git a/user/test/com/google/gwt/emultest/java8/util/stream/IntStreamTest.java b/user/test/com/google/gwt/emultest/java8/util/stream/IntStreamTest.java
new file mode 100644
index 0000000..232aebe
--- /dev/null
+++ b/user/test/com/google/gwt/emultest/java8/util/stream/IntStreamTest.java
@@ -0,0 +1,462 @@
+/*
+ * 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 com.google.gwt.emultest.java.util.EmulTestBase;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.IntSummaryStatistics;
+import java.util.Iterator;
+import java.util.List;
+import java.util.OptionalInt;
+import java.util.Spliterator;
+import java.util.function.IntSupplier;
+import java.util.function.Supplier;
+import java.util.stream.DoubleStream;
+import java.util.stream.IntStream;
+import java.util.stream.LongStream;
+import java.util.stream.Stream;
+
+/**
+ * Tests {@link IntStream}.
+ */
+public class IntStreamTest extends EmulTestBase {
+
+  public void testEmptyStream() {
+    IntStream empty = IntStream.empty();
+    assertEquals(0, empty.count());
+    try {
+      empty.count();
+      fail("second terminal operation should have thrown IllegalStateEx");
+    } catch (IllegalStateException expected) {
+      // expected
+    }
+
+    assertEquals(0, IntStream.empty().limit(2).toArray().length);
+    assertEquals(0L, IntStream.empty().count());
+    assertEquals(0L, IntStream.empty().limit(2).count());
+
+    assertFalse(IntStream.empty().findFirst().isPresent());
+    assertFalse(IntStream.empty().findAny().isPresent());
+    assertFalse(IntStream.empty().max().isPresent());
+    assertFalse(IntStream.empty().min().isPresent());
+    assertTrue(IntStream.empty().noneMatch(item -> true));
+    assertTrue(IntStream.empty().allMatch(item -> true));
+    assertFalse(IntStream.empty().anyMatch(item -> true));
+    assertEquals(new int[0], IntStream.empty().toArray());
+  }
+
+  public void testStreamOfOne() {
+    Supplier<IntStream> one = () -> IntStream.of(1);
+    assertEquals(new int[]{1}, one.get().toArray());
+    assertEquals(1L, one.get().count());
+    assertEquals(1, one.get().findFirst().getAsInt());
+    assertEquals(1, one.get().findAny().getAsInt());
+  }
+
+  public void testBuilder() {
+    IntStream s = IntStream.builder()
+        .add(1)
+        .add(3)
+        .add(2)
+        .build();
+
+    assertEquals(
+        new int[] {1, 3, 2},
+        s.toArray()
+    );
+
+    IntStream.Builder builder = IntStream.builder();
+    IntStream built = builder.build();
+    assertEquals(0, built.count());
+    try {
+      builder.build();
+      fail("build() after build() should fail");
+    } catch (IllegalStateException expected) {
+      // expected
+    }
+    try {
+      builder.add(10);
+      fail("add() after build() should fail");
+    } catch (IllegalStateException expected) {
+      // expected
+    }
+  }
+
+  public void testConcat() {
+    Supplier<IntStream> adbc = () -> IntStream.concat(IntStream.of(1, 4), IntStream.of(2, 3));
+
+    assertEquals(new int[]{1, 4, 2, 3}, adbc.get().toArray());
+    assertEquals(new int[]{1, 2, 3, 4}, adbc.get().sorted().toArray());
+
+    List<String> closed = new ArrayList<>();
+    IntStream first = IntStream.of(1).onClose(() -> closed.add("first"));
+    IntStream second = IntStream.of(2).onClose(() -> closed.add("second"));
+
+    IntStream concat = IntStream.concat(first, second);
+
+    // read everything, make sure we saw it all and didn't close automatically
+    int collectedAll = concat.sum();
+    assertEquals(3, collectedAll);
+    assertEquals(0, closed.size());
+
+    concat.close();
+    assertEquals(Arrays.asList("first", "second"), closed);
+  }
+
+  public void testIterate() {
+    assertEquals(
+        new int[]{10, 11, 12, 13, 14},
+        IntStream.iterate(0, i -> i + 1)
+            .skip(10)
+            .limit(5)
+            .toArray()
+    );
+  }
+
+  public void testGenerate() {
+    // infinite, but if you limit it is already too short to skip much
+    assertEquals(new int[0], IntStream.generate(makeGenerator()).limit(4).skip(5).toArray());
+
+    assertEquals(
+        new int[] {10, 11, 12, 13, 14},
+        IntStream.generate(makeGenerator())
+            .skip(10)
+            .limit(5)
+            .toArray()
+    );
+  }
+
+  private IntSupplier makeGenerator() {
+    return new IntSupplier() {
+      int next = 0;
+
+      @Override
+      public int getAsInt() {
+        return next++;
+      }
+    };
+  }
+
+  public void testRange() {
+    assertEquals(new int[] {1, 2, 3, 4}, IntStream.range(1, 5).toArray());
+    assertEquals(new int[] {-1, 0, 1, 2, 3, 4}, IntStream.range(-1, 5).toArray());
+    assertEquals(new int[] {}, IntStream.range(1, -5).toArray());
+    assertEquals(new int[] {}, IntStream.range(-1, -5).toArray());
+  }
+
+  public void testRangeClosed() {
+    assertEquals(new int[] {1, 2, 3, 4, 5}, IntStream.rangeClosed(1, 5).toArray());
+    assertEquals(new int[] {-1, 0, 1, 2, 3, 4, 5}, IntStream.rangeClosed(-1, 5).toArray());
+    assertEquals(new int[] {}, IntStream.rangeClosed(1, -5).toArray());
+    assertEquals(new int[] {}, IntStream.rangeClosed(-1, -5).toArray());
+  }
+
+  public void testToArray() {
+    assertEquals(new int[0], IntStream.of().toArray());
+    assertEquals(new int[] {1}, IntStream.of(1).toArray());
+    assertEquals(new int[] {3,2,0}, IntStream.of(3,2,0).toArray());
+  }
+
+  public void testReduce() {
+    int reduced = IntStream.of(1, 2, 4).reduce(0, Integer::sum);
+    assertEquals(7, reduced);
+
+    reduced = IntStream.of().reduce(0, Integer::sum);
+    assertEquals(0, reduced);
+
+    OptionalInt maybe = IntStream.of(1, 4, 8).reduce(Integer::sum);
+    assertTrue(maybe.isPresent());
+    assertEquals(13, maybe.getAsInt());
+    maybe = IntStream.of().reduce(Integer::sum);
+    assertFalse(maybe.isPresent());
+  }
+
+  public void testFilter() {
+    // unconsumed stream never runs filter
+    boolean[] data = {false};
+    IntStream.of(1, 2, 3).filter(i -> data[0] |= true);
+    assertFalse(data[0]);
+
+    // one result
+    assertEquals(
+        new int[] {1},
+        IntStream.of(1, 2, 3, 4, 3).filter(a -> a == 1).toArray()
+    );
+    // zero results
+    assertEquals(
+        new int[0],
+        IntStream.of(1, 2, 3, 4, 3).filter(a -> false).toArray()
+    );
+    // two results
+    assertEquals(
+        new int[] {2, 4},
+        IntStream.of(1, 2, 3, 4, 3).filter(a -> a % 2 == 0).toArray()
+    );
+    // all
+    assertEquals(
+        new int[] {1, 2, 3, 4, 3},
+        IntStream.of(1, 2, 3, 4, 3).filter(a -> true).toArray()
+    );
+  }
+
+  public void testMap() {
+    // unconsumed stream never runs map
+    int[] data = {0};
+    IntStream.of(1, 2, 3).map(i -> data[0]++);
+    assertEquals(0, data[0]);
+
+    assertEquals(
+        new int[] {2, 4, 6},
+        IntStream.of(1, 2, 3).map(i -> i * 2).toArray()
+    );
+  }
+
+  public void testPeek() {
+    // unconsumed stream never peeks
+    boolean[] data = {false};
+    IntStream.of(1, 2, 3).peek(i -> data[0] |= true);
+    assertFalse(data[0]);
+
+    // make sure we saw it all in order
+    int[] items = new int[] {1, 2, 3};
+    List<Integer> peeked = new ArrayList<>();
+    IntStream.of(items).peek(peeked::add).forEach(item -> {
+      // do nothing, just run
+    });
+    assertEquals(items.length, peeked.size());
+    for (int i = 0; i < items.length; i++) {
+      assertEquals(items[i], (int) peeked.get(i));
+    }
+  }
+
+  // same impl, no parallel in browser
+  public void testFindFirstOrAny() {
+    OptionalInt any = IntStream.of(1, 2).findAny();
+    assertTrue(any.isPresent());
+    assertEquals(1, any.getAsInt());
+  }
+
+  public void testAnyMatch() {
+    // all
+    assertTrue(IntStream.of(1, 2).anyMatch(s -> true));
+
+    // some
+    assertTrue(IntStream.of(1, 2).anyMatch(s -> s == 1));
+
+    // none
+    assertFalse(IntStream.of(1, 2).anyMatch(s -> false));
+  }
+
+  public void testAllMatch() {
+    // all
+    assertTrue(IntStream.of(1, 2).allMatch(s -> true));
+
+    // some
+    assertFalse(IntStream.of(1, 2).allMatch(s -> s == 1));
+
+    // none
+    assertFalse(IntStream.of(1, 2).allMatch(s -> false));
+  }
+
+  public void testNoneMatch() {
+    // all
+    assertFalse(IntStream.of(1, 2).noneMatch(s -> true));
+
+    // some
+    assertFalse(IntStream.of(1, 2).noneMatch(s -> s == 1));
+
+    // none
+    assertTrue(IntStream.of(1, 2).noneMatch(s -> false));
+  }
+
+  public void testFlatMap() {
+    assertEquals(0, IntStream.empty().flatMap(value -> IntStream.of(1)).count());
+    assertEquals(0, IntStream.of(1).flatMap(value -> IntStream.empty()).count());
+    assertEquals(0, IntStream.of(1).flatMap(value -> IntStream.of()).count());
+    assertEquals(0, IntStream.of().flatMap(value -> IntStream.of(1)).count());
+    assertEquals(1, IntStream.of(1).flatMap(value -> IntStream.of(1)).count());
+
+    IntStream values = IntStream.of(1, 2, 3);
+
+    assertEquals(
+        new int[] {1, 2, 2, 4, 3, 6},
+        values.flatMap(i -> IntStream.of(i, i * 2)).toArray()
+    );
+  }
+
+  public void testMapToOthers() {
+    Supplier<IntStream> s = () -> IntStream.of(1, 2, 10);
+
+    assertEquals(
+        new String[] {"1", "2", "10"},
+        s.get().mapToObj(String::valueOf).toArray(String[]::new)
+    );
+
+    assertEquals(
+        new long[] {1, 2, 10},
+        s.get().mapToLong(i -> (long) i).toArray()
+    );
+
+    assertEquals(
+        new double[] {1, 2, 10},
+        s.get().mapToDouble(i -> (double) i).toArray()
+    );
+  }
+
+  public void testDistinct() {
+    int[] distinct = IntStream.of(1, 2, 3, 2).distinct().toArray();
+    assertEquals(3, distinct.length);
+    assertEquals(1 + 2 + 3, distinct[0] + distinct[1] + distinct[2]);
+  }
+
+  public void testSorted() {
+    int[] sorted = IntStream.of(3, 1, 2).sorted().toArray();
+    assertEquals(new int[] {1, 2, 3}, sorted);
+  }
+
+  public void testMinMax() {
+    Supplier<IntStream> stream = () -> IntStream.of(2, 3, 4, 1);
+
+    assertEquals(1, stream.get().min().orElse(0));
+    assertEquals(4, stream.get().max().orElse(0));
+
+    assertFalse(stream.get().filter(a -> false).max().isPresent());
+    assertFalse(stream.get().filter(a -> false).min().isPresent());
+  }
+
+  public void testCountLimitSkip() {
+    Supplier<IntStream> stream = () -> IntStream.of(1, 2, 3, 4);
+
+    assertEquals(4, stream.get().count());
+
+    assertEquals(4, stream.get().limit(4).count());
+    assertEquals(4, stream.get().limit(5).count());
+    assertEquals(3, stream.get().limit(3).count());
+
+    assertEquals(3, stream.get().skip(1).limit(3).count());
+
+    assertEquals(2, stream.get().limit(3).skip(1).count());
+
+    assertEquals(1, stream.get().skip(3).count());
+
+    assertEquals(new int[] {3, 4}, stream.get().skip(2).limit(3).toArray());
+    assertEquals(new int[] {3}, stream.get().skip(2).limit(1).toArray());
+
+    assertEquals(new int[] {4}, stream.get().skip(3).toArray());
+    assertEquals(new int[] {}, stream.get().skip(5).toArray());
+
+    assertEquals(new int[] {1, 2}, stream.get().limit(2).toArray());
+
+    assertEquals(new int[] {2}, stream.get().limit(2).skip(1).toArray());
+  }
+
+  public void testBoxed() {
+    Supplier<IntStream> stream = () -> IntStream.of(1, 2);
+    Stream<Integer> expected = stream.get().mapToObj(Integer::valueOf);
+    assertEquals(expected.toArray(), stream.get().boxed().toArray());
+  }
+
+  public void testAsOtherPrimitive() {
+    Supplier<IntStream> stream = () -> IntStream.of(1, 2);
+
+    DoubleStream actualDoubleStream = stream.get().asDoubleStream();
+    assertEquals(new double[]{1, 2}, actualDoubleStream.toArray());
+
+    LongStream actualLongStream = stream.get().asLongStream();
+    assertEquals(new long[] {1, 2}, actualLongStream.toArray());
+  }
+
+  public void testSummaryStats() {
+    Supplier<IntStream> stream = () -> IntStream.of(1, 2, 3);
+    IntSummaryStatistics summaryStats = stream.get().summaryStatistics();
+    assertEquals(3, summaryStats.getCount());
+    assertEquals(1, summaryStats.getMin());
+    assertEquals(2, summaryStats.getAverage(), 0d);
+    assertEquals(3, summaryStats.getMax());
+    assertEquals(6, summaryStats.getSum());
+
+    summaryStats.accept(6);
+    assertEquals(4, summaryStats.getCount());
+    assertEquals(1, summaryStats.getMin());
+    assertEquals(3, summaryStats.getAverage(), 0d);
+    assertEquals(6, summaryStats.getMax());
+    assertEquals(12, summaryStats.getSum());
+
+    IntSummaryStatistics combinedSumStats = stream.get().summaryStatistics();
+    combinedSumStats.combine(IntStream.of(4, 5, 6, 0).summaryStatistics());
+    assertEquals(7, combinedSumStats.getCount());
+    assertEquals(0, combinedSumStats.getMin());
+    assertEquals(3, combinedSumStats.getAverage(), 0d);
+    assertEquals(6, combinedSumStats.getMax());
+    assertEquals(21, combinedSumStats.getSum());
+  }
+
+  public void testAverage() {
+    assertFalse(IntStream.empty().average().isPresent());
+    assertEquals(2.0d, IntStream.of(1, 2, 3).average().getAsDouble(), 0d);
+    assertEquals(0d, IntStream.of(1, 2, -3).average().getAsDouble(), 0d);
+    assertEquals(-2.0d, IntStream.of(-1, -2, -3).average().getAsDouble(), 0d);
+  }
+
+  public void testSum() {
+    assertEquals(6, IntStream.of(1, 2, 3).sum());
+    assertEquals(0, IntStream.of(1, 2, -3).sum());
+    assertEquals(-6, IntStream.of(-1, -2, -3).sum());
+  }
+
+  public void testCollect() {
+    String val = IntStream.of(1, 2, 3, 4, 5).collect(StringBuilder::new,
+        // TODO switch to a lambda reference once #9340 is fixed
+        (stringBuilder, i) -> stringBuilder.append(i),
+        StringBuilder::append).toString();
+
+    assertEquals("12345", val);
+  }
+
+  public void testForEach() {
+    List<Integer> vals = new ArrayList<>();
+    IntStream.of(1, 2, 3, 4, 5).forEach(vals::add);
+    assertEquals(5, vals.size());
+    assertEquals(new Integer[] {1, 2, 3, 4, 5}, vals.toArray(new Integer[vals.size()]));
+  }
+
+  public void testIterator() {
+    List<Integer> vals = new ArrayList<>();
+    Iterator<Integer> iterator = IntStream.of(1, 2, 3, 4, 5).iterator();
+    while (iterator.hasNext()) {
+      vals.add(iterator.next());
+    }
+    assertEquals(5, vals.size());
+    assertEquals(new Integer[] {1, 2, 3, 4, 5}, vals.toArray(new Integer[vals.size()]));
+  }
+
+  public void testSpliterator() {
+    Spliterator<Integer> spliterator = IntStream.of(1, 2, 3, 4, 5).spliterator();
+    assertEquals(5, spliterator.estimateSize());
+    assertEquals(5, spliterator.getExactSizeIfKnown());
+
+    List<Integer> vals = new ArrayList<>();
+    while (spliterator.tryAdvance(vals::add)) {
+      // work is all done in the condition
+    }
+
+    assertEquals(5, vals.size());
+    assertEquals(new Integer[] {1, 2, 3, 4, 5}, vals.toArray(new Integer[vals.size()]));
+  }
+}
diff --git a/user/test/com/google/gwt/emultest/java8/util/stream/LongStreamTest.java b/user/test/com/google/gwt/emultest/java8/util/stream/LongStreamTest.java
new file mode 100644
index 0000000..407f4bf
--- /dev/null
+++ b/user/test/com/google/gwt/emultest/java8/util/stream/LongStreamTest.java
@@ -0,0 +1,457 @@
+/*
+ * 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 com.google.gwt.emultest.java.util.EmulTestBase;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.LongSummaryStatistics;
+import java.util.OptionalLong;
+import java.util.Spliterator;
+import java.util.function.LongSupplier;
+import java.util.function.Supplier;
+import java.util.stream.DoubleStream;
+import java.util.stream.LongStream;
+import java.util.stream.Stream;
+
+/**
+ * Tests {@link LongStream}.
+ */
+public class LongStreamTest extends EmulTestBase {
+
+  public void testEmptyStream() {
+    LongStream empty = LongStream.empty();
+    assertEquals(0, empty.count());
+    try {
+      empty.count();
+      fail("second terminal operation should have thrown IllegalStateEx");
+    } catch (IllegalStateException expected) {
+      // expected
+    }
+
+    assertEquals(0, LongStream.empty().limit(2).toArray().length);
+    assertEquals(0L, LongStream.empty().count());
+    assertEquals(0L, LongStream.empty().limit(2).count());
+
+    assertFalse(LongStream.empty().findFirst().isPresent());
+    assertFalse(LongStream.empty().findAny().isPresent());
+    assertFalse(LongStream.empty().max().isPresent());
+    assertFalse(LongStream.empty().min().isPresent());
+    assertTrue(LongStream.empty().noneMatch(item -> true));
+    assertTrue(LongStream.empty().allMatch(item -> true));
+    assertFalse(LongStream.empty().anyMatch(item -> true));
+    assertEquals(new long[0], LongStream.empty().toArray());
+  }
+
+  public void testStreamOfOne() {
+    Supplier<LongStream> one = () -> LongStream.of(1);
+    assertEquals(new long[]{1L}, one.get().toArray());
+    assertEquals(1L, one.get().count());
+    assertEquals(1, one.get().findFirst().getAsLong());
+    assertEquals(1, one.get().findAny().getAsLong());
+  }
+
+  public void testBuilder() {
+    LongStream s = LongStream.builder()
+        .add(1L)
+        .add(3L)
+        .add(2L)
+        .build();
+
+    assertEquals(
+        new long[] {1L, 3L, 2L},
+        s.toArray()
+    );
+
+    LongStream.Builder builder = LongStream.builder();
+    LongStream built = builder.build();
+    assertEquals(0L, built.count());
+    try {
+      builder.build();
+      fail("build() after build() should fail");
+    } catch (IllegalStateException expected) {
+      // expected
+    }
+    try {
+      builder.add(10L);
+      fail("add() after build() should fail");
+    } catch (IllegalStateException expected) {
+      // expected
+    }
+  }
+
+  public void testConcat() {
+    Supplier<LongStream> adbc = () -> LongStream.concat(LongStream.of(1, 4), LongStream.of(2, 3));
+
+    assertEquals(new long[]{1L, 4L, 2L, 3L}, adbc.get().toArray());
+    assertEquals(new long[]{1L, 2L, 3L, 4L}, adbc.get().sorted().toArray());
+
+    List<String> closed = new ArrayList<>();
+    LongStream first = LongStream.of(1L).onClose(() -> closed.add("first"));
+    LongStream second = LongStream.of(2L).onClose(() -> closed.add("second"));
+
+    LongStream concat = LongStream.concat(first, second);
+
+    // read everything, make sure we saw it all and didn't close automatically
+    long collectedAll = concat.sum();
+    assertEquals(3L, collectedAll);
+    assertEquals(0, closed.size());
+
+    concat.close();
+    assertEquals(Arrays.asList("first", "second"), closed);
+  }
+
+  public void testIterate() {
+    assertEquals(
+        new long[]{10L, 11L, 12L, 13L, 14L},
+        LongStream.iterate(0L, l -> l + 1L)
+            .skip(10)
+            .limit(5)
+            .toArray()
+    );
+  }
+
+  public void testGenerate() {
+    // infinite, but if you limit it is already too short to skip much
+    assertEquals(new long[0], LongStream.generate(makeGenerator()).limit(4).skip(5).toArray());
+
+    assertEquals(
+        new long[]{10L, 11L, 12L, 13L, 14L},
+        LongStream.generate(makeGenerator())
+            .skip(10)
+            .limit(5)
+            .toArray()
+    );
+  }
+
+  private LongSupplier makeGenerator() {
+    return new LongSupplier() {
+      long next = 0L;
+
+      @Override
+      public long getAsLong() {
+        return next++;
+      }
+    };
+  }
+
+  public void testRange() {
+    assertEquals(new long[] {1L, 2L, 3L, 4L}, LongStream.range(1, 5).toArray());
+    assertEquals(new long[] {-1L, 0L, 1L, 2L, 3L, 4L}, LongStream.range(-1, 5).toArray());
+    assertEquals(new long[] {}, LongStream.range(1, -5).toArray());
+    assertEquals(new long[] {}, LongStream.range(-1, -5).toArray());
+  }
+
+  public void testRangeClosed() {
+    assertEquals(new long[] {1L, 2L, 3L, 4L, 5L}, LongStream.rangeClosed(1, 5).toArray());
+    assertEquals(new long[] {-1L, 0L, 1L, 2L, 3L, 4L, 5L}, LongStream.rangeClosed(-1, 5).toArray());
+    assertEquals(new long[] {}, LongStream.rangeClosed(1, -5).toArray());
+    assertEquals(new long[] {}, LongStream.rangeClosed(-1, -5).toArray());
+  }
+
+  public void testToArray() {
+    assertEquals(new long[0], LongStream.of().toArray());
+    assertEquals(new long[] {1L}, LongStream.of(1L).toArray());
+    assertEquals(new long[] {3L, 2L, 0L}, LongStream.of(3L,2L,0L).toArray());
+  }
+
+  public void testReduce() {
+    long reduced = LongStream.of(1L, 2L, 4L).reduce(0, Long::sum);
+    assertEquals(7, reduced);
+
+    reduced = LongStream.of().reduce(0, Long::sum);
+    assertEquals(0L, reduced);
+
+    OptionalLong maybe = LongStream.of(1L, 4L, 8L).reduce(Long::sum);
+    assertTrue(maybe.isPresent());
+    assertEquals(13L, maybe.getAsLong());
+    maybe = LongStream.of().reduce(Long::sum);
+    assertFalse(maybe.isPresent());
+  }
+
+  public void testFilter() {
+    // unconsumed stream never runs filter
+    boolean[] data = {false};
+    LongStream.of(1L, 2L, 3L).filter(i -> data[0] |= true);
+    assertFalse(data[0]);
+
+    // one result
+    assertEquals(
+        new long[] {1L},
+        LongStream.of(1L, 2L, 3L, 4L, 3L).filter(a -> a == 1).toArray()
+    );
+    // zero results
+    assertEquals(
+        new long[0],
+        LongStream.of(1L, 2L, 3L, 4L, 3L).filter(a -> false).toArray()
+    );
+    // two results
+    assertEquals(
+        new long[] {2L, 4L},
+        LongStream.of(1L, 2L, 3L, 4L, 3L).filter(a -> a % 2 == 0).toArray()
+    );
+    // all
+    assertEquals(
+        new long[] {1L, 2L, 3L, 4L, 3L},
+        LongStream.of(1L, 2L, 3L, 4L, 3L).filter(a -> true).toArray()
+    );
+  }
+
+  public void testMap() {
+    // unconsumed stream never runs map
+    int[] data = {0};
+    LongStream.of(1L, 2L, 3L).map(i -> data[0]++);
+    assertEquals(0, data[0]);
+
+    assertEquals(
+        new long[] {2L, 4L, 6L},
+        LongStream.of(1L, 2L, 3L).map(i -> i * 2).toArray()
+    );
+  }
+
+  public void testPeek() {
+    // unconsumed stream never peeks
+    boolean[] data = {false};
+    LongStream.of(1L, 2L, 3L).peek(i -> data[0] |= true);
+    assertFalse(data[0]);
+
+    // make sure we saw it all in order
+    long[] items = new long[] {1L, 2L, 3L};
+    List<Long> peeked = new ArrayList<>();
+    LongStream.of(items).peek(peeked::add).forEach(item -> {
+      // do nothing, just run
+    });
+    assertEquals(items.length, peeked.size());
+    for (int i = 0; i < items.length; i++) {
+      assertEquals(items[i], (long) peeked.get(i));
+    }
+  }
+
+  // same impl, no parallel in browser
+  public void testFindFirstOrAny() {
+    OptionalLong any = LongStream.of(1L, 2L).findAny();
+    assertTrue(any.isPresent());
+    assertEquals(1L, any.getAsLong());
+  }
+
+  public void testAnyMatch() {
+    // all
+    assertTrue(LongStream.of(1L, 2L).anyMatch(s -> true));
+
+    // some
+    assertTrue(LongStream.of(1L, 2L).anyMatch(s -> s == 1L));
+
+    // none
+    assertFalse(LongStream.of(1L, 2L).anyMatch(s -> false));
+  }
+
+  public void testAllMatch() {
+    // all
+    assertTrue(LongStream.of(1L, 2L).allMatch(s -> true));
+
+    // some
+    assertFalse(LongStream.of(1L, 2L).allMatch(s -> s == 1L));
+
+    // none
+    assertFalse(LongStream.of(1L, 2L).allMatch(s -> false));
+  }
+
+  public void testNoneMatch() {
+    // all
+    assertFalse(LongStream.of(1L, 2L).noneMatch(s -> true));
+
+    // some
+    assertFalse(LongStream.of(1L, 2L).noneMatch(s -> s == 1L));
+
+    // none
+    assertTrue(LongStream.of(1L, 2L).noneMatch(s -> false));
+  }
+
+  public void testFlatMap() {
+    assertEquals(0L, LongStream.empty().flatMap(value -> LongStream.of(1L)).count());
+    assertEquals(0L, LongStream.of(1L).flatMap(value -> LongStream.empty()).count());
+    assertEquals(0L, LongStream.of(1L).flatMap(value -> LongStream.of()).count());
+    assertEquals(0L, LongStream.of().flatMap(value -> LongStream.of(1L)).count());
+    assertEquals(1L, LongStream.of(1L).flatMap(value -> LongStream.of(1L)).count());
+
+    LongStream values = LongStream.of(1L, 2L, 3L);
+
+    assertEquals(
+        new long[] {1L, 2L, 2L, 4L, 3L, 6L},
+        values.flatMap(i -> LongStream.of(i, i * 2)).toArray()
+    );
+  }
+
+  public void testMapToOthers() {
+    Supplier<LongStream> s = () -> LongStream.of(1, 2, 10);
+
+    assertEquals(
+        new String[] {"1", "2", "10"},
+        s.get().mapToObj(String::valueOf).toArray(String[]::new)
+    );
+
+    assertEquals(
+        new int[] {1, 2, 10},
+        s.get().mapToInt(i -> (int) i).toArray()
+    );
+
+    assertEquals(
+        new double[] {1d, 2d, 10d},
+        s.get().mapToDouble(i -> (double) i).toArray()
+    );
+  }
+
+  public void testDistinct() {
+    long[] distinct = LongStream.of(1L, 2L, 3L, 2L).distinct().toArray();
+    assertEquals(3, distinct.length);
+    assertEquals(1L + 2L + 3L, distinct[0] + distinct[1] + distinct[2]);
+  }
+
+  public void testSorted() {
+    long[] sorted = LongStream.of(3L, 1L, 2L).sorted().toArray();
+    assertEquals(new long[] {1L, 2L, 3L}, sorted);
+  }
+
+  public void testMinMax() {
+    Supplier<LongStream> stream = () -> LongStream.of(2L, 3L, 4L, 1L);
+
+    assertEquals(1L, stream.get().min().orElse(0));
+    assertEquals(4L, stream.get().max().orElse(0));
+
+    assertFalse(stream.get().filter(a -> false).max().isPresent());
+    assertFalse(stream.get().filter(a -> false).min().isPresent());
+  }
+
+  public void testCountLimitSkip() {
+    Supplier<LongStream> stream = () -> LongStream.of(1L, 2L, 3L, 4L);
+
+    assertEquals(4L, stream.get().count());
+
+    assertEquals(4L, stream.get().limit(4).count());
+    assertEquals(4L, stream.get().limit(5).count());
+    assertEquals(3L, stream.get().limit(3).count());
+
+    assertEquals(3L, stream.get().skip(1).limit(3).count());
+
+    assertEquals(2L, stream.get().limit(3).skip(1).count());
+
+    assertEquals(1L, stream.get().skip(3).count());
+
+    assertEquals(new long[] {3L, 4L}, stream.get().skip(2).limit(3).toArray());
+    assertEquals(new long[] {3L}, stream.get().skip(2).limit(1).toArray());
+
+    assertEquals(new long[] {4L}, stream.get().skip(3).toArray());
+    assertEquals(new long[] {}, stream.get().skip(5).toArray());
+
+    assertEquals(new long[] {1L, 2L}, stream.get().limit(2).toArray());
+
+    assertEquals(new long[] {2L}, stream.get().limit(2).skip(1).toArray());
+  }
+
+  public void testBoxed() {
+    Supplier<LongStream> stream = () -> LongStream.of(1L, 2L);
+    Stream<Long> expected = stream.get().mapToObj(Long::valueOf);
+    assertEquals(expected.toArray(), stream.get().boxed().toArray());
+  }
+
+  public void testAsOtherPrimitive() {
+    Supplier<LongStream> stream = () -> LongStream.of(1L, 2L);
+    DoubleStream actualDoubleStream = stream.get().asDoubleStream();
+    assertEquals(new double[]{1, 2}, actualDoubleStream.toArray());
+  }
+
+  public void testSummaryStats() {
+    Supplier<LongStream> stream = () -> LongStream.of(1L, 2L, 3L);
+    LongSummaryStatistics summaryStats = stream.get().summaryStatistics();
+    assertEquals(3L, summaryStats.getCount());
+    assertEquals(1L, summaryStats.getMin());
+    assertEquals(2L, summaryStats.getAverage(), 0d);
+    assertEquals(3L, summaryStats.getMax());
+    assertEquals(6L, summaryStats.getSum());
+
+    summaryStats.accept(6L);
+    assertEquals(4L, summaryStats.getCount());
+    assertEquals(1L, summaryStats.getMin());
+    assertEquals(3L, summaryStats.getAverage(), 0d);
+    assertEquals(6L, summaryStats.getMax());
+    assertEquals(12L, summaryStats.getSum());
+
+    LongSummaryStatistics combinedSumStats = stream.get().summaryStatistics();
+    combinedSumStats.combine(LongStream.of(4L, 5L, 6L, 0L).summaryStatistics());
+    assertEquals(7L, combinedSumStats.getCount());
+    assertEquals(0L, combinedSumStats.getMin());
+    assertEquals(3L, combinedSumStats.getAverage(), 0d);
+    assertEquals(6L, combinedSumStats.getMax());
+    assertEquals(21L, combinedSumStats.getSum());
+  }
+
+  public void testAverage() {
+    assertFalse(LongStream.empty().average().isPresent());
+    assertEquals(2.0d, LongStream.of(1L, 2L, 3L).average().getAsDouble(), 0d);
+    assertEquals(0d, LongStream.of(1L, 2L, -3L).average().getAsDouble(), 0d);
+    assertEquals(-2.0d, LongStream.of(-1L, -2L, -3L).average().getAsDouble(), 0d);
+  }
+
+  public void testSum() {
+    assertEquals(6L, LongStream.of(1L, 2L, 3L).sum());
+    assertEquals(0L, LongStream.of(1L, 2L, -3L).sum());
+    assertEquals(-6L, LongStream.of(-1L, -2L, -3L).sum());
+  }
+
+  public void testCollect() {
+    String val = LongStream.of(1L, 2L, 3L, 4L, 5L).collect(StringBuilder::new,
+        // TODO switch to a lambda reference once #9340 is fixed
+        (stringBuilder, lng) -> stringBuilder.append(lng),
+        StringBuilder::append).toString();
+
+    assertEquals("12345", val);
+  }
+
+  public void testForEach() {
+    List<Long> vals = new ArrayList<>();
+    LongStream.of(1L, 2L, 3L, 4L, 5L).forEach(vals::add);
+    assertEquals(5, vals.size());
+    assertEquals(new Long[] {1L, 2L, 3L, 4L, 5L}, vals.toArray(new Long[vals.size()]));
+  }
+
+  public void testIterator() {
+    List<Long> vals = new ArrayList<>();
+    Iterator<Long> iterator = LongStream.of(1L, 2L, 3L, 4L, 5L).iterator();
+    while (iterator.hasNext()) {
+      vals.add(iterator.next());
+    }
+    assertEquals(5, vals.size());
+    assertEquals(new Long[] {1L, 2L, 3L, 4L, 5L}, vals.toArray(new Long[vals.size()]));
+  }
+
+  public void testSpliterator() {
+    Spliterator<Long> spliterator = LongStream.of(1L, 2L, 3L, 4L, 5L).spliterator();
+    assertEquals(5, spliterator.estimateSize());
+    assertEquals(5, spliterator.getExactSizeIfKnown());
+
+    List<Long> vals = new ArrayList<>();
+    while (spliterator.tryAdvance(vals::add)) {
+      // work is all done in the condition
+    }
+
+    assertEquals(5, vals.size());
+    assertEquals(new Long[] {1L, 2L, 3L, 4L, 5L}, vals.toArray(new Long[vals.size()]));
+  }
+}
diff --git a/user/test/com/google/gwt/emultest/java8/util/stream/StreamSupportTest.java b/user/test/com/google/gwt/emultest/java8/util/stream/StreamSupportTest.java
new file mode 100644
index 0000000..0ff6331
--- /dev/null
+++ b/user/test/com/google/gwt/emultest/java8/util/stream/StreamSupportTest.java
@@ -0,0 +1,109 @@
+/*
+ * 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.StreamSupport.doubleStream;
+import static java.util.stream.StreamSupport.intStream;
+import static java.util.stream.StreamSupport.longStream;
+import static java.util.stream.StreamSupport.stream;
+
+import com.google.gwt.emultest.java.util.EmulTestBase;
+
+import java.util.Spliterators;
+import java.util.stream.DoubleStream;
+import java.util.stream.IntStream;
+import java.util.stream.LongStream;
+import java.util.stream.Stream;
+
+/**
+ * Tests {@link java.util.stream.StreamSupport}.
+ */
+public class StreamSupportTest extends EmulTestBase {
+
+  public void testDoubleStream() {
+    DoubleStream doubles = doubleStream(
+        Spliterators.spliterator(new double[] {1d, 2d, 3d, 4d}, 0),
+        false
+    );
+
+    assertNotNull(doubles);
+    assertEquals(new double[]{1d, 2d, 3d}, doubles.limit(3).toArray());
+
+    doubles = doubleStream(() -> Spliterators.spliterator(new double[] {1d, 2d, 3d, 4d}, 0), 0,
+        false);
+
+    assertNotNull(doubles);
+    assertEquals(
+        new double[] {2d, 3d},
+        doubles.skip(1).limit(2).toArray()
+    );
+  }
+
+  public void testIntStream() {
+    IntStream ints = intStream(
+        Spliterators.spliterator(new int[] {1, 2, 3, 4}, 0),
+        false
+    );
+
+    assertNotNull(ints);
+    assertEquals(new int[] {1, 2, 3}, ints.limit(3).toArray());
+
+    ints = intStream(() -> Spliterators.spliterator(new int[] {1, 2, 3, 4}, 0), 0, false);
+
+    assertNotNull(ints);
+    assertEquals(
+        new int[] {2, 3},
+        ints.skip(1).limit(2).toArray()
+    );
+  }
+
+  public void testLongStream() {
+    LongStream longs = longStream(
+        Spliterators.spliterator(new long[] {1L, 2L, 3L, 4L}, 0),
+        false
+    );
+
+    assertNotNull(longs);
+    assertEquals(new long[] {1L, 2L, 3L}, longs.limit(3).toArray());
+
+    longs = longStream(() -> Spliterators.spliterator(new long[] {1L, 2L, 3L, 4L}, 0), 0, false);
+
+    assertNotNull(longs);
+    assertEquals(
+        new long[] {2L, 3L},
+        longs.skip(1).limit(2).toArray()
+    );
+  }
+
+  public void testStream() {
+    Stream<String> strings = stream(
+        Spliterators.spliterator(new String[] {"a", "b", "c", "d"}, 0),
+        false
+    );
+
+    assertNotNull(strings);
+    assertEquals(new String[] {"a", "b", "c"}, strings.limit(3).toArray());
+
+    strings = stream(() -> Spliterators.spliterator(new String[] {"a", "b", "c", "d"}, 0), 0, false);
+
+    assertNotNull(strings);
+    assertEquals(
+        new String[] {"b", "c"},
+        strings.skip(1).limit(2).toArray()
+    );
+  }
+}
diff --git a/user/test/com/google/gwt/emultest/java8/util/stream/StreamTest.java b/user/test/com/google/gwt/emultest/java8/util/stream/StreamTest.java
new file mode 100644
index 0000000..d46bfaf
--- /dev/null
+++ b/user/test/com/google/gwt/emultest/java8/util/stream/StreamTest.java
@@ -0,0 +1,627 @@
+/*
+ * 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.Arrays.asList;
+
+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.Iterator;
+import java.util.List;
+import java.util.Optional;
+import java.util.Spliterator;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+import java.util.stream.DoubleStream;
+import java.util.stream.IntStream;
+import java.util.stream.LongStream;
+import java.util.stream.Stream;
+
+/**
+ * Tests {@link Stream}.
+ */
+public class StreamTest extends EmulTestBase {
+
+  public void testEmptyStream() {
+    Stream<Object> empty = Stream.empty();
+    assertEquals(0, empty.count());
+    try {
+      empty.count();
+      fail("second terminal operation should have thrown IllegalStateEx");
+    } catch (IllegalStateException expected) {
+      // expected
+    }
+
+    assertEquals(0, Stream.empty().limit(2).collect(Collectors.toList()).size());
+    assertEquals(0, Stream.empty().count());
+    assertEquals(0, Stream.empty().limit(2).count());
+
+    assertFalse(Stream.empty().findFirst().isPresent());
+    assertFalse(Stream.empty().findAny().isPresent());
+    assertFalse(Stream.<String>empty().max(Comparator.naturalOrder()).isPresent());
+    assertFalse(Stream.<String>empty().min(Comparator.naturalOrder()).isPresent());
+    assertTrue(Stream.empty().allMatch(item -> true));
+    assertFalse(Stream.empty().anyMatch(item -> true));
+    assertTrue(Stream.empty().noneMatch(item -> false));
+    assertFalse(Stream.empty().iterator().hasNext());
+    assertFalse(Stream.empty().spliterator().tryAdvance(a -> fail("should not advance")));
+    Stream.empty().spliterator().forEachRemaining(a -> fail("should not advance"));
+    assertEquals(new Object[0], Stream.empty().toArray());
+    assertEquals(new Object[0], Stream.empty().toArray(Object[]::new));
+  }
+
+  public void testStreamOfOne() {
+    Supplier<Stream<String>> one = () -> Stream.of("");
+    assertEquals(Collections.singletonList(""), one.get().collect(Collectors.toList()));
+    assertEquals(1L, one.get().count());
+    assertEquals("", one.get().findFirst().get());
+    assertEquals("", one.get().findAny().get());
+  }
+
+  public void testBuilder() {
+    Supplier<Stream<String>> s = () -> Stream.<String>builder()
+        .add("1")
+        .add("3")
+        .add("2")
+        .build();
+
+    Optional<String> max = s.get().filter(str -> !str.equals("3")).max(Comparator.naturalOrder());
+    assertTrue(max.isPresent());
+    assertEquals("2", max.get());
+
+    max = s.get().max(Comparator.reverseOrder());
+    assertTrue(max.isPresent());
+    assertEquals("1", max.get());
+
+    Stream.Builder<Object> builder = Stream.builder();
+    Stream<Object> built = builder.build();
+    assertEquals(0, built.count());
+    try {
+      builder.build();
+      fail("build() after build() should fail");
+    } catch (IllegalStateException expected) {
+      // expected
+    }
+    try {
+      builder.add("asdf");
+      fail("add() after build() should fail");
+    } catch (IllegalStateException expected) {
+      // expected
+    }
+  }
+
+  public void testConcat() {
+    Supplier<Stream<String>> adbc = () -> Stream.concat(Stream.of("a", "d"), Stream.of("b", "c"));
+
+    assertEquals(new String[]{"a", "d", "b", "c"}, adbc.get().toArray(String[]::new));
+    assertEquals(new String[]{"a", "b", "c", "d"}, adbc.get().sorted().toArray(String[]::new));
+
+    List<String> closed = new ArrayList<>();
+    Stream<String> first = Stream.of("first").onClose(() -> closed.add("first"));
+    Stream<String> second = Stream.of("second").onClose(() -> closed.add("second"));
+
+    Stream<String> concat = Stream.concat(first, second);
+
+    // read everything, make sure we saw it all and didn't close automatically
+    String collectedAll = concat.collect(Collectors.joining());
+    assertEquals("firstsecond", collectedAll);
+    assertEquals(0, closed.size());
+
+    concat.close();
+    assertEquals(Arrays.asList("first", "second"), closed);
+  }
+
+  public void testIterate() {
+    assertEquals(
+        new Integer[]{10, 11, 12, 13, 14},
+        Stream.iterate(0, i -> i + 1)
+            .skip(10)
+            .limit(5)
+            .toArray(Integer[]::new)
+    );
+  }
+
+  public void testGenerate() {
+    // infinite, but if you limit it is already too short to skip much
+    assertEquals(new Integer[]{}, Stream.generate(makeGenerator())
+        .limit(4)
+        .skip(5)
+        .toArray(Integer[]::new));
+
+    assertEquals(new Integer[]{10, 11, 12, 13, 14}, Stream.generate(makeGenerator())
+        .skip(10)
+        .limit(5)
+        .toArray(Integer[]::new));
+  }
+
+  private Supplier<Integer> makeGenerator() {
+    return new Supplier<Integer>() {
+      int next = 0;
+
+      @Override
+      public Integer get() {
+        return next++;
+      }
+    };
+  }
+
+  public void testSpliterator() {
+    final String[] values = new String[] {"a", "b", "c"};
+
+    Spliterator<String> spliterator = Stream.of(values).spliterator();
+    assertEquals(3, spliterator.estimateSize());
+    assertEquals(3, spliterator.getExactSizeIfKnown());
+
+    List<String> actualValues = new ArrayList<>();
+    while (spliterator.tryAdvance(actualValues::add)) {
+      // work is all done in the condition
+    }
+
+    assertEquals(asList(values), actualValues);
+  }
+
+  public void testIterator() {
+    final String[] values = new String[] {"a", "b", "c"};
+
+    List<String> actualValues = new ArrayList<String>();
+    Iterator<String> iterator = Stream.of(values).iterator();
+    while (iterator.hasNext()) {
+      actualValues.add(iterator.next());
+    }
+    assertEquals(asList(values), actualValues);
+  }
+
+  public void testForEach() {
+    final String[] values = new String[] {"a", "b", "c"};
+
+    List<String> actualValues = new ArrayList<>();
+    Stream.of(values).forEach(actualValues::add);
+    assertEquals(asList(values), actualValues);
+  }
+
+  // toArray
+  public void testToArray() {
+    assertEquals(
+        new Object[]{"a", "b"},
+        asList("a", "b").stream().toArray()
+    );
+    assertEquals(
+        new String[]{"a", "b"},
+        asList("a", "b").stream().toArray(String[]::new)
+    );
+  }
+
+  // reduce
+  public void testReduce() {
+    String reduced = Stream.of("a", "b", "c").reduce("", String::concat);
+    assertEquals("abc", reduced);
+
+    reduced = Stream.<String>of().reduce("initial", String::concat);
+    assertEquals("initial", reduced);
+
+    Optional<String> maybe = Stream.of("a", "b", "c").reduce(String::concat);
+    assertTrue(maybe.isPresent());
+    assertEquals("abc", maybe.get());
+    maybe = Stream.<String>of().reduce(String::concat);
+    assertFalse(maybe.isPresent());
+
+    reduced = Stream.of("a", "b", "c").reduce("", String::concat, String::concat);
+    assertEquals("abc", reduced);
+  }
+
+  public void testCollect() {
+    final String[] values = new String[] {"a", "b", "c"};
+
+    String collectedString = Stream.of(values).collect(StringBuilder::new,
+        StringBuilder::append,
+        StringBuilder::append).toString();
+    assertEquals("abc", collectedString);
+
+    List<String> collectedList = Stream.of(values).collect(Collectors.toList());
+    assertEquals(asList(values), collectedList);
+  }
+
+  public void testFilter() {
+    // unconsumed stream never runs filter
+    boolean[] data = {false};
+    Stream.of(1, 2, 3).filter(i -> data[0] |= true);
+    assertFalse(data[0]);
+
+    // one result
+    assertEquals(
+        Collections.singletonList("a"),
+        Stream.of("a", "b", "c", "d", "c").filter(a -> a.equals("a")).collect(Collectors.toList())
+    );
+    // zero results
+    assertEquals(
+        Collections.emptyList(),
+        Stream.of("a", "b", "c", "d", "c").filter(a -> false).collect(Collectors.toList())
+    );
+    // two results
+    assertEquals(
+        asList("c3", "c5"),
+        Stream.of("a1", "b2", "c3", "d4", "c5")
+            .filter(a -> a.startsWith("c"))
+            .collect(Collectors.toList())
+    );
+    // all
+    assertEquals(
+        asList("a", "b", "c", "d", "c"),
+        Stream.of("a", "b", "c", "d", "c").filter(a -> true).collect(Collectors.toList())
+    );
+  }
+
+  public void testMap() {
+    // unconsumed stream never runs map
+    boolean[] data = {false};
+    Stream.of(1, 2, 3).map(i -> data[0] |= true);
+    assertFalse(data[0]);
+
+    assertEquals(
+        asList("#1", "#2", "#3"),
+        Stream.of(1, 2, 3).map(i -> "#" + i).collect(Collectors.toList())
+    );
+  }
+
+  public void testPeek() {
+    // unconsumed stream never peeks
+    boolean[] data = {false};
+    Stream.of(1, 2, 3).peek(i -> data[0] |= true);
+    assertFalse(data[0]);
+
+    // make sure we saw it all in order
+    List<String> items = asList("a", "b", "c");
+    List<String> peeked = new ArrayList<>();
+    items.stream().peek(peeked::add).forEach(item -> {
+      // deliberately do nothing, just run
+    });
+    assertEquals(items, peeked);
+  }
+
+  // same impl, no parallel in browser
+  public void testFindFirstOrAny() {
+    Optional<String> any = Stream.of("a", "b").findAny();
+    assertTrue(any.isPresent());
+    assertEquals("a", any.get());
+  }
+
+  public void testAnyMatch() {
+    // all
+    assertTrue(Stream.of("a", "b").anyMatch(s -> true));
+
+    // some
+    assertTrue(Stream.of("a", "b").anyMatch(s -> s.equals("a")));
+
+    // none
+    assertFalse(Stream.of("a", "b").anyMatch(s -> false));
+  }
+
+  public void testAllMatch() {
+    // all
+    assertTrue(Stream.of("a", "b").allMatch(s -> true));
+
+    // some
+    assertFalse(Stream.of("a", "b").allMatch(s -> s.equals("a")));
+
+    // none
+    assertFalse(Stream.of("a", "b").allMatch(s -> false));
+  }
+
+  public void testNoneMatch() {
+    // all
+    assertFalse(Stream.of("a", "b").noneMatch(s -> true));
+
+    // some
+    assertFalse(Stream.of("a", "b").noneMatch(s -> s.equals("a")));
+
+    // none
+    assertTrue(Stream.of("a", "b").noneMatch(s -> false));
+  }
+
+  public void testFlatMap() {
+    assertEquals(0, Stream.<Stream<String>>empty().flatMap(Function.identity()).count());
+    assertEquals(0, Stream.of(Stream.<String>empty()).flatMap(Function.identity()).count());
+    assertEquals(0, Stream.of(Stream.of()).flatMap(Function.identity()).count());
+    assertEquals(1, Stream.of(Stream.of("")).flatMap(Function.identity()).count());
+
+    Stream<Stream<String>> strings = Stream.of(Stream.of("a", "b"), Stream.empty(), Stream.of("c"));
+
+    assertEquals(
+        asList("a", "b", "c"),
+        strings.flatMap(Function.identity()).collect(Collectors.toList())
+    );
+  }
+  public void testMapToPrimitives() {
+    Supplier<Stream<String>> s = () -> Stream.of("1", "2", "10");
+
+    assertEquals(
+        new int[]{1, 2, 10},
+        s.get().mapToInt(Integer::parseInt).toArray()
+    );
+
+    assertEquals(
+        new long[]{1, 2, 10},
+        s.get().mapToLong(Long::parseLong).toArray()
+    );
+
+    assertEquals(
+        new double[]{1, 2, 10},
+        s.get().mapToDouble(Double::parseDouble).toArray()
+    );
+  }
+
+  public void testFlatMapToPrimitives() {
+    assertEquals(0, Stream.<IntStream>empty().flatMapToInt(Function.identity()).count());
+    assertEquals(0, Stream.of(IntStream.empty()).flatMapToInt(Function.identity()).count());
+    assertEquals(0, Stream.of(IntStream.of()).flatMapToInt(Function.identity()).count());
+    assertEquals(1, Stream.of(IntStream.of(0)).flatMapToInt(Function.identity()).count());
+
+    Stream<IntStream> intStreams = Stream.of(
+        IntStream.of(1, 2),
+        IntStream.empty(),
+        IntStream.of(5)
+    );
+    assertEquals(
+        new int[]{1, 2, 5},
+        intStreams.flatMapToInt(Function.identity()).toArray()
+    );
+
+    Stream<LongStream> longStreams = Stream.of(
+        LongStream.of(1, 2),
+        LongStream.empty(),
+        LongStream.of(5)
+    );
+    assertEquals(
+        new long[]{1, 2, 5},
+        longStreams.flatMapToLong(Function.identity()).toArray()
+    );
+
+    Stream<DoubleStream> doubleStreams = Stream.of(
+        DoubleStream.of(1, 2),
+        DoubleStream.empty(),
+        DoubleStream.of(5)
+    );
+    assertEquals(
+        new double[]{1, 2, 5},
+        doubleStreams.flatMapToDouble(Function.identity()).toArray()
+    );
+  }
+
+  public void testDistinct() {
+    List<String> distinct = asList("a", "b", "c", "b").stream()
+        .distinct()
+        .collect(Collectors.toList());
+    assertEquals(3, distinct.size());
+    assertTrue(distinct.contains("a"));
+    assertTrue(distinct.contains("b"));
+    assertTrue(distinct.contains("c"));
+  }
+
+  public void testSorted() {
+    List<String> sorted = asList("c", "a", "b").stream().sorted().collect(Collectors.toList());
+    List<String> reversed = asList("c", "a", "b").stream()
+        .sorted(Comparator.reverseOrder())
+        .collect(Collectors.toList());
+
+    assertEquals(asList("a", "b", "c"), sorted);
+    assertEquals(asList("c", "b", "a"), reversed);
+  }
+
+  public void testMinMax() {
+    Supplier<Stream<String>> stream = () -> Stream.of("b", "c", "d", "a");
+
+    assertEquals("a", stream.get().min(Comparator.naturalOrder()).orElse(null));
+    assertEquals("d", stream.get().min(Comparator.reverseOrder()).orElse(null));
+    assertEquals("a", stream.get().max(Comparator.reverseOrder()).orElse(null));
+    assertEquals("d", stream.get().max(Comparator.naturalOrder()).orElse(null));
+
+    assertFalse(stream.get().filter(a -> false).max(Comparator.naturalOrder()).isPresent());
+    assertFalse(stream.get().filter(a -> false).min(Comparator.naturalOrder()).isPresent());
+  }
+
+  public void testCountLimitSkip() {
+    Supplier<Stream<String>> stream = () -> asList("a", "b", "c", "d").stream();
+
+    assertEquals(4, stream.get().count());
+
+    assertEquals(4, stream.get().limit(4).count());
+    assertEquals(4, stream.get().limit(5).count());
+    assertEquals(3, stream.get().limit(3).count());
+
+    assertEquals(3, stream.get().skip(1).limit(3).count());
+
+    assertEquals(2, stream.get().limit(3).skip(1).count());
+
+    assertEquals(1, stream.get().skip(3).count());
+
+    assertEquals(asList("c", "d"), stream.get().skip(2).limit(3).collect(Collectors.toList()));
+    assertEquals(Collections.singletonList("c"), stream.get()
+        .skip(2)
+        .limit(1)
+        .collect(Collectors.toList()));
+
+    assertEquals(Collections.singletonList("d"), stream.get()
+        .skip(3)
+        .collect(Collectors.toList()));
+    assertEquals(Collections.emptyList(), stream.get()
+        .skip(5)
+        .collect(Collectors.toList()));
+
+    assertEquals(asList("a", "b"), stream.get()
+        .limit(2)
+        .collect(Collectors.toList()));
+
+    assertEquals(Collections.singletonList("b"), stream.get()
+        .limit(2)
+        .skip(1)
+        .collect(Collectors.toList()));
+  }
+
+  // This frustrating test was written first on the JVM stream to discover the basic behavior before
+  // trying to implement it in GWT. As far as I can tell, none of this is clearly described in
+  // javadoc. Also note that it is *not* required to use the returned stream from calling onClose
+  public void testCloseQuirks() {
+    // all subclasses use the same close()/onClose(...) impl, just test once with Stream.empty()
+
+    Stream<Object> s = Stream.of(1);
+    s.close();
+    // allow multiple close
+    s.close();
+
+    // Add a handler, close, and attempt to re-close - handler only runs the one time
+    int[] calledCount = {0};
+    s = Stream.empty();
+    s.onClose(() -> calledCount[0]++);
+    // shouldn't have been called yet
+    assertEquals(0, calledCount[0]);
+    s.close();
+    // called once
+    assertEquals(1, calledCount[0]);
+    s.close();
+    // not called again on subsequent closes
+    assertEquals(1, calledCount[0]);
+
+    // Add a handler after close, and re-close, the handler will only go off after the _second_
+    // close
+    calledCount[0] = 0;
+    s = Stream.of(1);
+    s.close();
+    s = s.onClose(() -> calledCount[0]++);
+    // shouldn't have been called yet
+    assertEquals(0, calledCount[0]);
+    s.close();
+    // frustratingly, the JVM apparently permits each handler when added to let the stream be closed
+    // _again_
+    assertEquals(1, calledCount[0]);
+
+    // Adding yet another runnable and closing again demonstrates this - only the new one is run,
+    // the old ones are not
+    s = s.onClose(() -> calledCount[0]++);
+    s.close();
+    assertEquals(2, calledCount[0]);
+
+    // Add two handlers, ensure both are called, and neither called the second time the stream is
+    // closed
+    calledCount[0] = 0;
+    s = Stream.empty();
+    s = s.onClose(() -> calledCount[0]++);
+    s = s.onClose(() -> calledCount[0]++);
+    s.close();
+    assertEquals(2, calledCount[0]);
+    s.close();
+    assertEquals(2, calledCount[0]);
+  }
+
+  public void testClose() {
+    // terminate stream before closing, confirm that handler is called
+    Stream<Object> s = Stream.of("a", "b", "c");
+    int[] calledCount = {0};
+    s = s.onClose(() -> calledCount[0]++);
+
+    long count = s.count();
+    assertEquals(3, count);
+    assertEquals(0, calledCount[0]);
+
+    s.close();
+    assertEquals(1, calledCount[0]);
+
+    // terminate stream after closing, confirm that handler is called, and terminating fails
+    s = Stream.of("a", "b", "c");
+    calledCount[0] = 0;
+    s = s.onClose(() -> calledCount[0]++);
+
+    s.close();
+    assertEquals(1, calledCount[0]);
+
+    try {
+      s.count();
+      fail("Expected IllegalStateException");
+    } catch (IllegalStateException expected) {
+      // expected
+    }
+    assertEquals(1, calledCount[0]);
+  }
+
+  public void testCloseException() {
+    // Try a single exception, confirm we catch it
+    Stream<Object> s = Stream.of(1, 2, 3);
+
+    RuntimeException a = new RuntimeException("a");
+    s.onClose(() -> {
+      throw a;
+    });
+    try {
+      s.close();
+      fail("RuntimeException expected");
+    } catch (RuntimeException expected) {
+      assertSame(a, expected);
+      assertEquals(0, expected.getSuppressed().length);
+    }
+
+    // Throw an exception in two of the three handlers, confirm both arrive and the third was called
+    // correctly
+    s = Stream.of(1, 2, 3);
+
+    RuntimeException a2 = new RuntimeException("a");
+    IllegalStateException b = new IllegalStateException("b");
+    int[] calledCount = {0};
+    s
+        .onClose(() -> {
+          throw a2;
+        })
+        .onClose(() -> {
+          throw b;
+        })
+        .onClose(() -> calledCount[0]++);
+
+    try {
+      s.close();
+      fail("RuntimeException expected");
+    } catch (RuntimeException expected) {
+      assertSame(a2, expected);
+      assertEquals(1, expected.getSuppressed().length);
+      Throwable firstSuppressed = expected.getSuppressed()[0];
+      assertSame(b, firstSuppressed);
+    }
+    assertEquals(1, calledCount[0]);
+
+    // Throw the same exception instance twice, ensure it only arrives once
+    s = Stream.of(1, 2, 3);
+
+    RuntimeException t = new RuntimeException("a");
+    s
+        .onClose(() -> {
+          throw t;
+        })
+        .onClose(() -> {
+          throw t;
+        });
+
+    try {
+      s.close();
+      fail("RuntimeException expected");
+    } catch (RuntimeException expected) {
+      assertSame(t, expected);
+      assertEquals(0, expected.getSuppressed().length);
+    }
+  }
+}
\ No newline at end of file