Reputation: 103
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
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
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
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
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