如何将两个java8流操作 - 一个终端和一个懒惰 - 组合成一个操作?

时间:2017-03-10 15:08:44

标签: java functional-programming java-8 java-stream lazy-evaluation

我正在做一些Java 8流的“代数”,也就是说,我正在尝试编写一个简单的操作Op,它将两个流作为输入并产生另一个流。

所以我有这个简单的代码,其目的是在一系列数字中打印secund最高值:

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);   
    }
}

除非我以这种方式拆分单行操作,否则此程序将无法运行:

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

将操作保持在一行会引发此异常:

线程“main”中的异常java.lang.IllegalStateException:stream已经被操作或关闭 ...

我知道它与filter()方法固有的懒惰有关。 API解释说:

  

流操作分为中间(流生成)操作和终端(生成价值或副作用)操作。中间操作总是很懒惰。

实际上,堆栈跟踪显示在我尝试在下一行中访问其结果之前,操作不会执行。

这种行为在java8中是否有缺陷?这是一个错误吗?最重要的是,如何将操作保持在一行并使其工作?

4 个答案:

答案 0 :(得分:6)

如果通过源进行流式传输是可能的并且不是很昂贵,例如数组,您可以只流式传输两次,例如azro’s answer

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

如果无法进行两次流式传输或昂贵,则需要一个自定义收集器来有效地获得第二大值,即只保留必要的两个值。 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();
    }
}

使用此帮助程序,您可以在单个流操作中获取值:

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

答案 1 :(得分:2)

你有四行:

 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();

同样在2:

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

很难重新使用流,以便更好地单向执行,更好地计算变量中的最大值并重新使用,以便每次都不计算

答案 2 :(得分:2)

这不起作用的原因:

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

是因为S2只能执行一次。和过滤器重新计算S3中的每个条目。

将其视为for循环,将s2视为只能准备一次的值。 您可以将流与System.in进行比较 - 一旦您无法重新读取它,就会读取该值。你必须得到一个新的。

更多信息: 该操作不是懒惰的,因为你有这行代码使它成为终端:

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

附注:要获得Xth maxNumber,您也可以这样做:您不需要多次使用该流。

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

参考文献:

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-

答案 3 :(得分:1)

由于输入是一个int数组,@ azro提供的解决方案对我来说已经足够了。只是第二个@Holger:不必定义新类:

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];

或使用第三方库中提供的API:AbacusUtil

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