【问题标题】:Java 8 Stream Mapping Grouping operationJava 8 流映射分组操作
【发布时间】:2017-10-19 02:38:55
【问题描述】:

我有以下两个课程:

Person:

public class Person {

    private final Long id;
    private final String address;
    private final String phone;

    public Person(Long id, String address, String phone) {
        this.id = id;
        this.address = address;
        this.phone = phone;
    }

    public Long getId() {
        return id;
    }

    public String getAddress() {
        return address;
    }

    public String getPhone() {
        return phone;
    }

    @Override
    public String toString() {
        return "Person [id=" + id + ", address=" + address + ", phone=" + phone + "]";
    }
}

CollectivePerson:

import java.util.HashSet;
import java.util.Set;

public class CollectivePerson {

    private final Long id;
    private final Set<String> addresses;
    private final Set<String> phones;

    public CollectivePerson(Long id) {
        this.id = id;
        this.addresses = new HashSet<>();
        this.phones = new HashSet<>();
    }

    public Long getId() {
        return id;
    }

    public Set<String> getAddresses() {
        return addresses;
    }

    public Set<String> getPhones() {
        return phones;
    }

    @Override
    public String toString() {
        return "CollectivePerson [id=" + id + ", addresses=" + addresses + ", phones=" + phones + "]";
    }
}

我想进行流式操作,以便:

  • Person 映射到 CollectivePerson
  • Personaddressphone 分别合并到CollectivePerson 中的addressesphones,对于所有具有相同idPersons

我为此编写了以下代码:

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

public class Main {

    public static void main(String[] args) {
        Person person1 = new Person(1L, "Address 1", "Phone 1");
        Person person2 = new Person(2L, "Address 2", "Phone 2");
        Person person3 = new Person(3L, "Address 3", "Phone 3");
        Person person11 = new Person(1L, "Address 4", "Phone 4");
        Person person21 = new Person(2L, "Address 5", "Phone 5");
        Person person22 = new Person(2L, "Address 6", "Phone 6");

        List<Person> persons = new ArrayList<>();
        persons.add(person1);
        persons.add(person11);
        persons.add(person2);
        persons.add(person21);
        persons.add(person22);
        persons.add(person3);

        Map<Long, CollectivePerson> map = new HashMap<>();
        List<CollectivePerson> collectivePersons = persons.stream()
                .map((Person person) -> {
                    CollectivePerson collectivePerson = map.get(person.getId());

                    if (Objects.isNull(collectivePerson)) {
                        collectivePerson = new CollectivePerson(person.getId());
                        map.put(person.getId(), collectivePerson);

                        collectivePerson.getAddresses().add(person.getAddress());
                        collectivePerson.getPhones().add(person.getPhone());

                        return collectivePerson;
                    } else {
                        collectivePerson.getAddresses().add(person.getAddress());
                        collectivePerson.getPhones().add(person.getPhone());

                        return null;
                    }
                })
                .filter(Objects::nonNull)
                .collect(Collectors.<CollectivePerson>toList());

        collectivePersons.forEach(System.out::println);
    }
}

它完成这项工作并输出为:

CollectivePerson [id=1, addresses=[Address 1, Address 4], phones=[Phone 1, Phone 4]]
CollectivePerson [id=2, addresses=[Address 2, Address 6, Address 5], phones=[Phone 5, Phone 2, Phone 6]]
CollectivePerson [id=3, addresses=[Address 3], phones=[Phone 3]]

但我相信可能有更好的方法,即流式分组方式来实现相同的效果。任何指针都会很棒。

【问题讨论】:

    标签: java lambda java-8 java-stream


    【解决方案1】:

    您可以将Collectors.toMap 与合并功能一起使用:

    public static <T, K, U, M extends Map<K, U>>
    Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper,
                                Function<? super T, ? extends U> valueMapper,
                                BinaryOperator<U> mergeFunction,
                                Supplier<M> mapSupplier)
    

    映射如下所示:

    Map<Long,CollectivePerson> collectivePersons =
      persons.stream()
             .collect(Collectors.toMap (Person::getId,
                                        p -> {
                                          CollectivePerson cp = new CollectivePerson (p.getId());
                                          cp.getAddresses().add (p.getAddress());
                                          cp.getPhones().add(p.getPhone());
                                          return cp;
                                        },
                                        (cp1,cp2) -> {
                                          cp1.getAddresses().addAll(cp2.getAddresses());
                                          cp1.getPhones().addAll(cp2.getPhones());
                                          return cp1;
                                        },
                                        HashMap::new));
    

    您可以使用以下方法轻松地从 Map 中提取 List&lt;CollectivePerson&gt;

    new ArrayList<>(collectivePersons.values())
    

    这是您的示例输入的输出 Map

    {1=CollectivePerson [id=1, addresses=[Address 1, Address 4], phones=[Phone 1, Phone 4]], 
     2=CollectivePerson [id=2, addresses=[Address 2, Address 6, Address 5], phones=[Phone 5, Phone 2, Phone 6]], 
     3=CollectivePerson [id=3, addresses=[Address 3], phones=[Phone 3]]}
    

    【讨论】:

    • 不需要指定HashMap::new;此任务不需要地图是HashMap 的实例...
    • @Holger 你是对的。出于某种原因,我认为toMap 的唯一变体采用合并功能也需要供应商。也就是说,我只是注意到 3 参数 toMap 的实现(即没有供应商)是return toMap(keyMapper, valueMapper, mergeFunction, HashMap::new); :)
    • 是的,在当前的实现中,它总是产生一个HashMap,就像toList()总是产生一个ArrayList,但是,这并不能保证,如果没有要求得到一个这些类型的确切实例,您应该允许实现更改以获得更改所承诺的任何好处。反之亦然,没有一种方法可以在不需要合并功能的情况下获取地图供应商。
    【解决方案2】:

    您应该使用收集器,而不是操纵外部Map。有toMapgroupingBy,两者都允许解决问题,但由于您的类设计有点冗长。主要障碍是缺乏现有的方法,将Person 合并到CollectivePerson 或从给定的Person 实例构造CollectivePerson,或合并两个CollectivePerson 实例的方法。

    使用内置收集器的一种方法是

    List<CollectivePerson> collectivePersons = persons.stream()
        .map(p -> {
            CollectivePerson cp = new CollectivePerson(p.getId());
            cp.getAddresses().add(p.getAddress());
            cp.getPhones().add(p.getPhone());
            return cp;
        })
        .collect(Collectors.collectingAndThen(Collectors.toMap(
            CollectivePerson::getId, Function.identity(),
            (cp1, cp2) -> {
                cp1.getAddresses().addAll(cp2.getAddresses());
                cp1.getPhones().addAll(cp2.getPhones());
                return cp1;
            }),
          m -> new ArrayList<>(m.values())
        ));
    

    但在这种情况下,自定义收集器可能更简单:

    Collection<CollectivePerson> collectivePersons = persons.stream()
        .collect(
            HashMap<Long,CollectivePerson>::new,
            (m,p) -> {
                CollectivePerson cp=m.computeIfAbsent(p.getId(), CollectivePerson::new);
                cp.getAddresses().add(p.getAddress());
                cp.getPhones().add(p.getPhone());
            },
            (m1,m2) -> m2.forEach((l,cp) -> m1.merge(l, cp, (cp1,cp2) -> {
                cp1.getAddresses().addAll(cp2.getAddresses());
                cp1.getPhones().addAll(cp2.getPhones());
                return cp1;
            }))).values();
    

    两者都将受益于合并两个CollectivePerson 实例的预定义方法,而第一个变体也将受益于CollectivePerson(Long id, Set&lt;String&gt; addresses, Set&lt;String&gt; phones) 构造函数,甚至更好的是CollectivePerson(Person p) 构造函数,而第二个变体将受益于CollectivePerson.add(Person p)方法……

    请注意,第二个变体返回 Maps 值的 Collection 视图而不进行复制。如果你真的需要 List,你可以像使用 new ArrayList&lt;&gt;( «map» .values()) 一样简单地收缩它,就像第一个变体在 Finisher 函数中所做的那样。

    【讨论】:

    • 谢谢,我采用了您的第二个变体,速度非常快。在 ~10 秒内对 ~10000000 Persons 进行了操作。
    【解决方案3】:

    使用groupBy 收集器对您的人员进行分组!

    List<CollectivePerson> list = persons.stream().collect(Collectors.groupingBy(Person::getId)).entrySet().stream().map(x -> {
        // map all the addresses from the list of persons sharing the same id
        Set<String> addresses = x.getValue().stream().map(Person::getAddress).collect(Collectors.toSet());
        // map all the phones from the list of persons sharing the same id
        Set<String> phones = x.getValue().stream().map(Person::getPhone).collect(Collectors.toSet());
        // declare this constructor that takes three parameters
        return new CollectivePerson(x.getKey(), addresses, phones);
    }).collect(Collectors.toList());
    

    为此,您需要添加此构造函数:

    public CollectivePerson(Long id, Set<String> addresses, Set<String> phones) {
        this.id = id;
        this.addresses = addresses;
        this.phones = phones;
    }
    

    【讨论】:

      【解决方案4】:
      Map<Long, CollectivePerson> map = persons.stream().
                  collect(Collectors.groupingBy(Person::getId, 
                          Collectors.collectingAndThen(Collectors.toList(),
                                  Main::downColl)));
      

      使用方法引用从具有相同id 的人员列表中创建CollectivePerson 对象。

      public static CollectivePerson downColl(List<Person> ps) {
      
          CollectivePerson cp = new CollectivePerson(ps.get(0).getId());          
          for (Person p:ps) {
              cp.getAddresses().add(p.getAddress());
              cp.getPhones().add(p.getPhone());
          }
          return cp;
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-09-16
        • 2017-12-04
        • 2015-02-11
        • 2015-12-27
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多