Java Streams - 过滤先前过滤的值

时间:2015-08-19 17:31:42

标签: java filter lambda java-8 java-stream

我正在尝试使用Java的Streams并试图找出可能的内容以及它们的优点和缺点。目前我正在尝试使用流来实现Eratosthenes的Sieve,但似乎无法找到循环使用先前过滤的值而不将其存储在单独集合中的好方法。

我想要完成这样的事情:

IntStream myStream = IntStream.range(0,3);
myStream.filter(s -> {
    System.out.print("[filtering "+s+"] ");
    myStream.forEach(q -> System.out.print(q+", "));
    System.out.println();
    return true; //eventually respond to values observed on the line above
});

具有所需的输出:

[filtering 0] 
[filtering 1] 0, 
[filtering 2] 0, 1, 
[filtering 3] 0, 1, 2, 

请注意,在过滤每个新值时,会观察到所有先前过滤的值。这样可以轻松实现Eratosthenes的Sieve,因为我可以过滤掉所有非素数值,并为每个新值检查对所有先前通过素数过滤器的数字的可分性。

但是,上面的示例在NetBeans中给出了一个错误:

local variables referenced from a lambda expression must be final or effectively final

这似乎是因为我在已经作用于myStream的过滤器中引用myStream。是否有任何解决此错误的好方法(即,制作仅包含到目前为止已过滤的值的流的最终副本),或者是否有更好的方法解决此类问题而不使用单独的集合来存储值?

4 个答案:

答案 0 :(得分:3)

我设法使用Eratosthenes筛子创建了无限from datetime import datetime import pandas as pd pd.core.format.header_style = None # <--- Workaround for header formatting dt = datetime.now() d = datetime.date(datetime.now()) df1 = pd.DataFrame([{'c1': 'alpha', 'c2': 1}, {'c1': 'beta', 'c2': 2}]) df2 = pd.DataFrame([{'c1': dt, 'c2': d}, {'c1': dt, 'c2': d}]) with pd.ExcelWriter('output.xlsx') as writer: writer.date_format = None # <--- Workaround for date formatting writer.datetime_format = None # <--- this one for datetime df1.to_excel(writer,'Sheet1') df2.to_excel(writer,'Sheet2') 素数,但它实际上并没有使用过去的值。相反,它删除了尾部中的素数的倍数(以懒惰的方式,因为尾部是无限的),就像原始的Eratosthenes算法的Sieve一样。为此,我使用Stream作为辅助(因为Iterator只能使用一次)并为流实现了Stream

lazyConcat

我的Eratosthenes溪流看起来像这样:

class StreamUtils {
    public static IntStream fromIterator(PrimitiveIterator.OfInt it) {
        return StreamSupport.intStream(
                Spliterators.spliteratorUnknownSize(it, Spliterator.ORDERED), false);
    }

    public static IntStream lazyConcat(Supplier<IntStream> a, Supplier<IntStream> b) {
        return StreamSupport.intStream(new Spliterator.OfInt() {
            boolean beforeSplit = true;
            Spliterator.OfInt spliterator;

            @Override
            public OfInt trySplit() {
                return null;
            }

            @Override
            public long estimateSize() {
                return Long.MAX_VALUE;
            }

            @Override
            public int characteristics() {
                return Spliterator.ORDERED;
            }

            @Override
            public boolean tryAdvance(IntConsumer action) {
                boolean hasNext;
                if (spliterator == null) {
                    spliterator = a.get().spliterator();
                }
                hasNext = spliterator.tryAdvance(action);
                if (!hasNext && beforeSplit) {
                    beforeSplit = false;
                    spliterator = b.get().spliterator();
                    hasNext = spliterator.tryAdvance(action);
                }
                return hasNext;
            }
        }, false);
    }
}

然后我们可以这样使用它:

class Primes {
    public static IntStream stream() {
        return sieve(IntStream.iterate(2, n -> n + 1));
    }

    private static IntStream sieve(IntStream s) {
        PrimitiveIterator.OfInt it = s.iterator();
        int head = it.nextInt();
        IntStream tail = StreamUtils.fromIterator(it);
        return StreamUtils.lazyConcat(
                () -> IntStream.of(head),
                () -> sieve(tail.filter(n -> n % head != 0)));
    }
}

输出:

  

[2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71]

我认为这是一个很好的练习,但它看起来效率很低,而且根本不适合堆栈。

答案 1 :(得分:2)

您不能多次处理Stream,因此无法在过滤方法中调用myStream.forEach

您可以在过滤器中创建新的IntStream

请注意,您必须向外部Stream管道添加一些终端操作才能对其进行处理:

IntStream myStream = IntStream.range(0,4);
myStream.filter(s -> {
    System.out.print("[filtering "+s+"] ");
    IntStream.range(0,s).forEach(q -> System.out.print(q+", "));
    System.out.println();
    return true; //eventually respond to values observed on the line above
}).forEach(i->{});

这会产生:

[filtering 0] 
[filtering 1] 0, 
[filtering 2] 0, 1, 
[filtering 3] 0, 1, 2, 

答案 2 :(得分:1)

如果流是合适的工具,那么这是有争议的,但.filter()肯定不是。过滤器应该是无状态的,所以这个想法不应该首先出现。根据您的答案中的示例,收集器可能是一个可行的解决方案。

List<Integer> primes = IntStream.range(2, UPPER_BOUND)
  .collect(ArrayList::new,
          (list, number) -> { 
                for(int j=0; j < list.size(); j++) {
                    int prime = list.get(j);

                    if(prime > Math.sqrt(number)) {
                        break;
                    }

                    if(number % prime == 0) {
                        return;
                    }
                }

                list.add(number);
          },
          List::addAll);

ArrayList::new创建一个新列表,然后由消费者称为list。消费者被调用流中的每个元素,其中number是元素。

List::addAll仅适用于无法用于此算法的并行流。

答案 3 :(得分:0)

其他答案表明,我一直在尝试的方法是不可能的,并且必须使用单独的集合。

为了提供更完整的答案,我想使用流提供一种有效的方法解决这个问题,并将其与更传统的方法进行比较。

使用流列出素数(使用Eratosthenes筛选):

List<Integer> primes = new ArrayList<Integer>();

IntStream.iterate(2, i -> i + 1)
    .limit(UPPER_BOUND)
    .filter(i -> {
        for(int j=0; j<primes.size(); j++) {
            int prime = primes.get(j);

            if(prime > Math.sqrt(i)) {
                break;
            }

            if(i % prime == 0) {
                return false;
            }
        }
        return true;
    })
    .forEach(primes::add);

不使用流的传统,等效方法:

List<Integer> primes = new ArrayList<Integer>();

for(int i=2; i < UPPER_BOUND; i++) {
    boolean isPrime = true;

    for(int j=0; j<primes.size(); j++) {
        int prime = primes.get(j);

        if(prime > Math.sqrt(i)) {
            break;
        }

        if(i % prime == 0) {
            isPrime = false;
            break;
        }
    }

    if(isPrime) {
        primes.add(i);
    }
}

效果比较:

对每个函数的一些实验一致表明,传统方法实际上比在这种情况下使用流更快。与传统方法相比,流方法持续花费1.5倍的时间来查找低于100万的所有素数(在我的机器上平均分别为106ms和70ms)。

如果流的.parallel()函数可以轻松并行化问题,那么这种性能差异可能很容易弥补。但是,在这种情况下并行化并不容易,因为ArrayList不是线程安全的,并且很快会导致错误和/或不准确的结果。

<强>结论:

假设其他答案都是正确的,那么在Java中无法在同一个流的过滤器中过滤已过滤的数据。

可以使用流来处理列表素数。但是,等待比我自己更好的解决方案,目前最好坚持使用传统的无流方法。