处理某些流元素

时间:2016-05-17 13:36:54

标签: java concurrency java-8 java-stream

我有一个我必须实现的接口,需要Stream响应。我的源代码中的一些元素缺少数据,我必须使用源代码中的其他元素来查找它。它太大了,无法将所有元素保存在内存中。我可以编写一个例程来查找丢失的数据,但前提是我最后处理缺少数据的元素。

以下是我尝试解决此问题的简化示例。在这种情况下,我试图在addOne的另一个例程之后保存30个元素以便在最后处理。但是当程序试图从列表流中读取时,我收到一个ConcurrentModificationException。

package test;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;

public class TestStreams {
    private static List<Integer> savedForLater = new ArrayList<>();

    public static void main(String[] args) {
        Stream<Integer> origStream = Stream.of(10, 20, 30, 40, 50).filter(
                i -> saveThirtyForLater(i));
        Stream<Integer> savedForLaterStream = savedForLater.stream().map(
                i -> addOne(i));

        // Exception
        Stream.concat(origStream, savedForLaterStream).forEach(
            i -> System.out.println(i));

        // No Exception
        // origStream.forEach(i -> System.out.println(i));
        // savedForLaterStream.forEach(i -> System.out.println(i));
    }

    private static Integer addOne(Integer i) {
        return new Integer(i + 1);
    }

    private static boolean saveThirtyForLater(Integer i) {
        if (i == 30) {
            savedForLater.add(i);
            return false;
        }
        return true;
    }
}

此代码产生以下结果:

10
20
40
50
Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1380)
    at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:512)
    at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:502)
    at java.util.stream.StreamSpliterators$WrappingSpliterator.forEachRemaining(StreamSpliterators.java:312)
    at java.util.stream.Streams$ConcatSpliterator.forEachRemaining(Streams.java:742)
    at java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:580)
    at test.TestStreams.main(TestStreams.java:17)

我尝试使用线程安全列表,但它也没有产生所需的结果。

Per JavaDoc Stream.concat创建一个 lazily 连接流,其元素是第一个流的所有元素,后跟第二个流的所有元素。

流上的concat不应该调用List的流,直到它从中拉出一个对象,此时列表不会改变。

如果所有其他方法都失败了,我可以阅读该文件两次,但我真的想知道为什么这不起作用,如果有人有另外的想法来操纵流以避免第二次传递。

3 个答案:

答案 0 :(得分:2)

Streams很懒。除非您使用forEachcollect等终端操作,否则不会执行中间操作(例如filtermap)。

Stream<Integer> origStream = Stream.of(10, 20, 30, 40, 50).filter(
        i -> saveThirtyForLater(i));

执行上面一行代码后,您的savedForLater列表保持不变。只有在此流上使用终端操作后才会对其进行修改。

在最后一个表达式Stream.concat(origStream, savedForLaterStream).forEach(i -> System.out.println(i));中,您在流forEachorigStream上使用终端操作savedForLaterStream。前一个流将修改savedForLater列表,而后者实际上会迭代它 - 这就是您获得ConcurrentModificationException的原因。

filter方法中修改字段是一种非常糟糕的方法,它实际上违反了filter方法的约定。来自它的javadoc:

  

谓词 - 应用于每个元素的非干扰无状态谓词,以确定是否应包含它

您的谓词saveThirtyForLater不是无状态的,因为它会修改savedForLater列表。

<强>解决方案:

您可以单独处理这些流,而不是使用concat

origStream.forEach(i -> System.out.println(i));
savedForLaterStream.forEach(i -> System.out.println(i));

这些产生了预期的结果:

10
20
40
50
31

答案 1 :(得分:2)

你不能用concat来解决这个问题,因为它会破坏后期绑定。它在调用时会立即请求两个流的大小,因此您应该事先知道将保存多少元素以供日后使用。但是,由于后期绑定,可以使用flatMap执行此操作:

public static void main(String[] args) {
    Stream<Integer> origStream = Stream.of(10, 20, 30, 40, 50).filter(
            i -> saveThirtyForLater(i));
    Stream<Integer> savedForLaterStream = savedForLater.stream().map(
            i -> addOne(i));

    Stream.of(origStream, savedForLaterStream)
        .flatMap(Function.identity())
        .forEach(
        i -> System.out.println(i));
}

此代码可以很好地工作并打印10 / 20 / 40 / 50 / 31。如果你将它并行化,它会无法预测。

请注意,我的解决方案在很大程度上依赖于OpenJDK / OracleJDK中Stream API的当前实现。流API规范明确指出filter中使用的谓词必须是无状态且无干扰的。由于这里违反了这些属性,因此根据规范,结果是不可预测的。

答案 2 :(得分:0)

我感谢其他人的帮助,但确实希望发布我的最终解决方案。

我使用LinkedBlockingQueue和自定义Spliterator而不是ArrayList。调用Stream.concat会立即生成参数流的Spliterators(可以说是不必要的)。一旦按照其他人的指示生成列表,ArrayListSpliterator就不能容忍修改列表。

默认情况下,LinkedBlockingQueue有一个weakly consistent分裂器,可能返回在分割器初始化后添加到底层队列的项目。然而,在我的测试中,为了避免出现任何不同的生产行为,我提供了一个自定义分裂器,返回初始化后添加到底层队列的项目。 QSpliterator代码是从https://codereview.stackexchange.com/a/105308

复制的
package test;

import java.util.ArrayList;
import java.util.List;
import java.util.Spliterator;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.function.Consumer;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

public class TestStreams {
    private static LinkedBlockingQueue<Integer> savedForLater = new LinkedBlockingQueue<>();

    public static void main(String[] args) {
        Stream<Integer> origStream = Stream.of(10, 20, 30, 40, 50).filter(
                i -> saveThirtyForLater(i));
        Spliterator<Integer> qSpliterator = new QSpliterator<>(savedForLater);
        Stream<Integer> savedForLaterStream = StreamSupport.stream(
                qSpliterator, false).map(i -> addOne(i));

        Stream.concat(origStream, savedForLaterStream).forEach(
                i -> System.out.println(i));
    }

    private static Integer addOne(Integer i) {
        return new Integer(i + 1);
    }

    private static boolean saveThirtyForLater(Integer i) {
        if (i == 30) {
            savedForLater.add(i);
            return false;
        }
        return true;
    }

    private static final class QSpliterator<T> implements Spliterator<T> {

        private final BlockingQueue<T> queue;

        public QSpliterator(BlockingQueue<T> queue) {
            this.queue = queue;
        }

        @Override
        public boolean tryAdvance(Consumer<? super T> action) {
            try {
                action.accept(queue.take());
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new IllegalStateException("Take interrupted.", e);
            }
            return true;
        }

        @Override
        public Spliterator<T> trySplit() {
            try {
                final int size = queue.size();
                List<T> vals = new ArrayList<>(size + 1);
                vals.add(queue.take());
                queue.drainTo(vals);
                return vals.spliterator();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new IllegalStateException(
                        "Thread interrupted during trySplit.", e);
            }
        }

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

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

    }
}