Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java 8 Stream Mapping Grouping operation

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 + "]";
    }
}

I would like to have stream operation so that:

  • The Person mapped into CollectivePerson
  • The address and phone of Person merged into addresses and phones respectively in CollectivePerson for all the Persons having same id

I have written the following piece of code for this purpose:

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

It does the job and outputs as:

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]]

But I believe there could be a better way, stream way of grouping to achieve the same. Any pointer would be great.

like image 946
Tapas Bose Avatar asked Jan 04 '23 07:01

Tapas Bose


2 Answers

You can use Collectors.toMap with a merge function:

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)

The mapping looks like this:

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));

You can easily extract the List<CollectivePerson> from that Map using:

new ArrayList<>(collectivePersons.values())

Here's the output Map for your sample input:

{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]]}
like image 120
Eran Avatar answered Jan 13 '23 17:01

Eran


Instead of manipulating an external Map, you should use a collector. There are toMap and groupingBy, both allowing to solve the problem, though a bit verbose due to your class design. The main obstacle is the lack of an existing method to either, merge a Person into a CollectivePerson or construct a CollectivePerson from a given Person instance, or a method for merging two CollectivePerson instances.

One way to do it using built-in collectors would be

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())
    ));

but in this case, a custom collector might be simpler:

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();

Both would benefit from a predefined method to merge two CollectivePerson instances, whereas the first variant would also benefit from a CollectivePerson(Long id, Set<String> addresses, Set<String> phones) constructor or even better, a CollectivePerson(Person p) constructor while the second would benefit from an CollectivePerson.add(Person p) method…

Note that the second variant returns the Collection view of the Maps values without copying. If you really need a List, you can contract it as easy as using new ArrayList<>( «map» .values()) like the first variant does in the finisher function.

like image 45
Holger Avatar answered Jan 13 '23 17:01

Holger