分组和减少对象列表

ryb*_*ber 18 java java-8

我有一个对象列表,其中有许多重复,有些字段需要合并.我想将它简化为仅使用Java 8 Streams的唯一对象列表(我知道如何通过old-skool方法执行此操作,但这是一个实验.)

这就是我现在所拥有的.我真的不喜欢这个,因为地图构建看起来无关紧要,而且values()集合是支持地图的视图,你需要将它包装成一个新的ArrayList<>(...)以获得更具体的集合.有没有更好的方法,也许使用更一般的减少操作?

    @Test
public void reduce() {
    Collection<Foo> foos = Stream.of("foo", "bar", "baz")
                     .flatMap(this::getfoos)
                     .collect(Collectors.toMap(f -> f.name, f -> f, (l, r) -> {
                         l.ids.addAll(r.ids);
                         return l;
                     })).values();

    assertEquals(3, foos.size());
    foos.forEach(f -> assertEquals(10, f.ids.size()));
}

private Stream<Foo> getfoos(String n) {
    return IntStream.range(0,10).mapToObj(i -> new Foo(n, i));
}

public static class Foo {
    private String name;
    private List<Integer> ids = new ArrayList<>();

    public Foo(String n, int i) {
        name = n;
        ids.add(i);
    }
}
Run Code Online (Sandbox Code Playgroud)

Bri*_*ent 10

如果您打破分组并减少步骤,您可以获得更清洁的东西:

Stream<Foo> input = Stream.of("foo", "bar", "baz").flatMap(this::getfoos);

Map<String, Optional<Foo>> collect = input.collect(Collectors.groupingBy(f -> f.name, Collectors.reducing(Foo::merge)));

Collection<Optional<Foo>> collected = collect.values();
Run Code Online (Sandbox Code Playgroud)

这假设您的Foo班级有一些便利方法:

public Foo(String n, List<Integer> ids) {
    this.name = n;
    this.ids.addAll(ids);
}

public static Foo merge(Foo src, Foo dest) {
    List<Integer> merged = new ArrayList<>();
    merged.addAll(src.ids);
    merged.addAll(dest.ids);
    return new Foo(src.name, merged);
}
Run Code Online (Sandbox Code Playgroud)

  • @ryber肯定,但在现实世界的场景中,很容易产生意想不到的问题,特别是如果你的减少并行运行.我建议减少流媒体操作中的可变性.请参阅:https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html#Reduction. (3认同)
  • 如果输入为空,则在没有标识值的情况下减少将返回空的"可选".但是在"groupingBy"的上下文中减少永远不会导致空的"可选",因为只有存在值时才会创建映射条目.所以这只是API中的噪音.对于这个例子,OP的三参数`toMap()`调用可能比groupingBy/reduction更可取. (2认同)

小智 5

As already pointed out in the comments, a map is a very natural thing to use when you want to identify unique objects. If all you needed to do was find the unique objects, you could use the Stream::distinct method. This method hides the fact that there is a map involved, but apparently it does use a map internally, as hinted by this question that shows you should implement a hashCode method or distinct may not behave correctly.

In the case of the distinct method, where no merging is necessary, it is possible to return some of the results before all of the input has been processed. In your case, unless you can make additional assumptions about the input that haven't been mentioned in the question, you do need to finish processing all of the input before you return any results. Thus this answer does use a map.

It is easy enough to use streams to process the values of the map and turn it back into an ArrayList, though. I show that in this answer, as well as providing a way to avoid the appearance of an Optional<Foo>, which shows up in one of the other answers.

public void reduce() {
    ArrayList<Foo> foos = Stream.of("foo", "bar", "baz").flatMap(this::getfoos)
            .collect(Collectors.collectingAndThen(Collectors.groupingBy(f -> f.name,
            Collectors.reducing(Foo.identity(), Foo::merge)),
            map -> map.values().stream().
                collect(Collectors.toCollection(ArrayList::new))));

    assertEquals(3, foos.size());
    foos.forEach(f -> assertEquals(10, f.ids.size()));
}

private Stream<Foo> getfoos(String n) {
    return IntStream.range(0, 10).mapToObj(i -> new Foo(n, i));
}

public static class Foo {
    private String name;
    private List<Integer> ids = new ArrayList<>();

    private static final Foo BASE_FOO = new Foo("", 0);

    public static Foo identity() {
        return BASE_FOO;
    }

    // use only if side effects to the argument objects are okay
    public static Foo merge(Foo fooOne, Foo fooTwo) {
        if (fooOne == BASE_FOO) {
            return fooTwo;
        } else if (fooTwo == BASE_FOO) {
            return fooOne;
        }
        fooOne.ids.addAll(fooTwo.ids);
        return fooOne;
    }

    public Foo(String n, int i) {
        name = n;
        ids.add(i);
    }
}
Run Code Online (Sandbox Code Playgroud)