Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Create a nested parent child list in Java 8

I am new to Java 8 and need a solution to the below problem.

I have two classes as below:

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

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

Now I have a list coming from database like this:

List<Person> findPerson;

adam
26
<123, xyz, yyy>

adam
26
<456, rrr, kkk>

bill
31
<666, uuu, hhh>

Now I need to combine the same person objects with different address objects in one, like below?

List<Person> findPerson;

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

bill
31
<666, uuu, 999>

How this can be done in Java 8 streams?

like image 733
user3378550 Avatar asked Feb 04 '18 00:02

user3378550


3 Answers

I'd suggest you implement equals and hashcode in your Person class.

Example:

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

Then you can have a Map<Person, List<Address>> as the receiver type of the result set.

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

This solution utilizes the flatMapping collector which is only available as of Java-9.

if you're still on Java-8 then you can do:

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 - As is, I am currently assuming two or more people are considered equal based on name and age, however, if it's only based on name or just the age then you'll need to tweek the equals / hashcode methods a little bit.

like image 116
Ousmane D. Avatar answered Sep 21 '22 18:09

Ousmane D.


Assuming Person implements hashCode and equals consistently, you could collect to a Map:

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

This uses Collectors.toMap, which works by merging a new person with an old person that's already in the map, and by merging we mean adding the addresses.

Now, in the values of the map, you have the merged people:

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

If you need to return a list:

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

EDIT:

This assumes that the List<Address> of each person is mutable (so that addAll works). This is not always the case, particularly if you want to not break encapsulation, you shouldn't let the addresses of your Person objects be modified from outside.

In this case, you could provide a method in Person that would be in charged of merging the addresses without breaking encapsulation:

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

Or if the list of addresses is immutable:

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

Now the code in the collector can be refactored to:

Map<Person, Person> map = findPerson.stream()
    .collect(Collectors.toMap(
        p -> p, 
        p -> p,
        Person::merge));
like image 45
fps Avatar answered Sep 18 '22 18:09

fps


Another approach, won't require overriding equals and hashCode:

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() ) 
        ) 
    )
;
like image 26
tsolakp Avatar answered Sep 22 '22 18:09

tsolakp