3

Consider I have a Java object Student only with getters and setters that I cannot change, and two Lists – students1 and students2 – both of type ArrayList<Student>.

How can I call students1.removeAll(students2) by passing my own equals() method?

This is because I cannot modify the Student class.

Kaan
  • 5,434
  • 3
  • 19
  • 41

4 Answers4

6

Well one option might be to use the decorator pattern here, and just wrap the Student class in a new class which then overrides the equals() method with whatever new logic you want, e.g.

public class YourStudent {
    private Student instance;

    public YourStudent() { ... }

    @Override
    public boolean equals(Object obj) { ... }
}
Tim Biegeleisen
  • 502,043
  • 27
  • 286
  • 360
4

You can do it using removeIf and stream

students1.removeIf(stu1->students2.stream()
                                  .anyMatch(stu2->stu2.getId().equals(stu1.getId()) && /* other properties */ );
Ryuzaki L
  • 37,302
  • 12
  • 68
  • 98
  • 2
    Stream inside stream. Isn't it inefficient? – Prasannjeet Singh Jan 28 '20 at 16:10
  • 1
    removeIf i using iterator https://github.com/AdoptOpenJDK/openjdk-jdk11/blob/master/src/java.base/share/classes/java/util/Collection.java#L539 and since you can't override `equals()` method this will be the best way @misteeque – Ryuzaki L Jan 28 '20 at 16:15
  • 2
    @misteeque `listA.removeAll(listB)` is no faster. Both are `O(n*m)`. – MikeFHay Jan 28 '20 at 17:24
  • 1
    `removeIf` is the right thing, however, to avoid the O(n×m) time complexity, you should collect the Ids from `students2` into a `Set` first, e.g. `Set set = students2.stream().map(Student::getId).collect(Collectors.toSet()); students1.removeIf(st -> set.contains(st.getId()));` – Holger Jan 29 '20 at 09:46
  • 1
    @Deadpool the `default` implementation uses an `Iterator`, but `ArrayList` overrides it with an implementation which is significantly more efficient than an `Iterator` based operation could ever be. This has been described in [this answer](https://stackoverflow.com/a/57094996/2711488). – Holger Jan 29 '20 at 09:49
  • @Holger I agree collecting Student id's into `Set` but OP did not mentioned what properties that he need to check to find duplicates. – Ryuzaki L Jan 30 '20 at 01:29
  • 1
    You can always replace ID by an object holding all relevant properties. This can be a dedicated class or just `List` as an ad-hoc key type , e.g. `Set> set = students2.stream().map(st -> Arrays.asList(st.getProperty1(),st.getProperty2())).collect(Collectors.toSet()); students1.removeIf(st -> set.contains(Arrays.asList(st.getProperty1(),st.getProperty2())));` – Holger Jan 30 '20 at 07:55
2

You could convert your list to a stream and use the filter method, for example:

List<Student> filteredStudents = students2
    .stream().filter(s -> s.getName().compareTo("John")) // Write your filter
    .collect(Collectors.toCollection(() -> new ArrayList<>()));

students1.removeAll(students2);

Edit to explain what to do with students2.

  • Since the OP is modifying the list in-place, your approach gives different results than what was asked for. However, often it is a bad idea to modify a collection in place, so you could improve your answer by highlighting this difference and explaining when your method might be a better option than say, [`removeIf()`.](https://stackoverflow.com/a/59952456/3474) – erickson Jan 28 '20 at 15:56
  • Sorry but I don't know if I undertand what you mean by the difference between my answer and removeIf(). Do you refer to ConcurrentModificationException? – Jose Alberto Jan 28 '20 at 16:15
  • In your solution, you're comparing each item with a `constant` value, `John`, while in my case, I'll have to write another stream there, because I will compare each item with a list of items. I was expecting something that does not require me to use stream inside a stream. – Prasannjeet Singh Jan 28 '20 at 16:21
  • I think I did not explain well enough. My idea was to use the filter method to create a new List with the elements that match your "equal" condition. Then pass this list to students1.removeAll(filteredStudents). Do you know the equal implementation of Student class? Does the removeAll remove all the elements that match students2? – Jose Alberto Jan 28 '20 at 16:44
  • 1
    No, I mean that in the OP's example, the contents of the `List students1` is modified. But in your approach, the original list is unchanged, and a new list is created and assigned to `filteredStudents`. This can be a big difference if others have a reference to the original list, which could be good or bad, depending on the context. – erickson Jan 28 '20 at 16:46
  • @erickson I edited my answer to explain a bit better. The point of `filteredStudents` is, if there is any student left, pass this list to `student1.removeAll` and then you would remove those students from `student1`. The result of this relies on the implementation of `Student.equals()`. – Jose Alberto Jan 28 '20 at 16:54
0

Are you using Java 8? If yes, you could consider using Collection::removeIf with a lambda expression containing your custom equality check.

If you're forced to use Java 7 or below, then you'll probably have to create a new class extending Student if you want to stick with Collections::removeAll.

Another option would be to loop through your lists manually, removing everything from the first list that matches any element of your second list (Watch out though, since this will be a concurrent modification and may throw Exceptions if not implemented correctly).

Nightara
  • 117
  • 5