bistrot
bistrot

Reputation: 103

How to combine two java8 stream operations - one terminal and one lazy - into a single operation?

I'm doing some "algebra" of Java 8's streams, that is, I'm trying to code a simple operation Op that takes two streams as inputs and yields another stream as a result.

So I have this simple code whose aim is to print the secund highest value in a serie of numbers :

import java.util.Arrays;
import java.util.stream.IntStream;

public class SecundHighestValue {

    public static void main(String[] args) {

        //setting the input parameters
        int [] numbers = {1, 2, 3, 4, 3, 4, 2, 1};

        IntStream S1 = Arrays.stream(numbers);
        IntStream S2 = Arrays.stream(new int[] {Arrays.stream(numbers).max().getAsInt()} );

        // setting the operation
        IntStream S3 = S1.filter(x-> x != S2.toArray()[0]); // doesn't work

        /*** does work  ***
        int  maxNumber = S2.toArray()[0];
        IntStream S3 = S1.filter(x-> x != maxNumber);
        */

        // accessing the operation's result stream S3
        int secundMaxNumber = S3.max().getAsInt();
        System.out.println("the secund highest value in the serie " +
                    Arrays.toString(numbers) + " is " + secundMaxNumber);   
    }
}

This program won't work, unless I split the one-line operation this way :

    int  maxNumber = S2.toArray()[0];
    IntStream S3 = S1.filter(x-> x != maxNumber);

Keeping the operation in one line will raise this exception :

Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed ...

I understand that it's related to the filter() method's inherent laziness. The API explains :

Stream operations are divided into intermediate (Stream-producing) operations and terminal (value- or side-effect-producing) operations. Intermediate operations are always lazy.

and in fact, the stack trace shows that the operation doesn't execute until I try to access its result in the next line.

Is this behaviour a flawed desing in java8 ? Is it a bug ? And most important, how can I keep the operation in one line and have it work ?

Upvotes: 0

Views: 719

Answers (4)

user_3380739
user_3380739

Reputation: 1254

Since the input is an int array, The solution provided by @azro is good enough to me. just second @Holger: don't have to define a new class:

final Supplier<int[]> supplier = () -> new int[] { Integer.MIN_VALUE, Integer.MIN_VALUE };
final ObjIntConsumer<int[]> accumulator = (a, i) -> {
    if (i > a[0]) {
        a[1] = a[0];
        a[0] = i;
    } else if (i != a[0] && i > a[1]) {
        a[1] = i;
    }
};

int secondMax = Arrays.stream(nums).collect(supplier, accumulator, (a, b) -> {})[1];

Or with the API provided in third-party library: abacus-common

int secondMax = IntStream.of(nums).distinct().kthLargest(2).get();

Upvotes: 1

Bojan Petkovic
Bojan Petkovic

Reputation: 2576

The reason that this does not work:

IntStream S3 = S1.filter(x-> x != S2.toArray()[0]);

is because S2 can only be acted on once. and filter recalculates it for every entry in S3.

Think of it filter as a for loop, and s2 as a value that can be only ready once. You can compare streams to System.in - once you read the value once you cannot re-read it. You have to get a new one.

A bit more information: The operation is not lazy since you have this line of code which makes it terminal:

secundMaxNumber = S3.max().getAsInt();

Side note: to get the Xth maxNumber, you can also just do: you do not need to use the stream multiple times.

S1.sorted().limit(x).skip(x-1).findFirst().getAsInt();

References:

https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html#limit-long-

https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html#skip-long-

Upvotes: 2

Holger
Holger

Reputation: 298213

If streaming over the source is possible and not expensive, like with arrays, you may just stream twice, like in azro’s answer:

int maxNumber = Arrays.stream(numbers).max().getAsInt();
int secondMaxNumber = Arrays.stream(numbers).filter(x-> x != maxNumber).max().getAsInt();

If streaming twice isn’t possible or expensive, you need a custom collector to get the second largest value efficiently, i.e. with holding only the necessary two values. E.g.

final class SecondMax {
    long max=Long.MIN_VALUE, semi=max;

    void add(int next) {
        if(next>semi) {
            if(next>max) {
                semi=max;
                max=next;
            }
            else if(next<max) {
                semi=next;
            }
        }
    }
    void merge(SecondMax other) {
        if(other.max>Long.MIN_VALUE) {
            add((int)other.max);
            if(other.semi>Long.MIN_VALUE) add((int)other.semi);
        }
    }
    OptionalInt get() {
        return semi>Long.MIN_VALUE? OptionalInt.of((int)semi): OptionalInt.empty();
    }
}

With this helper, you can get the value in a single stream operation:

OptionalInt secondMax = Arrays.stream(array)
  .collect(SecondMax::new, SecondMax::add, SecondMax::merge).get();

Upvotes: 6

azro
azro

Reputation: 54148

You have four lines :

 IntStream S1 = Arrays.stream(numbers);
 IntStream S2 = Arrays.stream(new int[] {Arrays.stream(numbers).max().getAsInt()} );
 int  maxNumber = S2.toArray()[0];
 IntStream S3 = S1.filter(x-> x != maxNumber);
 int secundMaxNumber = S3.max().getAsInt();

Same in 2 :

int  maxNumber = Arrays.stream(numbers).max().getAsInt();
int secundMaxNumber = Arrays.stream(numbers).filter(x-> x != maxNumber).max().getAsInt();

It's hard to re-use streams so better to do it in one-way, and better calculate the max in a variable and re-use to not calculate it each time

Upvotes: 2

Related Questions