[英]Java 8 streams conditional processing
我有興趣將一個流分成兩個或多個子流,並以不同的方式處理元素。 例如,一個(大)文本文件可能包含 A 類型的行和 B 類型的行,在這種情況下,我想做類似的事情:
File.lines(path)
.filter(line -> isTypeA(line))
.forEachTrue(line -> processTypeA(line))
.forEachFalse(line -> processTypeB(line))
上一個是我嘗試抽象的情況。 實際上,我有一個非常大的文本文件,其中每一行都在針對正則表達式進行測試; 如果該行通過,則對其進行處理,而如果該行被拒絕,則我想更新一個計數器。 對拒絕字符串的進一步處理是我不簡單使用filter
的原因。
有什么合理的方法可以用流來做到這一點,還是我必須回退到循環? (我也希望它可以並行運行,所以流是我的首選)。
Java 8流不是為支持這種操作而設計的。 來自jdk :
應該僅對一個流進行操作(調用中間或終端流操作)。 例如,這排除了“分叉”流,其中相同的源提供兩個或更多個管道,或者同一個流的多個遍歷。
如果你可以將它存儲在內存中,你可以使用Collectors.partitioningBy
如果你只有兩種類型並使用Map<Boolean, List>
。 否則使用Collectors.groupingBy
。
只需測試每個元素,並采取相應的行動。
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(...);
}
雖然不鼓勵使用行為參數中的副作用,但只要不存在干擾,它們就不會被禁止,所以最簡單但不是最干凈的解決方案是在過濾器中計算:
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將添加當前缺少的filtering
和flatMapping
。
你需要一個對類型來保存兩個結果,所以假設一個草圖
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
返回的流可能在並行處理方面表現不佳,與“Reader#lines()相比, 由於不可配置的批處理而並行化很差分裂者中的規模政策“ 。 Java 9發行版計划進行了改進。
我處理這個問題的方法不是將它分開,而是寫下來
Files.lines(path)
.map(line -> {
if (condition(line)) {
return doThingA(line);
} else {
return doThingB(line);
}
})...
細節取決於您想要做什么以及您打算如何做。
好吧,你可以干脆做
Counter counter = new Counter();
File.lines(path)
.forEach(line -> {
if (isTypeA(line)) {
processTypeA(line);
}
else {
counter.increment();
}
});
不是很實用的風格,但它以與你的例子類似的方式實現。 當然,如果是並行的, Counter.increment()
和processTypeA()
都必須是線程安全的。
這是一種方法(忽略了強制將條件處理轉換為流的注意事項),它將謂詞和使用者包裝成單個謂詞副作用:
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 );
思考?
看來實際上你確實希望處理每一行,但是根據某些條件(類型)對它進行不同的處理。
我認為這或多或少是實現它的功能方式:
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;
}
@湯姆
那這個呢:
Arrays.stream( dir.listFiles() )
.peek( f -> { if(f.isDirectory()) { processDir(f); }} )
.peek( f -> { if(f.isFile()) { processFile(f);}}) )
.forEach( f -> this::processErr );
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.