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?
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.
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));
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() )
)
)
;
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With