¿Por qué la eliminación de un TreeSet con un comparador personalizado no elimina un conjunto más grande de elementos?

22

Usando Java 8 y Java 11, considere lo siguiente TreeSetcon un String::compareToIgnoreCasecomparador:

final Set<String> languages = new TreeSet<>(String::compareToIgnoreCase);
languages.add("java");
languages.add("c++");
languages.add("python");

System.out.println(languages);                 // [c++, java, python]

Cuando intento eliminar los elementos exactos presentes en el TreeSet, funciona: se eliminan todos los especificados:

languages.removeAll(Arrays.asList("PYTHON", "C++"));

System.out.println(languages);                 // [java]

Sin embargo, si intento eliminar en su lugar más de lo que está presente en la TreeSetllamada, la llamada no elimina nada (esta no es una llamada posterior, sino que se llama en lugar del fragmento anterior):

languages.removeAll(Arrays.asList("PYTHON", "C++", "LISP"));

System.out.println(languages);                 // [c++, java, python]

¿Qué estoy haciendo mal? ¿Por qué se comporta de esta manera?

Editar: String::compareToIgnoreCasees un comparador válido:

(l, r) -> l.compareToIgnoreCase(r)
Nikolas
fuente
55
Entrada de error relacionada: bugs.openjdk.java.net/browse/JDK-8180409 (TreeSet removeTodo comportamiento inconsistente con String.CASE_INSENSITIVE_ORDER)
Progman
Un Q& A estrechamente relacionado .
Naman

Respuestas:

22

Aquí está el javadoc de removeAll () :

Esta implementación determina cuál es el más pequeño de este conjunto y la colección especificada, invocando el método de tamaño en cada uno. Si este conjunto tiene menos elementos, entonces la implementación itera sobre este conjunto, verificando cada elemento devuelto por el iterador para ver si está contenido en la colección especificada. Si está contenido, se elimina de este conjunto con el método remove del iterador. Si la colección especificada tiene menos elementos, entonces la implementación itera sobre la colección especificada, eliminando de este conjunto cada elemento devuelto por el iterador, utilizando el método remove de este conjunto.

En tu segundo experimento, estás en el primer caso del javadoc. Por lo tanto, itera sobre "java", "c ++", etc. y comprueba si están contenidos en el conjunto devuelto por Set.of("PYTHON", "C++"). No lo son, así que no se eliminan. Use otro TreeSet usando el mismo comparador como argumento, y debería funcionar bien. Usar dos implementaciones de Set diferentes, una usando equals()y la otra usando un comparador, es algo realmente peligroso.

Tenga en cuenta que hay un error abierto sobre esto: [JDK-8180409] TreeSet removeTodo comportamiento inconsistente con String.CASE_INSENSITIVE_ORDER .

JB Nizet
fuente
¿Quieres decir que cuando ambos conjuntos tendrían las mismas características, funciona? final Set<String> subLanguages = new TreeSet<>(String::compareToIgnoreCase); subLanguages.addAll(Arrays.asList("PYTHON", "C++", "LISP")); languages.removeAll(subLanguages);
Nikolas
1
Estás en el caso "Si este conjunto tiene menos elementos", descrito por el javadoc. El otro caso es "Si la colección especificada tiene menos elementos".
JB Nizet
8
Esta respuesta es correcta, pero es un comportamiento muy poco intuitivo. Se siente como una falla en el diseño de TreeSet.
Boann
Estoy de acuerdo, pero no puedo hacer nada al respecto.
JB Nizet
44
Es a la vez: es un comportamiento muy poco intuitivo que está correctamente documentado, pero, al ser poco intuitivo y engañoso, también es un error de diseño que algún día podría solucionarse.
JB Nizet