Java 8流映射分组操作

4

I have following two classes:

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,对于所有具有相同idPerson
为此,我编写了以下代码:
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]]

但我相信有一种更好的方式,流式分组可以实现相同的效果。任何指针都将是很棒的。

4个回答

5
你可以使用具有合并功能的 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<CollectivePerson>:
new ArrayList<>(collectivePersons.values())

以下是您示例输入的输出Map

以下是您示例输入的输出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]]}

1
不需要指定 HashMap::new;这个任务不要求该映射是 HashMap 的实例... - Holger
@Holger 你是对的。由于某种原因,我认为只有带有合并函数的toMap变体还需要一个供应商。话虽如此,我刚刚注意到三个参数的toMap(即没有供应商)的实现是return toMap(keyMapper, valueMapper, mergeFunction, HashMap::new); :) - Eran
1
是的,在当前实现中,它总是生成一个HashMap,就像toList()总是生成一个ArrayList一样,但是这并不是保证的。如果没有要求获取这些类型的确切实例,您应该允许实现根据更改承诺的任何好处进行更改。而且正好相反,没有采用映射供应商而不需要合并函数的方法。 - Holger

3

不要直接操作外部的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<String> addresses, Set<String> phones)构造函数或者更好的CollectivePerson(Person p)构造函数中受益,而第二种变体则可以从CollectivePerson.add(Person p)方法中受益。
请注意,第二种变体返回Map的值的Collection视图而不进行复制。如果您确实需要List,则可以像第一种变体在finisher函数中使用new ArrayList<>( «map» .values())这样轻松地创建它。

{btsdaf} - Tapas Bose

1
使用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;
}

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

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