18

I have two lists. They contain objects of different types, but both types contain id and name, and id is what I am comparing on. List one is fetched from DB, and list two is sent from frontend.

What I need to do is loop through them and find which list item is newly added and which one was deleted.

I was able to do it, but the problem is that it look ugly.

Let's say I have a object that is called NameDTO which can have id and name. List two is filled with that type of objects.

This is how I did it:

final ArrayList<NamedDTO> added = new ArrayList<>();
final ArrayList<NamedDTO> removed = new ArrayList<>();

for(NamedDTO listTwoObject : listTwo) {
   boolean contained = false;
   for(SomeObject listOneObject : listOne) {
       if(listTwoObject.getId().equals(listOneObject.getId()) {
           contained = true;
       }
   }
   if(!contained) {
      added.add(listTwoObject);
   }
}

for(SomeObject listOneObject : listOne) {
   boolean contained = false;
   for(NamedDTO listTwoObject : listTwo) {
       if(listTwoObject.getId().equals(listOneObject.getId()) {
           contained = true;
       }
   }
   if(!contained) {
      removed.add(new NamedDTO(listOneObject.getId(), listOneObject.getName()));
  }
}

This works, I have tested it. Are there better solutions? I was thinking of using Sets so I can compare them, Is there a downside to that ?

mirzak
  • 1,043
  • 4
  • 15
  • 30

4 Answers4

19

If I understand correctly, this is the example scenario:

  • listOne [datab] items: [A, B, C, D]
  • listTwo [front] items: [B, C, D, E, F]

and what you need to get as an effect is:

  • added: [E, F]
  • deleted: [A]

First thing first, I would use some type adapter or extend the different types from one common class and override the equals method so you can match them by id and name

Secondly, this is very easy operations on sets (you could use set's but list are fine too). I recommend using a library: https://commons.apache.org/proper/commons-collections/apidocs/org/apache/commons/collections4/CollectionUtils.html

And now basically:

  • added is listTwo - listOne
  • deleted is listOne - listTwo

and using java code:

  • added: CollectionUtils.removeAll(listTwo, listOne)
  • deleted: CollectionUtils.removeAll(listOne, listTwo)

Otherwise, all collections implementing Collection (Java Docs) also has removeAll method, which you can use.

Atais
  • 10,857
  • 6
  • 71
  • 111
  • 1
    This answer would make sense if the OP was okay with using a third party library. We can't be sure unless we ask. – Chetan Kinger Feb 03 '17 at 14:24
  • @CKing ok we can do without as well. – Atais Feb 03 '17 at 14:27
  • I have looked into this, and found it very simple and easy to read, which in my case is important since i am not the only developer working on this project. I will use this. – mirzak Feb 03 '17 at 18:41
18

I propose solution using java 8 streams:

    ArrayList<ObjOne> list = new ArrayList<>(Arrays.asList(new ObjOne("1","1"),new ObjOne("3","3"),new ObjOne("2","2")));
    ArrayList<ObjTwo> list2 = new ArrayList<>(Arrays.asList(new ObjTwo("1","1"),new ObjTwo("3","3"),new ObjTwo("4","4")));

    List<ObjOne> removed = list.stream().filter(o1 -> list2.stream().noneMatch(o2 -> o2.getId().equals(o1.getId())))
            .collect(Collectors.toList());
    System.out.print("added ");
    removed.forEach(System.out::println);

    List<ObjTwo> added = list2.stream().filter(o1 -> list.stream().noneMatch(o2 -> o2.getId().equals(o1.getId())))
             .collect(Collectors.toList());

    System.out.print("removed ");
    added.forEach(System.out::println);

This is basically your solution but implemented using streams, which will make your code shorter and easer to read

Kamil Banaszczyk
  • 1,133
  • 1
  • 6
  • 23
  • This is basically just a different way of writing the OP's approach - it has the same complexity (`list.size() * list2.size()`). Still, +1 for a slightly more compact version – Hulk Feb 06 '17 at 10:10
  • Thanks, as you can see i commented below code, that this is his solution, but it's shorter and easier to read :) – Kamil Banaszczyk Feb 06 '17 at 10:38
  • 2
    Important to know, API level 24 or higher is required to use Stream. So not usable for everyone. – Izoman Jan 22 '20 at 15:03
10

This nested list processing is not only ugly, it’s inefficient. You are always better off storing the IDs of one list into a Set allowing efficient lookup, then process the other list utilizing the Set. This way, you’re not performing list1.size() times list2.size() operations, but list1.size() plus list2.size() operations, which is a significant difference for larger lists. Then, since both operations are fundamentally the same, it’s worth abstracting them into a method:

public static <A,B,R,ID> List<R> extract(
    List<A> l1, List<B> l2, Function<A,ID> aID, Function<B,ID> bID, Function<A,R> r) {

    Set<ID> b=l2.stream().map(bID).collect(Collectors.toSet());
    return l1.stream().filter(a -> !b.contains(aID.apply(a)))
             .map(r).collect(Collectors.toList());
}

This method can be used as

List<NamedDTO> added   = extract(listTwo, listOne, NamedDTO::getId, SomeObject::getId,
                                 Function.identity());
List<NamedDTO> removed = extract(listOne, listTwo, SomeObject::getId, NamedDTO::getId,
                                 so -> new NamedDTO(so.getId(), so.getName()));

Since swapping the two lists requires the helper method to be independent from the element types, it expects functions for accessing the id property, which can be specified via method references. Then, a function describing the result element is required, which is an identity function in one case (just getting the NamedDTO) and a lambda expression constructing a NamedDTO from SomeObject in the other.

The operation itself is as straight-forward as described above, iterate over one list, map to the id and collect into a Set, then, iterate over the other list, keep only elements whose id is not in the set, map to the result type and collect into a List.

Holger
  • 285,553
  • 42
  • 434
  • 765
4

If these id's are unique, you could put them in a HashSet and find thus the ids you are interested in:

    Set<Integer> uiList = Stream.of(new FromUI(1, "db-one"), new FromUI(2, "db-two"), new FromUI(3, "db-three"))
            .map(FromUI::getId)
            .collect(Collectors.toCollection(HashSet::new));
    Set<Integer> dbList = Stream.of(new FromDB(3, "ui-one"), new FromDB(5, "ui-five"))
            .map(FromDB::getId)
            .collect(Collectors.toCollection(HashSet::new));

    uiList.removeIf(dbList::remove);

added/uiSet :   [1,2]
removed/dbSet : [5]

I've created FromUI and FromDB classes with a constructor that takes the id and name as input.

I also assume that if an element is contained in the uiSet, but not in dbSet is has been added and the other way around.

Eugene
  • 117,005
  • 15
  • 201
  • 306
  • 2
    Now, you’ve simplified to much. While `Collectors.toSet()` currently returns a `HashSet`, it’s not guaranteed to return a mutable `Set`, hence, you have to use `toCollection(HashSet::new)`. By the way, the `oneSet.removeIf(otherSet::remove)` trick is neat, I’m sure, I might find a use for it sometimes. This will work even with `map1.keySet().removeIf(map2.keySet()::remove)`, which allows to have the owners in the `values()`… – Holger Feb 03 '17 at 15:15
  • 1
    @Holger oh damn that is scary! I've read the doc (I should have by now) and I had not known about *mutability*. Big thx for this. – Eugene Feb 03 '17 at 15:20