我有一个我必须实现的接口,需要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的流,直到它从中拉出一个对象,此时列表不会改变。
如果所有其他方法都失败了,我可以阅读该文件两次,但我真的想知道为什么这不起作用,如果有人有另外的想法来操纵流以避免第二次传递。
答案 0 :(得分:2)
Streams很懒。除非您使用forEach
或collect
等终端操作,否则不会执行中间操作(例如filter
或map
)。
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));
中,您在流forEach
和origStream
上使用终端操作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;
}
}
}