如何在Java 8中使用concat获取对象的不同列表

7

我有两个Java类。

class A {
 String name;
 List<B> numbers;
}

class B {
 Integer number;
}

我希望获取 A 类的不同项,并将其中的 B 类列表连接起来。
例如,假设我有一个包含以下对象的列表。
List<A>{
 name = "abc"
 List<B>{1,2}

 name= "xyz"
 List<B>{3,4}

 name = "abc"
 List<B>{3,5}
}

结果应该是:
List<A>{
 name = "abc"
 List<B>{1,2,3,5}

 name="xyz"
 List<B>{3,4}
}

希望能得到任何帮助。

注意:我希望使用Java 8流实现此功能。

谢谢


@PaulBoddington 是的,我正在使用数据库中的值创建新的A类实例,并将其添加到列表中。一旦列表被填充,我需要获取具有连接的B类列表的A类不同对象。 - Umair Ansari
1
谢谢。我删除了评论,因为一开始看起来 AB 是伪代码,但后来意识到它们是完整的可以编译的类。 - Paul Boddington
3个回答

3
您可以使用toMap收集器:
Collection<A> result = list.stream()
         .collect(Collectors.toMap(a -> a.name, a -> a, 
                      (a, b) -> {a.numbers.addAll(b.numbers); return a;}))
         .values();

你可以将结果复制到 List (如 new ArrayList<>(result)),但是由于我们不保留任何特定的顺序,所以拥有 List 不是非常有用。在大多数情况下,将 Collection 作为结果是可以的。


3
您可以通过使用toMap()的四个参数重载,并将LinkedHashMap :: new作为Supplier来保留顺序。 - jaco0646
对我来说,在第39行会出现java.lang.UnsupportedOperationException:http://pastebin.com/fAvd6jMi - ctomek
@ctomek,你的列表是使用Arrays.asList创建的,它不支持添加新元素。这可以很容易地通过改用ArrayList来解决。 - Tagir Valeev

2

我认为没有绕过地图的方法。但是,你可以使用 groupingBy 来隐藏它,但这与 Paul Boddington 建议的代码和性能实际上是相同的。

List<A> merge(List<A> input) {
    return input.stream()
            .collect(groupingBy(a -> a.name)) // map created here
            .entrySet()
            .stream()
            .map(entry -> new A(
                    entry.getKey(),
                    entry.getValue().stream()
                            .flatMap(list -> list.numbers.stream())
                            .collect(toList()) // merging behaviour
            )).collect(toList());
}

原始列表没有变异,您可以轻松更改合并列表的行为 - 例如,如果要摆脱重复项,请在flatMap(list -> list.numbers.stream())之后添加.distinct()(记住将equals添加到B),或者以类似的方式通过添加.sorted()对它们进行排序(您必须使B实现Comparable接口或只使用.sorted(Comparator<B>))。
这是完整的代码,包括测试和导入:
import org.junit.Test;

import java.util.List;

import static com.shazam.shazamcrest.MatcherAssert.assertThat;
import static com.shazam.shazamcrest.matcher.Matchers.sameBeanAs;
import static java.util.Arrays.asList;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toList;

public class Main {

    class A {
        final String name;
        final List<B> numbers;
        A(String name, List<B> numbers) {
            this.name = name;
            this.numbers = numbers;
        }
    }

    class B {
        final Integer number;
        B(Integer number) {
            this.number = number;
        }
    }

    List<A> merge(List<A> input) {
        return input.stream()
                .collect(groupingBy(a -> a.name))
                .entrySet()
                .stream()
                .map(entry -> new A(
                        entry.getKey(),
                        entry.getValue().stream()
                                .flatMap(list -> list.numbers.stream())
                                .collect(toList())
                )).collect(toList());
    }

    @Test
    public void test() {
        List<A> input = asList(
                new A("abc", asList(new B(1), new B(2))),
                new A("xyz", asList(new B(3), new B(4))),
                new A("abc", asList(new B(3), new B(5)))
        );

        List<A> list = merge(input);

        assertThat(list, sameBeanAs(asList(
                new A("abc", asList(new B(1), new B(2), new B(3), new B(5))),
                new A("xyz", asList(new B(3), new B(4)))
        )));
    }

}

编辑:

根据您在评论中的问题,如果您想将多个字段添加到groupingBy子句中,则需要创建一个表示映射键的类。如果您有不想包含在键中的字段,则必须定义如何合并两个不同的值 - 类似于您在数字上所做的操作。根据字段是什么,合并行为可以只选择第一个值并丢弃其他值(就像我在下面的代码中对numbers所做的那样)。

class A {
    final String name;
    final String type;
    final List<B> numbers;
    A(String name, String type, List<B> numbers) {
        this.name = name;
        this.type = type;
        this.numbers = numbers;
    }
}

class B {
    final Integer number;
    B(Integer number) {
        this.number = number;
    }
}

class Group {
    final String name;
    final String type;
    Group(String name, String type) {
        this.name = name;
        this.type = type;
    }
    // this needs to have equals and hashCode methods as we use it as a key in a map
}

List<A> merge(List<A> input) {
    return input.stream()
            .collect(groupingBy(a -> new Group(a.name, a.type)))
            .entrySet()
            .stream()
            .map(entry -> new A(
                    entry.getKey().name, // entry.getKey() is now Group, not String
                    entry.getKey().type,
                    entry.getValue().get(0).numbers // no merging, just take first
            )).collect(toList());
}

嗨,感谢您的回答。我可以在groupingBy子句中添加多个字段吗?如果可以,那么语法应该是什么? - Umair Ansari
如果A类有更多的字段,但不应该添加到分组语句中,您的解决方案是否可行? - Umair Ansari

1

这是我的答案。我已经添加了一个A的构造函数,使它更容易些。

public class Main {

    static class A {
        String name;
        List<B> numbers;

        A(String name, List<B> numbers) {
            this.name = name;
            this.numbers = new ArrayList<>(numbers);
        }
    }

    static class B {
        Integer number;
    }

    static List<A> merge(List<A> list) {
        Map<String, List<B>> map = new LinkedHashMap<>();
        for (A a : list)
            map.computeIfAbsent(a.name, k -> new ArrayList<>()).addAll(a.numbers);
        return map.entrySet()
                  .stream()
                  .map(e -> new A(e.getKey(), e.getValue()))
                  .collect(Collectors.toList());
    }
}

几乎可以肯定有一种简单的方法可以用 list.stream()... 开头替换 merge 方法的前三行,使整个方法成为一行代码。但我无法想出来。也许其他人可以编辑此答案以显示如何实现?

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接