如何最好地比较Java中的两个集合并对其采取行动?

如何最好地比较Java中的两个集合并对其采取行动?

How Best to Compare Two Collections in Java and Act on Them?

我有同一个对象的两个集合Collection oldSetCollection newSet。所需的逻辑如下:

  • 如果foo位于(*)oldSet中而不是newSet中,则调用doRemove(foo)
  • 否则,如果foo不在oldSet中,而在newSet中,则调用doAdd(foo)
  • 否则,如果两个集合中都包含foo但已对其进行了修改,则调用doUpdate(oldFoo, newFoo)
  • 否则,如果!foo.activated && foo.startDate >= now,则调用doStart(foo)
  • 否则,如果foo.activated && foo.endDate <= now,请调用doEnd(foo)

(*)"中"是指唯一的标识符匹配,不一定是内容匹配。

当前(旧版)代码进行了许多比较,以找出removeSetaddSetupdateSetstartSetendSet,然后循环执行每个项目。

代码非常混乱(部分原因是我已经省略了一些意大利面条逻辑),并且我试图对其进行重构。一些更多的背景信息:

  • 据我所知,oldSetnewSet实际上是由ArrayList支持的
  • 每套包含少于100件物品,最有可能最多20件
  • 尽管设置很少不同,但经常调用此代码(以百万/天为单位)

我的问题:

  • 如果将oldSetnewSet转换为HashMap(此处不关心顺序),以ID作为键,是否会使代码更易于阅读和比较?转换损失多少时间和内存性能?
  • 迭代这两组并执行适当的操作会更高效和简洁吗?

Apache的commons.collections库具有CollectionUtils类,该类提供了易于使用的Collection操作/检查方法,例如交集,差和联合。

org.apache.commons.collections.CollectionUtils API文档在这里。


例如,您可以使用Java 8流

1
set1.stream().filter(s -> set2.contains(s)).collect(Collectors.toSet());

或来自番石榴的Sets类:

1
2
3
4
Set<String> intersection = Sets.intersection(set1, set2);
Set<String> difference = Sets.difference(set1, set2);
Set<String> symmetricDifference = Sets.symmetricDifference(set1, set2);
Set<String> union = Sets.union(set1, set2);


我已经创建了一个大概的视图,我认为您只是在Java中使用Collections Framework。坦白说,正如@Mike Deck指出的那样,我认为这可能是过大了。对于这么少的项目进行比较和处理,我认为从过程的角度来看,数组是一个更好的选择,但这是我的伪编码(因为我很懒)解决方案。我假设Foo类是基于其唯一ID而不是其内容中的所有数据都是可比较的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
Collection<Foo> oldSet = ...;
Collection<Foo> newSet = ...;

private Collection difference(Collection a, Collection b) {
    Collection result = a.clone();
    result.removeAll(b)
    return result;
}

private Collection intersection(Collection a, Collection b) {
    Collection result = a.clone();
    result.retainAll(b)
    return result;
}

public doWork() {
    // if foo is in(*) oldSet but not newSet, call doRemove(foo)
    Collection removed = difference(oldSet, newSet);
    if (!removed.isEmpty()) {
        loop removed {
            Foo foo = removedIter.next();
            doRemove(foo);
        }
    }
    //else if foo is not in oldSet but in newSet, call doAdd(foo)
    Collection added = difference(newSet, oldSet);
    if (!added.isEmpty()) {
        loop added  {
            Foo foo = addedIter.next();
            doAdd(foo);
        }
    }

    // else if foo is in both collections but modified, call doUpdate(oldFoo, newFoo)
    Collection matched = intersection(oldSet, newSet);
    Comparator comp = new Comparator() {
        int compare(Object o1, Object o2) {
            Foo f1, f2;
            if (o1 instanceof Foo) f1 = (Foo)o1;
            if (o2 instanceof Foo) f2 = (Foo)o2;
            return f1.activated == f2.activated ? f1.startdate.compareTo(f2.startdate) == 0 ? ... : f1.startdate.compareTo(f2.startdate) : f1.activated ? 1 : 0;
        }

        boolean equals(Object o) {
             // equal to this Comparator..not used
        }
    }
    loop matched {
        Foo foo = matchedIter.next();
        Foo oldFoo = oldSet.get(foo);
        Foo newFoo = newSet.get(foo);
        if (comp.compareTo(oldFoo, newFoo ) != 0) {
            doUpdate(oldFoo, newFoo);
        } else {
            //else if !foo.activated && foo.startDate >= now, call doStart(foo)
            if (!foo.activated && foo.startDate >= now) doStart(foo);

            // else if foo.activated && foo.endDate <= now, call doEnd(foo)
            if (foo.activated && foo.endDate <= now) doEnd(foo);
        }
    }
}

关于您的问题:
如果将oldSet和newSet转换为HashMap(此处不考虑顺序),并且将ID作为键,是否会使代码更易于阅读和比较?转换损失多少时间和内存性能?
我认为您可能会通过使用Map BUT使代码更易读...在转换过程中可能会使用更多的内存和时间。

迭代这两组并执行适当的操作会更高效和简洁吗?
是的,这将是两全其美的做法,特别是如果您遵循@Mike Sharek的建议,即使用特殊方法滚动自己的列表,或遵循类似Visitor Design模式的操作来遍历您的收藏夹并处理每个项目。


我认为最简单的方法是使用apache collections api-CollectionUtils.subtract(list1,list2),只要列表的类型相同。


我将移至列表并通过以下方式解决它:

  • 如果列表中的对象不可比较,则使用自定义比较器按ID升序对两个列表进行排序
  • 像在合并排序算法中的合并阶段那样,对两个列表中的元素进行迭代,但是要检查逻辑,而不是合并列表。
  • 该代码或多或少是这样的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    /* Main method */
    private void execute(Collection<Foo> oldSet, Collection<Foo> newSet) {
      List<Foo> oldList = asSortedList(oldSet);
      List<Foo> newList = asSortedList(newSet);

      int oldIndex = 0;
      int newIndex = 0;
      // Iterate over both collections but not always in the same pace
      while( oldIndex < oldList.size()
          && newIndex < newIndex.size())  {
        Foo oldObject = oldList.get(oldIndex);
        Foo newObject = newList.get(newIndex);

        // Your logic here
        if(oldObject.getId() < newObject.getId()) {
          doRemove(oldObject);
          oldIndex++;
        } else if( oldObject.getId() > newObject.getId() ) {
          doAdd(newObject);
          newIndex++;
        } else if( oldObject.getId() == newObject.getId()
                && isModified(oldObject, newObject) ) {
          doUpdate(oldObject, newObject);
          oldIndex++;
          newIndex++;
        } else {
          ...
        }
      }// while

      // Check if there are any objects left in *oldList* or *newList*

      for(; oldIndex < oldList.size(); oldIndex++ ) {
        doRemove( oldList.get(oldIndex) );  
      }// for( oldIndex )

      for(; newIndex < newList.size(); newIndex++ ) {
        doAdd( newList.get(newIndex) );
      }// for( newIndex )
    }// execute( oldSet, newSet )

    /** Create sorted list from collection
        If you actually perform any actions on input collections than you should
        always return new instance of list to keep algorithm simple.
    */

    private List<Foo> asSortedList(Collection<Foo> data) {
      List<Foo> resultList;
      if(data instanceof List) {
         resultList = (List<Foo>)data;
      } else {
         resultList = new ArrayList<Foo>(data);
      }
      Collections.sort(resultList)
      return resultList;
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public static boolean doCollectionsContainSameElements(
            Collection<Integer> c1, Collection<Integer> c2){

        if (c1 == null || c2 == null) {
            return false;
        }
        else if (c1.size() != c2.size()) {
            return false;
        } else {    
            return c1.containsAll(c2) && c2.containsAll(c1);
        }      
    }


    对于这么小的集合,通常不值得将其从Array转换为HashMap / set。实际上,最好将它们保留在数组中,然后按键对它们进行排序并同时遍历两个列表以进行比较。


    为了兼容列表或集合,我们可以使用Arrays.equals(object[], object[])。它将仅检查值。要获取Object[],我们可以使用Collection.toArray()方法。


    推荐阅读