3

In the process to get my hands in Java 8 streams, the following exercise stopped me.

Given the IntStream.range(0, 6). Produce the String Stream below:

"0, 1"
"1, 2"
"2, 3"
"3, 4"
"4, 5"

I thought of using Collectors.collectAndThen to pass it to the good old list or array and loop to construct the list of strings as follows:

List<String> strgs = new ArrayList<>();
String prev = String.valueOf(nums[0]);
for (int i = 1; i < nums.length; i++) {
    strgs.add(prev+", "+String.valueOf(nums[i]));
    prev = String.valueOf(nums[i]);  
}

But it does not use the power of streams. I felt like Venkat Subramaniam says "I felt like taking a shower afterwards". I want to know how to apply the functional techniques so I can skip taking a shower after coding!

Also, I would like to avoid libraries like StreamEx or JavaRx, I want to stick to the plain Java 8 APIs.

Edit: @Tunaki, thank you for pointing the unclear wording in my question. It is a pair formed of two consecutive elements of the Stream. More concrete, a Stream like [1, 3, 5, 7, 9, ...] will be

"1, 3"
"3, 5"
"5, 7"
...

Edit 2

After saluting all the answers, Although my question is a duplicate to another question as pointed out by Tunaki. I want to extend a community discussion for the answer contributed by Bohemian. Although his answer was disliked by some, It raises a serious issue which is a reduce operation with side effects. My request to the community is to provide a reasonable counter valid technique to the issue. Thus I want to reuse Bohemian answer as follows:

Given the input: nums =new int[]{1,3,5,7,9}

Please consider the snippet below:

List<CharSequence> stringList = new ArrayList<>();
IntBinaryOperator reductionWithSideEffect = (int left, int right) -> {
        stringList.add(new StringBuilder().append(left).append(", ").append(right));
        return right;
};
Arrays.stream(nums)
        .reduce(reductionWithSideEffect);
System.out.println(String.join(", ", stringList));
Sym-Sym
  • 3,578
  • 1
  • 29
  • 30
  • 4
    Take a look at this question: http://stackoverflow.com/q/20470010/1743880. Put simply, it's not something the Stream API was designed for. – Tunaki Feb 28 '16 at 00:00
  • 4
    Where did this exercise come from? – Stuart Marks Feb 28 '16 at 01:27
  • @Stuart Marks, it is my adaptation of an old arrays exercise by which I wanted to challenge my Streams understanding. I highly applaud your answer in the question linked in Tunaki's comment. It is close to a streams junior like myself. – Sym-Sym Feb 28 '16 at 19:40
  • Thanks. I was wondering if somebody was trying to teaching streams with this exercise. – Stuart Marks Feb 28 '16 at 21:16
  • 1
    @StuartMarks, actually I will present very similar problem on the upcoming Java conference along with good and bad solutions :-) – Tagir Valeev Feb 29 '16 at 10:59
  • 1
    @TagirValeev Stack Overflow is definitely a good source of conference material. – Stuart Marks Feb 29 '16 at 16:06

4 Answers4

7

To my opinion the most clean way to solve this problem is to write a custom spliterator and create a Stream over it. It's not very hard if you don't need absolutely maximum performance and don't care about parallel processing (parallel stream will work, but inefficiently). Something like this would work:

public static <T, R> Stream<R> pairMap(BaseStream<T, ?> source,
        BiFunction<? super T, ? super T, ? extends R> mapper) {
    Spliterator<T> spltr = source.spliterator();
    long sourceSize = spltr.estimateSize();
    Spliterator<R> result = new Spliterators.AbstractSpliterator<R>(
            sourceSize > 0 && sourceSize < Long.MAX_VALUE ? sourceSize - 1 : sourceSize,
            spltr.characteristics() & (Spliterator.ORDERED | Spliterator.SIZED)) {
        T prev;
        boolean started;

        @Override
        public boolean tryAdvance(Consumer<? super R> action) {
            if (!started) {
                if (!spltr.tryAdvance(t -> prev = t))
                    return false;
                started = true;
            }
            return spltr.tryAdvance(t -> action.accept(mapper.apply(prev, prev = t)));
        }
    };
    return StreamSupport.stream(result, source.isParallel()).onClose(source::close);
}

Here the mapper is the function which creates the element of the new stream based on the pair of adjacent elements of the input stream.

Usage example:

pairMap(IntStream.range(0, 6), (a, b) -> a + ", " + b).forEach(System.out::println);

Output:

0, 1
1, 2
2, 3
3, 4
4, 5
Tagir Valeev
  • 97,161
  • 19
  • 222
  • 334
  • Thank you for your answer. It is one of the few times I see Spliterators used in a concise code. It is a great example for a spliterator beginner. – Sym-Sym Feb 28 '16 at 22:22
  • You could implement `tryAdvance` as a single statement, `return (started || (started=spltr.tryAdvance(t -> prev = t))) && spltr.tryAdvance(t -> action.accept(mapper.apply(prev, prev = t)));` but I guess, too many people would consider it too hard to read… – Holger Feb 29 '16 at 10:09
  • @Holger, surely, too many, including me. – Tagir Valeev Feb 29 '16 at 10:12
0

As Tunaki mentions in the comments, the Stream API isn't ideal for this use case. The best I could do was to write a stateful mapping function to remember what the last encountered element was.

Note - as Tagir Valeev points out in the comments, this solution breaks if the result stream is reverted to parallel mode by the caller using BaseStream.parallel().

public class StreamExercise {
    public static void main(String[] args) {
        f(IntStream.range(0, 6))
            .forEach(s -> System.out.println(s));

        System.out.println();

        f(IntStream.of(1,3,5,7,9))
            .forEach(s -> System.out.println(s));
    }

    private static Stream<String> f(IntStream input) {
        IntFunction<String> mapper = new IntFunction<String>() {
            private int last = -1;

            @Override
            public String apply(int value) {
                String result = last + ", " + value;
                last = value;
                return result;
            }

        };

        return input.sequential().mapToObj(mapper).skip(1);
    }
}

Output:

0, 1
1, 2
2, 3
3, 4
4, 5

1, 3
3, 5
5, 7
7, 9
Kevin K
  • 9,344
  • 3
  • 37
  • 62
  • Thank you Kevin for the answer, it just falls into place with my knowledge set. I only hope that some Streams Guru would feed us back on the legitimacy of using a stateful mapping function like in your answer. The discussion in the question http://stackoverflow.com/questions/20470010/collect-successive-pairs-from-a-stream also raises the same concern in the answer supplied by @assylias – Sym-Sym Feb 28 '16 at 20:28
  • @Sam: feedback from me (if I can be considered as Streams guru): using stateful mapping function is bad. As a rule of thumb: if Stream behaves incorrectly in parallel mode, then the solution is dirty. Note that `.sequential()` here does not help as I can turn the stream to parallel afterwards again: `f(IntStream.of(1,3,5,7,9)).parallel().forEachOrdered(System.out::println)`. – Tagir Valeev Feb 29 '16 at 06:10
  • @TagirValeev ah, I hadn't considered that. Seems I still have a lot to learn about streams! – Kevin K Feb 29 '16 at 06:39
0

Simple and Easy :

public static void main(String[] args) {
    generatePairs(Arrays.asList(1, 3, 5, 7, 9)).forEach(System.out::println);
}

public static <E> List<Pair<E>> generatePairs(List<E> src) {
    if (src.size() < 2) {
        throw new IllegalArgumentException("src.size must be greater than 1");
    }
    return IntStream.range(0, src.size() - 1)
            .mapToObj(i -> new Pair<>(src.get(i), src.get(i + 1)))
            .collect(Collectors.toList());
}

and the Pair class :

public final class Pair<E> {

    private E left;
    private E right;

    public Pair() {
    }

    public Pair(E left, E right) {
        this.left = left;
        this.right = right;
    }

    public E getLeft() {
        return left;
    }

    public void setLeft(E left) {
        this.left = left;
    }

    public E getRight() {
        return right;
    }

    public void setRight(E right) {
        this.right = right;
    }

    @Override
    public String toString() {
        return "{" + left +
                ", " + right +
                '}';
    }

}

EDIT :

Here is a complete example :

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.DoubleStream;
import java.util.stream.IntStream;
import java.util.stream.LongStream;

public final class Pair<L, R> implements Comparable<Pair<L, R>> {

    private final Comparator<? super L> comparator;

    private L left;
    private R right;

    public Pair() {
        comparator = null;
    }

    public Pair(Comparator<? super L> comparator) {
        this.comparator = comparator;
    }

    public Pair(L left, R right) {
        this(left, right, null);
    }

    public Pair(L left, R right, Comparator<? super L> comparator) {
        this.left = left;
        this.right = right;
        this.comparator = comparator;
    }

    public L getLeft() {
        return left;
    }

    public void setLeft(L left) {
        this.left = left;
    }

    public R getRight() {
        return right;
    }

    public void setRight(R right) {
        this.right = right;
    }

    @Override
    public int hashCode() {
        return Objects.hash(left, right);
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == this) {
            return true;
        }
        if (obj == null || !(obj instanceof Pair)) {
            return false;
        }
        Pair that = (Pair) obj;
        return Objects.equals(left, that.left) && Objects.equals(right, that.right);
    }

    @Override
    public String toString() {
        return "{" + left +
                ", " + right +
                '}';
    }

    @Override
    @SuppressWarnings("unchecked")
    public int compareTo(Pair<L, R> o) {
        return comparator == null ? ((Comparable<? super L>) left).compareTo(o.left) : comparator.compare(left, o.left);
    }

    public static abstract class Stream {

        private Stream() {
        }

        public final java.util.stream.Stream<Pair<Integer, Integer>> of(IntStream src) {
            return of(src.boxed());
        }

        public final java.util.stream.Stream<Pair<Long, Long>> of(LongStream src) {
            return of(src.boxed());
        }

        public final java.util.stream.Stream<Pair<Double, Double>> of(DoubleStream src) {
            return of(src.boxed());
        }

        public final <E> java.util.stream.Stream<Pair<E, E>> of(java.util.stream.Stream<E> src) {
            return of(src.collect(Collectors.toList()));
        }

        @SuppressWarnings("all")
        public abstract <E> java.util.stream.Stream<Pair<E, E>> of(E... src);

        public abstract <E> java.util.stream.Stream<Pair<E, E>> of(List<E> src);

        protected static void checkSize(int size) {
            if (size < 1) {
                throw new IllegalArgumentException("Empty source.");
            }
        }

        protected static <E> E getOrNull(E[] array, int index) {
            return index < array.length ? array[index] : null;
        }

        protected static <E> E getOrNull(List<E> list, int index) {
            return index < list.size() ? list.get(index) : null;
        }

        public static Stream chained() {
            return new ChainedPairStream();
        }

        public static Stream distinct() {
            return new DistinctPairStream();
        }

    }

    private static final class ChainedPairStream extends Stream {

        @SafeVarargs
        public final <E> java.util.stream.Stream<Pair<E, E>> of(E... src) {
            int length = src.length;
            checkSize(length);
            return IntStream.range(0, Math.max(1, length - 1)).mapToObj(i -> new Pair<>(src[i], getOrNull(src, i + 1)));
        }

        public final <E> java.util.stream.Stream<Pair<E, E>> of(List<E> src) {
            int size = src.size();
            checkSize(size);
            return IntStream.range(0, Math.max(1, size - 1)).mapToObj(i -> new Pair<>(src.get(i), getOrNull(src, i + 1)));
        }

    }

    private static final class DistinctPairStream extends Stream {

        @SafeVarargs
        public final <E> java.util.stream.Stream<Pair<E, E>> of(E... src) {
            int length = src.length;
            checkSize(length);
            return IntStream.iterate(0, i -> i + 2)
                    .limit((long) Math.ceil(length / 2.0))
                    .mapToObj(i -> new Pair<>(src[i], getOrNull(src, i + 1)));
        }

        public final <E> java.util.stream.Stream<Pair<E, E>> of(List<E> src) {
            int size = src.size();
            checkSize(size);
            return IntStream.iterate(0, i -> i + 2)
                    .limit((long) Math.ceil(size / 2.0))
                    .mapToObj(i -> new Pair<>(src.get(i), getOrNull(src, i + 1)));
        }

    }

    public static void main(String[] args) {
        Pair.Stream.distinct().of(1, 2, 3, 4, 5, 6).forEach(System.out::println);

        Pair.Stream.chained().of(Arrays.asList(1, 3, 5, 7, 9)).forEach(System.out::println);

        Pair.Stream.chained().of(Arrays.stream(new int[]{0, 2, 4, 6, 8})).forEach(System.out::println);
        Pair.Stream.distinct().of(Arrays.stream(new int[]{0, 2, 4, 6, 8})).forEach(System.out::println);

        Pair.Stream.distinct().of(IntStream.range(0, 6)).forEach(System.out::println);
    }

}
FaNaJ
  • 1,329
  • 1
  • 16
  • 39
  • Thank you for the answer. It is looks like an honest adaptation of @StuartMarks answer in the question http://stackoverflow.com/questions/20470010/collect-successive-pairs-from-a-stream. I hope we can see some feedback to your answer from the community. – Sym-Sym Feb 28 '16 at 22:30
  • @Sam, feedback: this solution requires random-access source. For the streaming input it first dumps it to the intermediate `List` unnecessarily wasting memory. If it's acceptable limitation, then the solution is ok. At least it does not violate the specification. – Tagir Valeev Feb 29 '16 at 06:04
-1

I think the shortest code to get the job done is:

IntStream.range(0, 6).reduce((a, b) -> {System.out.println(a + ", " + b); return b;});

Since the above (working) code is causing debate, this then is a simple, readable solution that uses the API acceptably:

class PairPrinter implements IntConsumer {
    private boolean set;
    private int previous;

    public void accept(int i) {
        if (set) {
            System.out.println(previous + ", " + i);
        }
        set = true;
        previous = i;
    }
}

PairPrinter printer = new PairPrinter();
IntStream.range(0, 6).forEach(printer);

Which (also) produces the desired result.

Bohemian
  • 412,405
  • 93
  • 575
  • 722
  • Yep, but It doesn't work for `Stream`s like `[1, 3, 5, 7, 9, ...]` or `[0, 2, 4, 6, 8, ...]` – FaNaJ Feb 28 '16 at 08:13
  • 1
    @FaNaJ I just realised that the general solution is even simpler - see edited answer – Bohemian Feb 28 '16 at 08:27
  • 3
    That's an abuse of the API. While `(a,b)->b` is a correct function for `reduce`, side-effect free and associative, the print statement is not. It obviously will fail in parallel mode but even in sequential operation, it just happens to work as a side effect of how it is implemented. – Holger Feb 28 '16 at 11:27
  • 3
    In addition to absolutely correct @Holger comment as far as I understand, OP wants to have a Stream of pairs (to be able to perform any other stream ops there), not just the reduction. – Tagir Valeev Feb 28 '16 at 12:13
  • @tagir reduce() gives you exactly that: effectively a stream of pairs. And what's the problem here? The question clearly states *given IntStream.range(...)* which is guaranteed to be a **sequential** stream, so the descriptive word "abuse" is unwarranted and emotive. The fact is that this code is rock solid and completely determinant. – Bohemian Feb 28 '16 at 19:45
  • @holger this question states `IntStream.range()` as the source of the stream. The javadoc states *Returns a sequential ordered IntStream*, so parallel stream issues are not relevant. This code is determinant and works. Instead of "abuse" why not use the word "creative"? – Bohemian Feb 28 '16 at 19:49
  • 1
    @Holger, Thank you for bringing this up. I confess that I feel more comfortable abusing stateless streams and falling back to side-effects. This technique gives me a means to apply the imperative style within Streams, which is the comfort zone for many. I understand it looks unethical from an academic viewpoint but it looks creative from a pragmatic one. I think, at the end of the day, developers under the deadlines stress will follow Bohemian. – Sym-Sym Feb 28 '16 at 20:09
  • 2
    @Sam If you like your side effects, then feel free to write programs with side-effects -- but then you probably should just not use streams at all. Why not? Because _reading code_ is more important than _writing code_. It may be "in your comfort zone" to code this way, but by using the libraries improperly, you make things harder for readers, who may reasonably assume that you've used the libraries correctly. – Brian Goetz Feb 28 '16 at 21:09
  • @BrianGoetz thank you for your input which I totally believe in. We suffered for so many years reading others code. It is time to paradigm shift, although painful. My humble request is pointed out in my Edit#2 in the question, I hope it is a reasonable request! – Sym-Sym Feb 28 '16 at 21:57
  • @BrianGoetz I can read it, and the code is so small that the "side effect" is plainly visible in the lambda. Nevertheless, I've posted a simple alternative (using a stateful consumer) that adheres to acceptable usage of the stream API. – Bohemian Feb 28 '16 at 23:16
  • @Bohemian: of course, it works here. And I guess, no-one would ever carelessly take this code and use it in a different context, and no-one would ever use my code containing this broken 2-digit year storage in the year 2000, and anyway, after all, it’s so easy to get rid of a habit once we encounter its limitations, so why should we care *now*… – Holger Feb 29 '16 at 09:47
  • @Holger, for me to conclude a lesson and apply on the job. Should I understand that Plain Streams have to be pure stateless all along its (1) generation (2) intermediate operations (3) terminal operations. In case I can not help but to have a state I should look around for third party libraries, otherwise look for a solution which is developed using Spliterators!? – Sym-Sym Feb 29 '16 at 18:31
  • 1
    @Sam: not always. See, `BufferedReader.lines()` surely isn’t a stateless source and the `forEach` based solution here is valid but not a stateless terminal operation (which use case would a side-effect free `Consumer` have?). For intermediate operations, it’s usually recommended to be stateless, so the minimum is to put a comment on them, when they aren’t (and trying to find other solutions first). But the *reduction* operation used here is a special offense as the function should be stateless and *associative*, that means `(a op b) op c` must produce the same result as `a op (b op c)`. – Holger Feb 29 '16 at 18:50
  • @Holger, would you please recommend a book or a reference I can fall back to for the mathematical basis of terminal operations? Your answer suffices me as far as (1) generation and (2) intermediate operations but I need a reference to elaborate on (3) terminal operations characteristics! – Sym-Sym Feb 29 '16 at 19:08
  • @Sam: no book, but see [Reduction](https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html#Reduction) and [Associativity](https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html#Associativity) in the [Stream API’s package documentation](https://docs.oracle.com/javase/8/docs/api/?java/util/stream/package-summary.html) and, of course, the [documentation of the `Stream` methods](https://docs.oracle.com/javase/8/docs/api/?java/util/stream/Stream.html) itself. You only have to read carefully, a single word may imply an important characterization. – Holger Feb 29 '16 at 19:27
  • @Holger, on it. Appreciated! – Sym-Sym Feb 29 '16 at 19:29