Как лучше всего сравнивать две коллекции в Java и действовать на них?
У меня есть две коллекции одного и того же объекта, Collection<Foo> oldSet
и Collection<Foo> newSet
. Требуемая логика следующая:
- Если
foo
находится в (*) oldSet
, но не newSet
, вызовите doRemove(foo)
- else, если
foo
не находится в oldSet
, но в newSet
, вызовите doAdd(foo)
- else, если
foo
находится в обеих коллекциях, но изменен, вызовите doUpdate(oldFoo, newFoo)
- else if
!foo.activated && foo.startDate >= now
, вызов doStart(foo)
- else if
foo.activated && foo.endDate <= now
, вызов doEnd(foo)
(*) "in" означает, что уникальный идентификатор совпадает, не обязательно, содержимое.
Текущий (устаревший) код выполняет множество сравнений, чтобы определить removeSet
, addSet
, updateSet
, startSet
и endSet
, а затем цикл для каждого элемента.
Код довольно грязный (отчасти потому, что я уже оставил некоторую логику спагетти), и я пытаюсь его реорганизовать. Дополнительная информация:
- Насколько я знаю,
oldSet
и newSet
на самом деле поддерживаются ArrayList
- Каждый набор содержит менее 100 наименований, скорее всего, максимум в 20
- Этот код часто называется (измеряется в миллионы/день), хотя наборы редко отличаются
Мои вопросы:
- Если я конвертирую
oldSet
и newSet
в HashMap<Foo>
(порядок здесь не беспокоит), с идентификаторами в качестве ключей, облегчит ли его чтение и будет легче сравнивать? Сколько времени и производительности памяти является потерей при конвертации?
- Итерация двух наборов и выполнение соответствующей операции будет более эффективной и лаконичной?
Ответы
Ответ 1
Библиотека Apache commons.collections имеет класс CollectionUtils, который предоставляет простые в использовании методы для манипулирования/проверки коллекции, такие как пересечение, различие и объединение.
Документы API org.apache.commons.collections.CollectionUtils здесь.
Ответ 2
Вы можете использовать потоки Java 8, например
set1.stream().filter(s -> set2.contains(s)).collect(Collectors.toSet());
или Sets класс из Guava
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);
Ответ 3
Я создал приближение того, что, как я думаю, вы ищете, просто используя Framework Collections Framework в Java. Честно говоря, я думаю, что это, вероятно, слишком велико, как указывает @Mike Deck. Для такого небольшого набора элементов для сравнения и обработки я думаю, что массивы будут лучшим выбором с процедурной точки зрения, но вот мое псевдокодированное (потому что я ленивое) решение. У меня есть предположение, что класс Foo сопоставим на основе его уникального идентификатора, а не всех данных в нем:
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 (порядок здесь не беспокоит), с идентификаторами в качестве ключей, облегчит ли его чтение и будет проще сравнивать? Сколько времени и производительности памяти является потерей при конверсии?
Я думаю, что вы, вероятно, сделаете код более читаемым, используя карту BUT... вы, вероятно, будете использовать больше памяти и времени во время преобразования.
Будет ли итерация двух наборов и выполнение соответствующей операции более эффективной и сжатой?
Да, это было бы лучшим из обоих миров, особенно если бы вы следовали совету @Mike Sharek о сворачивании собственного списка с помощью специализированных методов или следовали примеру шаблона Visitor Design для запуска вашей коллекции и обработки каждого элемента.
Ответ 4
Я бы переместился в списки и решил его так:
- Сортируйте оба списка по возрастанию по имени, используя Comparator, если объекты в списках не Comparable
- Итерации по элементам в обоих списках, например, в фазе слияния алгоритм сортировки слияния, но вместо объединения списков вы проверяете свою логику.
Код будет примерно таким же:
/* 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;
}
Ответ 5
Я думаю, что самый простой способ сделать это - использовать коллекции apache - CollectionUtils.subtract(list1, list2), поскольку списки одного типа.
Ответ 6
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);
}
}
Ответ 7
Для небольшого набора, как правило, не стоит преобразовывать его из массива в HashMap/set. Фактически, вам, вероятно, лучше всего держать их в массиве, а затем сортировать их по ключевым словам и выполнять итерацию по обоим спискам одновременно для сравнения.
Ответ 8
Для сопоставления списка или набора мы можем использовать Arrays.equals(object[], object[])
. Он будет проверять только значения. Чтобы получить Object[]
, мы можем использовать метод Collection.toArray()
.