I'm new into programming and now facing a tricky problem: I got an Object which contains a list, so the Object looks like the following:
public class SampleClass {
private UUID id;
private List<ValueObject> values;
// getters, constructor, etc
}
The ValueObject
contains some Strings like:
public class ValueObject {
private String firstName;
private String lastName;
private String address;
// getters, constructor, etc
}
I have a SampleClass
intance which contains a list of multiple ValueObject
s. Some of the ValueObject
s have the same firstName
and lastName
.
What I want to archive is that I want to filter out all ValueObject
within a SampleClass
object having the same firstName
and lastName
. And I want to keep the last (according to the encounter order) ValueObject
out of each group duplicates in the list.
I've tried the following:
SampleClass listWithDuplicates = // intializing SampleClass instance
listWithDuplicates.getValues().stream()
.collect(Collectors.groupingBy(
ValueObject::getLastname,
Collectors.toList()
));
To group it by lastname but how do I find then the matching firstNames, because lastname can be equal but firstname can be different so I want to still keep it in my Object as it not equal. And then how to remove the duplicates? Thank you for your help
Update: The order of the list should not get affected by removing the duplicates. And the
listWithDuplicates
holds a SampleClass Object.
CodePudding user response:
You can solve this problem by using four-args version of the Collector toMap()
, which expects the following arguments:
- keyMapper - a function which generates a Key out of a stream element;
- *valueMapper - a function producing Value from a stream element;
- mergeFunctino - a function responsible for resolving Values mapped to the same Key;
- mapFunctory - allows to specify the required type of Map.
In case if you can't change the implementation of the equals/hashCode
in the ValueObject
you can introduce an auxiliary type that would serve as a Key.
public record FirstNameLastName(String firstName, String lastName) {
public FirstNameLastName(ValueObject value) {
this(value.getFirstName(), value.getLastName);
}
}
Note: if you're OK with overriding the equals/hashCode
contract of the ValueObject
on it's firstName
and lastName
then you don't the auxiliary type shown above. In the code below you can use Function.identity()
as both keyMapper and valueMapper of toMap()
.
And the stream can be implemented like this:
SampleClass listWithDuplicates = // initializing your domain object
List<ValueObject> uniqueValues = listWithDuplicates.getValues().stream()
.collect(Collectors.toMap(
FirstNameLastName::new, // keyMapper - creating Keys
Function.identity(), // valueMapper - generating Values
(left, right) -> right // mergeFunction - resolving duplicates
LinkedHashMap::new // mapFuctory - LinkedHashMap is needed to preserve the encounter order of the elements
))
.values().stream()
.toList();
CodePudding user response:
You can override the equals
and hashCode
methods in your ValueObject
class (not tested):
public class ValueObject {
private String firstName;
private String lastName;
private String adress;
// Constructor...
// Getters and setters...
@Override
public boolean equals(Object obj) {
return obj == this ||(obj instanceof ValueObject
&& ((ValueObject) obj).firstName.equals(this.firstName)
&& ((ValueObject) obj).lastName.equals(this.lastName)
);
}
@Override
public int hashCode() {
return (firstName lastName).hashCode();
}
}
Then all you need is to use Stream#distinct
to remove the duplicates
listWithDuplicates.getValues().stream().distinct()
.collect(Collectors.groupingBy(ValueObject::getLastname, Collectors.toList()));
You can read this answer for a little more information about.
CodePudding user response:
I cannot put it in a stream. But if you indeed create a equals method in the valueObject based on name and first name, you can filter based on the object. Put this in the ValueObject:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ValueObject that = (ValueObject) o;
return Objects.equals(firstName, that.firstName) && Objects.equals(lastName, that.lastName);
}
than it should you work fine with a loop like this:
List<ValueObject> listWithoutDuplicates = new ArrayList<>();
for(var vo: listWithDuplicates){
if(!listWithoutDuplicates.contains(vo)){
listWithoutDuplicates.add(vo);
}
}
But in a stream would be nicer, .. But you can work that out if you've implemented the equals method.
CodePudding user response:
I like the already provided answers, and think they give answer for you, but maybe for learning purposes, another approach could be to use a Collector
that compares firstName and lastName fields of each ValueObject and then retains only the last instance of duplicates.
List<ValueObject> filteredList = listWithDuplicates.getValues().stream()
.collect(Collectors.collectingAndThen(
Collectors.toMap(
vo -> vo.getLastName() vo.getFirstName(),
vo -> vo,
(vo1, vo2) -> vo2
),
map -> new ArrayList<>(map.values())
));
So you would group the ValueObject instances by lastName
and firstName
using a COllector that creates a Map and the keys for the map are the concatenated lastName
and firstName
. The collectingAndThen
collector transforms the Map into a List of ValueObjects.