![](/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.