简体   繁体   English

如何收集/减少java 8流到pojo?

[英]How collect / reduce java 8 stream into pojo?

Look at the code: 看看代码:

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();

Is there a way to do this with streams, like: 有没有办法用流来做到这一点,比如:

Some result = col.stream().collect(...)

Note that all stream values are collected into sigle pojo, not collection, stream or map. 请注意,所有流值都收集到sigle pojo中,而不是集合,流或映射。

The bottom line is that somewhere, somehow, you need to map the possible return values of MyDto.getType() to property setting methods of MyBuilder . 底线是,某处,不知何故,你需要的可能的返回值映射MyDto.getType()来的属性设置方法MyBuilder Your code does that via a switch statement, and that's just fine . 您的代码通过switch语句执行此操作, 这很好 You can write the reduction as a stream-based pipeline instead, but you still need to incorporate the mapping somehow. 您可以将缩减编写为基于流的管道,但您仍需要以某种方式合并映射。

A pretty direct way of doing that would be to construct a literal Map , which could be made static, final, and unmodifiable. 一个非常直接的方法是构造一个文字Map ,它可以是静态的,最终的和不可修改的。 For instance, if you're starting with classes structured like so ... 例如,如果您从类似结构的类开始...

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;
    }
}

... then you might set up the reduction you describe like this: ...那么你可以设置你所描述的减少:

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();
    }
}

That particular implementation uses a new builder every time, but it could be modified to instead use a builder passed into Reduction.reduce() via a parameter, in case you want to start with some properties pre-populated, and / or retain a record of the properties with which the returned object was built. 该特定实现每次都使用一个新的构建器,但是可以修改为使用通过参数传递给Reduction.reduce()的构建器,以防您想要预先填充一些属性,和/或保留记录用于构建返回对象的属性。

Finally, note well that although you can hide the details in one place or another, I don't see any scope to make the overall process any simpler than the switch -based code you started with. 最后,请注意尽管您可以在一个或另一个地方隐藏详细信息,但我没有看到任何范围使整个过程比您开始使用的基于switch的代码更简单。

I did not compile this, but just to give you an idea: 我没有编译这个,但只是为了给你一个想法:

 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()))

Assuming two MyBuilder instances are capable of being combined/merged then you can do this with a Collector . 假设两个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();
    }

}

Then you could do: 然后你可以这样做:

Collection<MyDto> col = ...;
Result r = col.stream().collect(new MyCollector());

If you don't want to make a custom implementation of Collector you can use Collector.of(...) . 如果您不想自定义Collector实现,可以使用Collector.of(...)


A different, maybe more maintainable, way to do this is to have the builder do all the work. 一种不同的,可能更易于维护的方法是让构建器完成所有工作。 This way all the mapping logic is in one place. 这样,所有映射逻辑都在一个地方。

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
    }

}

Then you could use the builder with or without streams. 然后你可以使用带或不带流的构建器。 With streams is very similar to before: 流与以前非常相似:

Collection<MyDto> col = ...;
Result r = col.stream().collect(ResultBuilder.resultCollector());

And now, for a depressingly boring answer: 现在,对于一个令人沮丧的无聊答案:

Don't do this. 不要这样做。

Using streams to effectively map across like this makes your code less readable and maintainable in the future. 使用流来有效地映射这样会使您的代码在将来更易于阅读和维护。 It is ill-advised to use this Java 8 feature for this purpose. 为此目的使用此Java 8功能是不明智的。

It absolutely can be done, as some answerers have pioneered, but that doesn't necessarily mean it should be done. 绝对可以做到,正如一些回答者开创的那样,但这并不一定意味着应该这样做。

More concisely, your initial premise is that you can capture all of your fields in some kind of enum or structure you can switch on, which breaks every time you introduce or remove a field, which can be time consuming to track down. 更简洁的是,您的初始前提是您可以捕获您可以switch某种枚举或结构中的所有字段,每次引入或删除字段时都会中断,这可能很耗时。 Clever ways of getting the fields out with reflection may be slightly more flexible, but then you're in a more rigid setup with reflection than you may realize; 通过反射获得场地的巧妙方法可能会稍微灵活一点,但是你的反射设置比你可能意识到的更严格。 if you want to map 1 to 1 this works fine, but if you want to do some data transformation, you have to be very careful about how you tweak your mapper. 如果你想将1映射到1,这可以正常工作,但是如果你想进行一些数据转换,你必须非常小心你如何调整你的映射器。

All of that to say... 所有这些都说......

Use a mapping framework instead like MapStruct or Dozer . 使用映射框架,而不是像MapStructDozer

Your main problem is that the mapping of each MyBuilder method to each MyDto type is arbitrary, ie there's no way for Java to automatically know which method to call for each type: you have to tell Java which is which. 您的主要问题是每个MyBuilder方法到每个MyDto类型的映射是任意的,即Java无法自动知道为每种类型调用哪个方法:您必须告诉Java哪个是哪个。

So, if each method of the builder is mapped to a different dto.getType() value, the simplest way of telling Java, is moving that switch to a general method inside MyBuilder that lets you inform the appropriate field, like this: 因此,如果构建器的每个方法都映射到不同的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);
        //...

So then you could do just this: 那么你可以做到这一点:

MyBuilder builder = new MyBuilder();
col.stream().forEach(builder::fieldFromDto);
Some result = builder.build();

Another possibility would be to turn that switch into a lambda map ( Type and Value being the types of MyDto 's fields): 另一种可能性是将该开关转换为lambda映射( TypeValueMyDto字段的类型):

class MyBuilder {
    public final Map<Type, Function<Value, MyBuilder>> mappings = new Map<>();
    public MyBuilder() {
        mappings.put(FIELD1, this::field1);
        mappings.put(FIELD2, this::field2);
        //...
    }

And then use those lambdas in a forEach : 然后在forEach使用这些lambdas:

MyBuilder builder = new MyBuilder();
col.stream().forEach(dto -> builder.mappings.get(dto.getType()).apply(dto.getValue()));
Some result = builder.build();

Other than that, you could use reflection like some other answers proposed, but then you need to make sure that FIELD1 , FIELD2 etc. are actual MyBuilder method names, losing some flexibility. 除此之外,你可以像其他一些答案一样使用反射,但是你需要确保FIELD1FIELD2等是实际的MyBuilder方法名称,失去一些灵活性。

In the end I wouldn't recommend doing any of the above. 最后,我不建议做上述任何一项。 Streams are great, but sometimes they don't offer any advantage over a normal for loop and can make your code uglier and difficult to maintain. 流很好,但有时它们没有提供优于正常for循环的任何优势,并且可能使您的代码更难以维护。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM