【问题标题】:Create a nested parent child list in Java 8在 Java 8 中创建嵌套的父子列表
【发布时间】:2018-07-14 04:17:35
【问题描述】:

我是 Java 8 的新手,需要解决以下问题。

我有两个类如下:

class Person {
    String name;
    int age;
    List<Address> address;
}

class Address {
    String street;
    String city;
    String country;
}

现在我有一个来自数据库的列表,如下所示:

List<Person> findPerson;

adam
26
<123, xyz, yyy>

adam
26
<456, rrr, kkk>

bill
31
<666, uuu, hhh>

现在我需要将相同的人对象与不同的地址对象合并为一个,如下所示?

List<Person> findPerson;

adam
26
<123, xyz, 456>
<456, rrr, 123>

bill
31
<666, uuu, 999>

如何在 Java 8 流中做到这一点?

【问题讨论】:

  • 您使用的是什么数据库?一些数据库有足够的数组支持,您可以直接从查询中返回序列类型。另外,这两张表的主键和外键是什么?

标签: java list java-8 iteration java-stream


【解决方案1】:

我建议你在 Person 类中实现 equalshashcode

例子:

@Override
public boolean equals(Object o) {
       if (this == o) return true;
       if (o == null || getClass() != o.getClass()) return false;

       Person person = (Person) o;

       if (age != person.age) return false;
       return name != null ? name.equals(person.name) : person.name == null;
}

@Override
public int hashCode() {
       int result = name != null ? name.hashCode() : 0;
       result = 31 * result + age;
       return result;
}

然后您可以将Map&lt;Person, List&lt;Address&gt;&gt; 作为结果集的接收者类型。

Map<Person, List<Address>> resultSet = findPerson.stream()
    .collect(
        Collectors.groupingBy(
            Function.identity(),
            Collectors.flatMapping(
                p -> p.getAddress().stream(), 
                Collectors.toList()
            )
        )
    )
;

这个解决方案使用了 flatMapping 收集器,它只在 Java-9 中可用。

如果您仍在使用 Java-8,那么您可以这样做:

Map<Person, List<Address>> resultSet = findPerson.stream()
    .collect(
        Collectors.groupingBy(
            Function.identity(),
            Collectors.collectingAndThen(
                Collectors.mapping(
                    Person::getAddress, 
                    Collectors.toList()
                ),
                lists -> lists.stream()
                    .flatMap(List::stream)
                    .collect( Collectors.toList() )
            )
        )
    )   
;

note - 目前,我假设两个或更多人被认为是平等的基于 nameage,但是,如果它仅基于 name 或仅基于 @ 987654332@ 那么你需要稍微调整一下equals / hashcode 方法。

【讨论】:

    【解决方案2】:

    假设Person 始终如一地实现hashCodeequals,您可以收集到Map

    Map<Person, Person> map = findPerson.stream()
        .collect(Collectors.toMap(
            p -> p,
            p -> p,
            (oldPerson, newPerson) -> { 
                oldPerson.getAddress().addAll(newPerson.getAddress()); 
                return oldPerson; 
            }));
    

    这使用Collectors.toMap,它通过合并一个新人与一个已经在地图中的老人,合并意味着添加地址。

    现在,在地图的值中,你有合并的人:

    Collection<Person> result = map.values();
    

    如果需要返回列表:

    List<Person> result = new ArrayList<>(map.values());
    

    编辑:

    这假定每个人的List&lt;Address&gt;可变的(因此addAll 有效)。情况并非总是如此,尤其是如果您不想破坏封装,则不应让您的Person 对象的地址被外部修改。

    在这种情况下,您可以在Person 中提供一个方法,负责在不破坏封装的情况下合并地址:

    public Person merge(Person another) {
        this.address.addAll(another.address);
        return this;
    }
    

    或者如果地址列表是不可变的:

    public Person merge(Person another) {
        List<Address> merged = new ArrayList<>(this.address);
        merged.addAll(another.address);
        this.address = merged;
        return this;
    }
    

    现在收集器中的代码可以重构为:

    Map<Person, Person> map = findPerson.stream()
        .collect(Collectors.toMap(
            p -> p, 
            p -> p,
            Person::merge));
    

    【讨论】:

    • 嗨,有没有办法在不覆盖 equals 和 hashcode 方法的情况下做到这一点?
    • @user3378550 是的,把第一个p -&gt; p改成p -&gt; Arrays.asList(p.getName(), p.getAge()),把map的类型改成Map&lt;List&lt;Object&gt;, Person&gt;
    【解决方案3】:

    另一种方法,不需要覆盖 equalshashCode

    List<Person> merged = findPerson.stream()
        .collect( 
            Collectors.collectingAndThen(
                Collectors.toMap( 
                    (p) -> new AbstractMap.SimpleEntry<>( p.getName(), p.getAge() ), 
                    Function.identity(), 
                    (left, right) -> { 
                        left.getAddress().addAll( right.getAddress() ); 
                        return left; 
                    }
                ),        
                ps -> new ArrayList<>( ps.values() ) 
            ) 
        )
    ;
    

    【讨论】:

      【解决方案4】:

      此答案与最近删除的答案非常相似:

      findPerson = findPerson.stream()
                             .collect(Collectors.groupingBy(Person::getName,
                                      Collectors.reducing((p1, p2) -> {
                                          p1.getAddress().addAll(p2.address);
                                          return p1;
                             }))).values().stream()
                                 .filter(Optional::isPresent)
                                 .map(Optional::get)
                                 .collect(Collectors.toList());
      

      在 Java 9 中,底部的两行可以替换为对 Optional::stream 的一次调用。

      使用你的例子,结果如下:

      [Person[bill, 31, [Address[666, uuu, hhh]]], Person[adam, 26, [Address[123, xyz, yyy], Address[456, rrr, kkk]]]]
      

      注意:记得为Person 适当地覆盖equalshashCode

      【讨论】:

      • 嗨,有没有办法在不覆盖 equals 和 hashcode 方法的情况下实现这一点?
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多