2

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 ValueObjects. Some of the ValueObjects 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.

Alexander Ivanchenko
  • 25,667
  • 5
  • 22
  • 46
noob123
  • 63
  • 1
  • 2
  • 9

4 Answers4

1

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.

Cardinal System
  • 2,749
  • 3
  • 21
  • 42
  • Thanks for your answer! Does ist keep the lastest entry when using distinct? – noob123 Dec 22 '22 at 14:42
  • @noob123 `distinct()` operation maintains a `LinkedHashSet` under the hood, and it would preserve the **first** encountered element in the stream from a group of elements identical according to the `equals()` method. – Alexander Ivanchenko Dec 22 '22 at 14:52
  • Please, don't get offended, but you're wrong (it's a matter of technical correctness, nothing personal) - the **first** encountered value would be preserved. Here's a [short code snippet](https://www.jdoodle.com/ia/BwC) that proves that. – Alexander Ivanchenko Dec 22 '22 at 15:11
  • Here's what [`Set.add()`](https://docs.oracle.com/en/java/javase/17/docs/api//java.base/java/util/HashSet.html#add(E)) contract says *"Adds the specified element to this set **if it is not already present**"*. Duplicated element would not be updated. – Alexander Ivanchenko Dec 22 '22 at 15:16
  • @AlexanderIvanchenko thank you for pointing that out! I suppose I will just stick with my original answer. – Cardinal System Dec 22 '22 at 15:33
1

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();
Alexander Ivanchenko
  • 25,667
  • 5
  • 22
  • 46
  • Thanks for your answer! Does this approach keep the order and take the latest? – noob123 Dec 22 '22 at 15:09
  • @noob123 Yes it does, this requirement is reflected in the implementation of the *merge function* `(left, right) -> right`. While resolving duplicates, the latest value would be preserved. – Alexander Ivanchenko Dec 22 '22 at 15:18
  • @noob123 From your question it's not very clear what should be the resulting type - a `List`, a `Map`? – Alexander Ivanchenko Dec 22 '22 at 15:27
  • Thank you! i tried it out and it works! But is there a way to not change the order of the other entries inside the list? so to intial state: V1 Duplicate1 Duplicate2 V2 V3 desired state: V1 Duplicate2 V2 V3 – noob123 Dec 22 '22 at 15:28
  • @noob123 Sure, you need to specify one additional argument in the `toMap()`, namely *mapFactory*. Can you add this requirement to the question. – Alexander Ivanchenko Dec 22 '22 at 15:29
0

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.

Yannick Mussche
  • 316
  • 2
  • 12
0

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.

lapadets
  • 1,067
  • 10
  • 38