Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8248166

Add new flatMap stream operation that is more amenable to pushing

XMLWordPrintable

    • Icon: CSR CSR
    • Resolution: Approved
    • Icon: P4 P4
    • 16
    • core-libs
    • None
    • source
    • minimal
    • Possible though unlikely risk of incompatibility with a custom implementation of the Stream API in a class that already contains a method of the same name but with a conflicting method signature.
    • Java API
    • SE

      Summary

      This CSR proposes to add a new flatMap-like operation <R> Stream<R> mapMulti(BiConsumer<? super T, ? super Consumer<R>> mapper) to the java.util.Stream class. This operation is more receptive to the pushing or yielding of values than the current implementation that internally assembles values (if any) into one or more streams. This addition includes the primitive variations of the operation i.e. mapMultiToInt, IntStream mapMulti, etc.

      Problem

      Currently, there is no efficient way to yield or push values to a stream. In particular, the cases of a one-to-zero and one-to-one mapping are not sufficiently covered by the existing API. The method flatMap, or a combination of methods filter and map, are the closest approximation to this operation but cannot accomplish this without the creation of one or more intermediate streams.

      Solution

      mapMulti avoids the overhead of creating one or more intermediate streams by pushing elements to a passed Consumer thus performing its operation locally in an eager way. Rather than creating streams containing the replacement elements, mapMulti allows elements to be generated one-by-one; it can replace a single element with zero or many elements and avoids the returning of null or an empty stream for the zero mapping case.

      Specification

      src/java.base/share/classes/java/util/stream/Stream.java

      +    /**
      +     * Returns a stream consisting of the results of replacing each element of
      +     * this stream with multiple elements, specifically zero or more elements.
      +     * Replacement is performed by applying the provided mapping function to each
      +     * element in conjunction with a {@linkplain Consumer consumer} argument
      +     * that accepts replacement elements. The mapping function calls the consumer
      +     * zero or more times to provide the replacement elements.
      +     *
      +     * <p>This is an <a href="package-summary.html#StreamOps">intermediate
      +     * operation</a>.
      +     * <p>If the {@linkplain Consumer consumer} argument is used outside the scope of
      +     * its application to the mapping function, the results are undefined.
      +     *
      +     * @implSpec
      +     * The default implementation invokes {@link #flatMap flatMap} on this stream,
      +     * passing a function that behaves as follows. First, it calls the mapper function
      +     * with a {@code Consumer} that accumulates replacement elements into a newly created
      +     * internal buffer. When the mapper function returns, it creates a stream from the
      +     * internal buffer. Finally, it returns this stream to {@code flatMap}.
      +     *
      +     * @apiNote
      +     * This method is similar to {@link #flatMap flatMap} in that it applies a one-to-many
      +     * transformation to the elements of the stream and flattens the result elements
      +     * into a new stream. This method is preferable to {@code flatMap} in the following
      +     * circumstances:
      +     * <ul>
      +     * <li>When replacing each stream element with a small (possibly zero) number of
      +     * elements. Using this method avoids the overhead of creating a new Stream instance
      +     * for every group of result elements, as required by {@code flatMap}.</li>
      +     * <li>When it is easier to use an imperative approach for generating result
      +     * elements than it is to return them in the form of a Stream.</li>
      +     * </ul>
      +     *
      +     * <p>If a lambda expression is provided as the mapper function argument, additional type
      +     * information maybe be necessary for proper inference of the element type {@code <R>} of
      +     * the returned stream. This can be provided in the form of explicit type declarations for
      +     * the lambda parameters or as an explicit type argument to the {@code mapMulti} call.
      +     *
      +     * <p><b>Examples</b>
      +     *
      +     * <p>Given a stream of {@code Number} objects, the following
      +     * produces a list containing only the {@code Integer} objects:
      +     * <pre>{@code
      +     *     Stream<Numbers> numbers = ... ;
      +     *     List<Integer> integers = numbers.<Integer>mapMulti((number, consumer) -> {
      +     *             if (number instanceof Integer)
      +     *                 consumer.accept((Integer) number);
      +     *         })
      +     *         .collect(Collectors.toList());
      +     * }</pre>
      +     *
      +     * <p>If we have an {@code Iterable<Object>} and need to recursively expand its elements
      +     * that are themselves of type {@code Iterable}, we can use {@code mapMulti} as follows:
      +     * <pre>{@code
      +     * class C {
      +     *     static void expandIterable(Object e, Consumer<Object> c) {
      +     *         if (e instanceof Iterable) {
      +     *             for (Object ie: (Iterable<?>) e) {
      +     *                 expandIterable(ie, c);
      +     *             }
      +     *         } else if (e != null) {
      +     *             c.accept(e);
      +     *         }
      +     *     }
      +     *
      +     *     public static void main(String[] args) {
      +     *         Stream<Object> stream = ...;
      +     *         Stream<Object> expandedStream = stream.mapMulti(C::expandIterable);
      +     *     }
      +     * }
      +     * }</pre>
      +     *
      +     * @param <R> The element type of the new stream
      +     * @param mapper a <a href="package-summary.html#NonInterference">non-interfering</a>,
      +     *               <a href="package-summary.html#Statelessness">stateless</a>
      +     *               function that generates replacement elements
      +     * @return the new stream
      +     * @see #flatMap flatMap
      +     * @since 16
      +     */
      +    default <R> Stream<R> mapMulti(BiConsumer<? super T, ? super Consumer<R>> mapper) {

      Specialized stream variants have also added for each of the primitive types (API documentation omitted):

      +    default IntStream mapMultiToInt(BiConsumer<? super T, ? super IntConsumer> mapper) {
      +    default DoubleStream mapMultiToDouble(BiConsumer<? super T, ? super DoubleConsumer> mapper) {
      +    default LongStream mapMultiToLong(BiConsumer<? super T, ? super LongConsumer> mapper) {

      src/java.base/share/classes/java/util/stream/IntStream.java

      +    default IntStream mapMulti(IntMapMultiConsumer mapper) {
      +    interface IntMapMultiConsumer { 
      +         void accept(int value, IntConsumer ic);
      +    }

      src/java.base/share/classes/java/util/stream/DoubleStream.java

      +    default DoubleStream mapMulti(DoubleMapMultiConsumer mapper) {
      +    interface DoubleMapMultiConsumer { 
      +         void accept(double value, DoubleConsumer dc);
      +    }

      src/java.base/share/classes/java/util/stream/LongStream.java

      +    default LongStream mapMulti(LongMapMultiConsumer mapper) {
      +    interface LongMapMultiConsumer { 
      +         void accept(long value, LongConsumer lc);
      +    }

      The full API documentation changes can be found in the specdiff: http://cr.openjdk.java.net/~pconcannon/8238286/specdiff/specout.04/overview-summary.html

      A zip containing the full specdiff will be attached when the CSR is finalized.

            pconcannon Patrick Concannon (Inactive)
            pconcannon Patrick Concannon (Inactive)
            Paul Sandoz, Stuart Marks
            Votes:
            0 Vote for this issue
            Watchers:
            4 Start watching this issue

              Created:
              Updated:
              Resolved: