我有兴趣将流分成两个或更多个子流,并以不同的方式处理这些元素。例如,(大)文本文件可能包含类型A的行和类型B的行,在这种情况下,我喜欢执行以下操作:
File.lines(path)
.filter(line -> isTypeA(line))
.forEachTrue(line -> processTypeA(line))
.forEachFalse(line -> processTypeB(line))
之前是我试图抽象情况。实际上我有一个非常大的文本文件,其中每一行都在测试正则表达式;如果该行通过,则处理它,而如果它被拒绝,那么我想更新一个计数器。对拒绝字符串的进一步处理是我不能简单地使用filter
。
有什么合理的方法可以使用流来执行此操作,还是必须回退到循环? (我希望这也是并行运行,所以溪流是我的第一选择。)
答案 0 :(得分:17)
Java 8流不是为支持这种操作而设计的。来自jdk:
应该只对一个流进行操作(调用中间或终端流操作)。例如,这排除了"分叉"流,其中相同的源提供两个或多个管道,或多个遍历同一个流。
如果您可以将其存储在内存中,则可以使用Collectors.partitioningBy
,如果您只有两种类型,请使用Map<Boolean, List>
。否则使用Collectors.groupingBy
。
答案 1 :(得分:11)
只需测试每个元素,并采取相应的行动。
lines.forEach(line -> {
if (isTypeA(line)) processTypeA(line);
else processTypeB(line);
});
此行为可能隐藏在辅助方法中:
public static <T> Consumer<T> branch(Predicate<? super T> test,
Consumer<? super T> t,
Consumer<? super T> f) {
return o -> {
if (test.test(o)) t.accept(o);
else f.accept(o);
};
}
然后用法如下:
lines.forEach(branch(this::isTypeA, this::processTypeA, this::processTypeB));
Files.lines()
方法不会关闭基础文件,因此您必须像这样使用它:
try (Stream<String> lines = Files.lines(path, encoding)) {
lines.forEach(...);
}
Stream
类型的变量对我来说有点红旗,所以我更喜欢直接管理BufferedReader
:
try (BufferedReader lines = Files.newBufferedReader(path, encoding)) {
lines.lines().forEach(...);
}
答案 2 :(得分:5)
虽然不鼓励对行为参数产生副作用,但只要不存在干扰,就不会禁止它们,所以最简单但不是最干净的解决方案是在过滤器中计算:
AtomicInteger rejected=new AtomicInteger();
Files.lines(path)
.filter(line -> {
boolean accepted=isTypeA(line);
if(!accepted) rejected.incrementAndGet();
return accepted;
})
// chain processing of matched lines
只要您处理所有项目,结果就会一致。只有在使用短路终端操作(并行流)时,结果才会变得不可预测。
更新原子变量可能不是最有效的解决方案,但在处理来自文件的行的上下文中,开销可能可以忽略不计。
如果您想要一个干净,并行友好的解决方案,一种通用方法是实现Collector
,它可以根据条件组合两个收集操作的处理。这要求您能够将下游操作表示为收集器,但大多数流操作可以表示为收集器(并且趋势可能以这种方式表达所有操作,即Java 9将添加当前缺少的{{ 3}}和filtering
。
你需要一个对类型来保存两个结果,所以假设像
这样的草图class Pair<A,B> {
final A a;
final B b;
Pair(A a, B b) {
this.a=a;
this.b=b;
}
}
组合收集器实现看起来像
public static <T, A1, A2, R1, R2> Collector<T, ?, Pair<R1,R2>> conditional(
Predicate<? super T> predicate,
Collector<T, A1, R1> whenTrue, Collector<T, A2, R2> whenFalse) {
Supplier<A1> s1=whenTrue.supplier();
Supplier<A2> s2=whenFalse.supplier();
BiConsumer<A1, T> a1=whenTrue.accumulator();
BiConsumer<A2, T> a2=whenFalse.accumulator();
BinaryOperator<A1> c1=whenTrue.combiner();
BinaryOperator<A2> c2=whenFalse.combiner();
Function<A1,R1> f1=whenTrue.finisher();
Function<A2,R2> f2=whenFalse.finisher();
return Collector.of(
()->new Pair<>(s1.get(), s2.get()),
(p,t)->{
if(predicate.test(t)) a1.accept(p.a, t); else a2.accept(p.b, t);
},
(p1,p2)->new Pair<>(c1.apply(p1.a, p2.a), c2.apply(p1.b, p2.b)),
p -> new Pair<>(f1.apply(p.a), f2.apply(p.b)));
}
并且可以用于例如将匹配项目收集到列表中并计算不匹配项,如下所示:
Pair<List<String>, Long> p = Files.lines(path)
.collect(conditional(line -> isTypeA(line), Collectors.toList(), Collectors.counting()));
List<String> matching=p.a;
long nonMatching=p.b;
收集器是并行友好的,允许任意复杂的委托收集器,但请注意,对于当前实现,Files.lines
返回的流可能在并行处理方面表现不佳,与flatMapping
相比。 Java 9发布计划进行了改进。
答案 3 :(得分:2)
我处理这个问题的方法不是将其分开,而是写下来
Files.lines(path)
.map(line -> {
if (condition(line)) {
return doThingA(line);
} else {
return doThingB(line);
}
})...
详细信息取决于您想要做什么以及您打算如何做。
答案 4 :(得分:1)
好吧,你可以简单地做
echo
不是非常功能风格,但它以与您的示例类似的方式执行。当然,如果是并行的,<xsd:complexType name="Sensor_Info">
<xsd:sequence>
<xsd:element name="Sensor" minOccurs="1" maxOccurs="unbounded">
<xsd:complexType>
<xsd:attribute name="id" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="location_id" type="xsd:string"/>
<xsd:attribute name="unit" type="xsd:string"/>
<xsd:attribute name="min_value" type="xsd:int"/>
<xsd:attribute name="max_value" type="xsd:int"/>
</xsd:complexType>
</xsd:element>
</xsd:sequence>
<xsd:attribute name="count" type="xsd:int"/>
和Counter counter = new Counter();
File.lines(path)
.forEach(line -> {
if (isTypeA(line)) {
processTypeA(line);
}
else {
counter.increment();
}
});
都必须是线程安全的。
答案 5 :(得分:1)
这是一种将谓词和使用者包装到具有副作用的谓词中的方法(它忽略了将条件处理强加到流中的警告):
public static class StreamProc {
public static <T> Predicate<T> process( Predicate<T> condition, Consumer<T> operation ) {
Predicate<T> p = t -> { operation.accept(t); return false; };
return (t) -> condition.test(t) ? p.test(t) : true;
}
}
然后过滤流:
someStream
.filter( StreamProc.process( cond1, op1 ) )
.filter( StreamProc.process( cond2, op2 ) )
...
.collect( ... )
流中剩余的元素尚未处理。
例如,使用外部迭代进行的典型文件系统遍历看起来像
File[] files = dir.listFiles();
for ( File f : files ) {
if ( f.isDirectory() ) {
this.processDir( f );
} else if ( f.isFile() ) {
this.processFile( f );
} else {
this.processErr( f );
}
}
有了流和内部迭代,这变成了
Arrays.stream( dir.listFiles() )
.filter( StreamProc.process( f -> f.isDirectory(), this::processDir ) )
.filter( StreamProc.process( f -> f.isFile(), this::processFile ) )
.forEach( f -> this::processErr );
我希望Stream直接实现处理方法。那我们可以有
Arrays.stream( dir.listFiles() )
.process( f -> f.isDirectory(), this::processDir ) )
.process( f -> f.isFile(), this::processFile ) )
.forEach( f -> this::processErr );
有想法吗?
答案 6 :(得分:0)
实际上,您确实希望处理每一行,但根据某些条件(类型)对其进行不同的处理。
我认为这或多或少是实现它的功能方式:
public static void main(String[] args) {
Arrays.stream(new int[] {1,2,3,4}).map(i -> processor(i).get()).forEach(System.out::println);
}
static Supplier<Integer> processor(int i) {
return tellType(i) ? () -> processTypeA(i) : () -> processTypeB(i);
}
static boolean tellType(int i) {
return i % 2 == 0;
}
static int processTypeA(int i) {
return i * 100;
}
static int processTypeB(int i) {
return i * 10;
}