El código que se muestra a continuación genera:
[b]
[a, b]
Sin embargo, esperaría que imprima dos líneas idénticas en la salida.
import java.util.*; public class Test{ static void test(String... abc) { Set<String> s = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER); s.addAll(Arrays.asList("a", "b")); s.removeAll(Arrays.asList(abc)); System.out.println(s); } public static void main(String[] args) { test("A"); test("A", "C"); } }
La especificación establece claramente que removeAll
"Elimina todos los elementos de esta colección que también están contenidos en la colección especificada".
Entonces, según tengo entendido, el comportamiento actual es impredecible. por favor ayúdame a entender esto
Solo lee la documentación en parte. Olvidaste un párrafo importante de TreeSet
:
Tenga en cuenta que el orden mantenido por un conjunto (ya sea que se proporcione o no un comparador explícito) debe ser consistente con los iguales si se va a implementar correctamente la interfaz del
Set
. (ConsulteComparable
oComparator
para obtener una definición precisa de consistente con iguales). Esto se debe a que la interfaz Set se define en términos de la operación de iguales, pero una instancia deTreeSet
realiza todas las comparaciones de elementos utilizando su método compareTo (o compare), por lo que dos los elementos que se consideran iguales por este método son, desde el punto de vista del conjunto, iguales. El comportamiento de un conjunto está bien definido incluso si su ordenamiento es inconsistente con los iguales; simplemente no cumple con el contrato general de la interfaz Set .
Ahora, la implementación de removeAll
proviene de AbstractSet
y utiliza el método equals
. De acuerdo con su código, tendrá que "a".equals("A")
no es true
, por lo que los elementos no se consideran iguales incluso si proporcionó un comparador que los administra cuando se usa en el TreeSet
. Si intenta con un envoltorio, el problema desaparece:
import java.util.*; import java.lang.*; class Test { static class StringWrapper implements Comparable<StringWrapper> { public final String string; public StringWrapper(String string) { this.string = string; } @Override public boolean equals(Object o) { return o instanceof StringWrapper && ((StringWrapper)o).string.compareToIgnoreCase(string) == 0; } @Override public int compareTo(StringWrapper other) { return string.compareToIgnoreCase(other.string); } @Override public String toString() { return string; } } static void test(StringWrapper... abc) { Set<StringWrapper> s = new TreeSet<>(); s.addAll(Arrays.asList(new StringWrapper("a"), new StringWrapper("b"))); s.removeAll(Arrays.asList(abc)); System.out.println(s); } public static void main(String[] args) { test(new StringWrapper("A")); test(new StringWrapper("A"), new StringWrapper("C")); } }
Esto se debe a que ahora está proporcionando una implementación consistente entre equals
y compareTo
de su objeto para que nunca tenga un comportamiento incoherente entre cómo se agregan los objetos dentro del conjunto ordenado y cómo los usa todo el comportamiento abstracto del conjunto.
Esto es cierto en general, una especie de regla de tres para el código Java: si implementa compareTo
o equals
o hashCode
, siempre debe implementarlos todos para evitar problemas con las colecciones estándar (incluso si hashCode es menos crucial a menos que esté usando estos objetos en cualquier colección hash). Esto se especifica muchas veces en la documentación de Java.
Esta es una inconsistencia en la implementación de TreeSet<E>
, bordeando el error. El código ignorará el comparador personalizado cuando la cantidad de elementos de la colección que pase a removeAll
sea mayor o igual que la cantidad de elementos del conjunto.
La inconsistencia es causada por una pequeña optimización: si observa la implementación de removeAll
, que se hereda de AbstractSet
, la optimización es la siguiente:
public boolean removeAll(Collection<?> c) { boolean modified = false; if (size() > c.size()) { for (Iterator<?> i = c.iterator(); i.hasNext(); ) modified |= remove(i.next()); } else { for (Iterator<?> i = iterator(); i.hasNext(); ) { if (c.contains(i.next())) { i.remove(); modified = true; } } } return modified; }
puede ver que el comportamiento es diferente cuando c
tiene menos elementos que este conjunto (rama superior) frente a cuando tiene tantos o más elementos (rama inferior).
La rama superior usa el comparador asociado con este conjunto, mientras que la rama inferior usa equals
para la comparación c.contains(i.next())
- ¡todo con el mismo método!
Puede demostrar este comportamiento agregando algunos elementos adicionales al conjunto de árboles original:
s.addAll(Arrays.asList("x", "z", "a", "b"));
Ahora la salida para ambos casos de prueba se vuelve idéntica, porque remove(i.next())
utiliza el comparador del conjunto.
La razón es porque el comparador String.CASE_INSENSITIVE_ORDER
que usa no es consistente con los iguales.
Según lo declarado por TreeSet
:
Tenga en cuenta que el orden mantenido por un conjunto (ya sea que se proporcione o no un comparador explícito) debe ser consistente con los iguales si se va a implementar correctamente la interfaz del conjunto.
Consistencia con iguales según lo establecido por Comparable
:
Se dice que el ordenamiento natural para una clase C es consistente con equals si y solo si e1.compareTo(e2) == 0 tiene el mismo valor booleano que e1.equals(e2) para cada e1 y e2 de la clase C.
Y como ejemplo para el comparador que no distingue entre mayúsculas y minúsculas que utiliza:
"a".compareTo("A") == 0 => true
tiempo
"a".equals("A") => false