listas - ¿Cuál es la mejor manera de comparar dos colecciones en Java y actuar sobre ellas?
list java español (8)
Tengo dos colecciones del mismo objeto, Collection<Foo> oldSet
y Collection<Foo> newSet
. La lógica requerida es la siguiente:
- si
foo
está en (*)oldSet
pero nonewSet
, llama adoRemove(foo)
- de lo contrario, si
foo
no está enoldSet
pero ennewSet
, llame adoAdd(foo)
- De lo contrario, si
foo
está en ambas colecciones pero modificado, llama adoUpdate(oldFoo, newFoo)
- else if
!foo.activated && foo.startDate >= now
, llame adoStart(foo)
- else if
foo.activated && foo.endDate <= now
, llame adoEnd(foo)
(*) "in" significa que el identificador único coincide, no necesariamente el contenido.
El código actual (heredado) hace muchas comparaciones para descubrir addSet
, updateSet
, startSet
, startSet
y endSet
, y luego endSet
para actuar sobre cada elemento.
El código es bastante desordenado (en parte porque ya he omitido algo de lógica de spaghetti) y estoy tratando de refactorizarlo. Algo más de información de fondo:
- Por lo que sé, el
oldSet
ynewSet
están respaldados porArrayList
- Cada conjunto contiene menos de 100 elementos, lo más probable es que tenga un máximo de 20
- Este código se llama con frecuencia (medido en millones / día), aunque los conjuntos rara vez difieren
Mis preguntas:
- Si convierto a
oldSet
ynewSet
enHashMap<Foo>
(el orden no es relevante aquí), con los ID como claves, ¿haría el código más fácil de leer y más fácil de comparar? ¿Cuánto de tiempo y rendimiento de memoria es pérdida en la conversión? - ¿Sería más eficiente y conciso iterar los dos conjuntos y realizar la operación apropiada?
Para un conjunto tan pequeño, en general, no vale la pena convertirlo de un array a un HashMap / set. De hecho, es mejor que los guardes en una matriz y luego los clasifiques por clave e iteremos sobre ambas listas simultáneamente para hacer la comparación.
Me movería a las listas y lo resolvería de esta manera:
- Ordene ambas listas por ID ascendente usando Comparator personalizado si los objetos en las listas no son Comparables
- Itere sobre los elementos en ambas listas, como en la fase de fusión en el algoritmo de ordenación por fusión , pero en lugar de combinar listas, verificas tu lógica.
El código sería más o menos así:
/* 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;
}
Creo que la forma más fácil de hacerlo es mediante el uso de las colecciones de apache api - CollectionUtils.subtract (list1, list2) siempre que las listas sean del mismo tipo.
Para comparar una lista o conjunto, podemos usar Arrays.equals(object[], object[])
. Verificará solo los valores. Para obtener el Object[]
podemos usar el método Collection.toArray()
.
Puede usar flujos Java 8, por ejemplo
set1.stream().filter(s -> set2.contains(s)).collect(Collectors.toSet());
o Establece una clase de 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);
He creado una aproximación de lo que creo que estás buscando simplemente usando el Framework Collections en Java. Francamente, creo que probablemente sea excesivo como lo señala @Mike Deck. Para un conjunto tan pequeño de elementos para comparar y procesar, creo que los arreglos serían una mejor opción desde un punto de vista de procedimiento, pero aquí está mi solución pseudo-codificada (porque soy floja). Supongo que la clase Foo es comparable en función de su id. Única y no de todos los datos contenidos en ella:
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);
}
}
}
En cuanto a sus preguntas: Si convierto a oldSet y newSet en HashMap (aquí no importa el orden), con las identificaciones como claves, ¿haría el código más fácil de leer y más fácil de comparar? ¿Cuánto de tiempo y rendimiento de memoria es pérdida en la conversión? Creo que probablemente harías el código más legible usando un Mapa PERO ... probablemente usarías más memoria y tiempo durante la conversión.
¿Sería más eficiente y conciso iterar los dos conjuntos y realizar la operación apropiada? Sí, esto sería lo mejor de ambos mundos, especialmente si sigues los consejos de @Mike Sharek de rodar tu propia lista con los métodos especializados o si sigues algo parecido al patrón de diseño de visitante para recorrer tu colección y procesar cada elemento.
La biblioteca commons.collections de Apache tiene una clase CollectionUtils que proporciona métodos fáciles de usar para la manipulación / comprobación de colecciones, como la intersección, la diferencia y la unión.
Los documentos de la API org.apache.commons.collections.CollectionUtils están aquí .
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);
}
}