![](/img/trans.png)
[英]Java 8 stream, how to “break” in reduce or in collect without throwing runtime exception?
[英]How collect / reduce java 8 stream into pojo?
看看代碼:
Collection<MyDto> col = ...
MyBuilder builder = new MyBuilder();
for (MyDto dto: col) {
switch (dto.getType()) {
case FIELD1:
builder.field1(dto.getValue());
break:
case FIELD2:
builder.field2(dto.getValue());
break:
}
}
Some result = builder.build();
有沒有辦法用流來做到這一點,比如:
Some result = col.stream().collect(...)
請注意,所有流值都收集到sigle pojo中,而不是集合,流或映射。
底線是,某處,不知何故,你需要的可能的返回值映射MyDto.getType()
來的屬性設置方法MyBuilder
。 您的代碼通過switch
語句執行此操作, 這很好 。 您可以將縮減編寫為基於流的管道,但您仍需要以某種方式合並映射。
一個非常直接的方法是構造一個文字Map
,它可以是靜態的,最終的和不可修改的。 例如,如果您從類似結構的類開始...
class Some {
}
class MyBuilder {
void field1(String s) { }
void field2(String s) { }
void field3(String s) { }
Some build() {
return null;
}
}
class ValueType {}
class MyDto {
int type;
ValueType value;
int getType() {
return type;
}
ValueType getValue() {
return value;
}
}
...那么你可以設置你所描述的減少:
public class Reduction {
// Map from DTO types to builder methods
private final static Map<Integer, BiConsumer<MyBuilder, ValueType>> builderMethods;
static {
// one-time map initialization
Map<Integer, BiConsumer<MyBuilder, ValueType>> temp = new HashMap<>();
temp.put(FIELD1, MyBuilder::field1);
temp.put(FIELD2, MyBuilder::field2);
temp.put(FIELD3, MyBuilder::field3);
builderMethods = Collections.unmodifiableMap(temp);
}
public Some reduce(Collection<MyDto> col) {
return col.stream()
// this reduction produces the populated builder
.reduce(new MyBuilder(),
(b, d) -> { builderMethods.get(d.getType()).accept(b, d); return b; })
// obtain the built object
.build();
}
}
該特定實現每次都使用一個新的構建器,但是可以修改為使用通過參數傳遞給Reduction.reduce()
的構建器,以防您想要預先填充一些屬性,和/或保留記錄用於構建返回對象的屬性。
最后,請注意盡管您可以在一個或另一個地方隱藏詳細信息,但我沒有看到任何范圍使整個過程比您開始使用的基於switch
的代碼更簡單。
我沒有編譯這個,但只是為了給你一個想法:
Map<Boolean, List<MyDto>> map = col.stream().collect(Collectors.partitioningBy(t -> t.getType() == FIELD2));
map.get(false).forEach(x -> builder.field1(x.getValue()))
map.get(true).forEach(x -> builder.field2(x.getValue()))
假設兩個MyBuilder
實例能夠組合/合並,那么您可以使用Collector
執行此操作。
public class MyCollector implements Collector<MyDto, MyBuilder, Result> {
@Override
public Supplier<MyBuilder> supplier() {
return MyBuilder::new;
}
@Override
public BiConsumer<MyBuilder, MyDto> accumulator() {
return (builder, dto) -> {
// Add "dto" to "builder" based on type
};
}
@Override
public BinaryOperator<MyBuilder> combiner() {
return (left, right) -> left.merge(right);
}
@Override
public Function<MyBuilder, Result> finisher() {
return MyBuilder::build;
}
@Override
public Set<Characteristics> characteristics() {
return Set.of();
}
}
然后你可以這樣做:
Collection<MyDto> col = ...;
Result r = col.stream().collect(new MyCollector());
如果您不想自定義Collector
實現,可以使用Collector.of(...)
。
一種不同的,可能更易於維護的方法是讓構建器完成所有工作。 這樣,所有映射邏輯都在一個地方。
public class ResultBuilder {
public static Collector<MyDto, ?, Result> resultCollector() {
return Collector.of(ResultBuilder::new, ResultBuilder::add,
ResultBuilder::merge, ResultBuilder::build);
}
public ResultBuilder add(MyDto dto) {
// Do what is needed based on the type of "dto"
return this;
}
public ResultBuilder merge(ResultBuilder other) {
// Merge "other" into "this"
return this;
}
public Result build() {
// Build result and return it
}
}
然后你可以使用帶或不帶流的構建器。 流與以前非常相似:
Collection<MyDto> col = ...;
Result r = col.stream().collect(ResultBuilder.resultCollector());
現在,對於一個令人沮喪的無聊答案:
使用流來有效地映射這樣會使您的代碼在將來更易於閱讀和維護。 為此目的使用此Java 8功能是不明智的。
絕對可以做到,正如一些回答者開創的那樣,但這並不一定意味着應該這樣做。
更簡潔的是,您的初始前提是您可以捕獲您可以switch
某種枚舉或結構中的所有字段,每次引入或刪除字段時都會中斷,這可能很耗時。 通過反射獲得場地的巧妙方法可能會稍微靈活一點,但是你的反射設置比你可能意識到的更嚴格。 如果你想將1映射到1,這可以正常工作,但是如果你想進行一些數據轉換,你必須非常小心你如何調整你的映射器。
所有這些都說......
您的主要問題是每個MyBuilder
方法到每個MyDto
類型的映射是任意的,即Java無法自動知道為每種類型調用哪個方法:您必須告訴Java哪個是哪個。
因此,如果構建器的每個方法都映射到不同的dto.getType()
值,那么告訴Java的最簡單方法是將該switch
移動到MyBuilder
中的通用方法,該方法允許您通知相應的字段,如下所示:
public MyBuilder fieldFromDto(MyDto dto) {
switch (dto.getType()) {
case FIELD1: return field1(dto.getValue);
case FIELD2: return field2(dto.getValue);
//...
那么你可以做到這一點:
MyBuilder builder = new MyBuilder();
col.stream().forEach(builder::fieldFromDto);
Some result = builder.build();
另一種可能性是將該開關轉換為lambda映射( Type
和Value
是MyDto
字段的類型):
class MyBuilder {
public final Map<Type, Function<Value, MyBuilder>> mappings = new Map<>();
public MyBuilder() {
mappings.put(FIELD1, this::field1);
mappings.put(FIELD2, this::field2);
//...
}
然后在forEach
使用這些lambdas:
MyBuilder builder = new MyBuilder();
col.stream().forEach(dto -> builder.mappings.get(dto.getType()).apply(dto.getValue()));
Some result = builder.build();
除此之外,你可以像其他一些答案一樣使用反射,但是你需要確保FIELD1
, FIELD2
等是實際的MyBuilder
方法名稱,失去一些靈活性。
最后,我不建議做上述任何一項。 流很好,但有時它們沒有提供優於正常for
循環的任何優勢,並且可能使您的代碼更難以維護。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.