¿Por qué las colecciones de Java no eliminan los métodos genéricos?

144

¿Por qué Collection.remove (Object o) no es genérico?

Parece que Collection<E>podría haberboolean remove(E o);

Luego, cuando accidentalmente intente eliminar (por ejemplo) en Set<String>lugar de cada Cadena individual de una Collection<String>, sería un error de tiempo de compilación en lugar de un problema de depuración posterior.

Chris Mazzola
fuente
13
Esto realmente puede morderte cuando lo combinas con autoboxing. Si intenta eliminar algo de una Lista y le pasa un Entero en lugar de un int, llama al método remove (Object).
ScArcher2
2
Pregunta similar con respecto al mapa: stackoverflow.com/questions/857420/…
AlikElzin-kilaka

Respuestas:

73

Josh Bloch y Bill Pugh se refieren a este tema en Java Puzzlers IV: The Phantom Reference Menace, Attack of the Clone y Revenge of The Shift .

Josh Bloch dice (6:41) que intentaron generar el método get de Map, remove method y algunos otros, pero "simplemente no funcionó".

Hay demasiados programas razonables que no podrían generarse si solo permite el tipo genérico de la colección como tipo de parámetro. El ejemplo dado por él es una intersección de a Listde Numbersy a Listde Longs.

dmeister
fuente
66
Por qué add () puede tomar un parámetro escrito pero remove () no puede estar un poco más allá de mi comprensión. Josh Bloch sería la referencia definitiva para las preguntas de Colecciones. Puede ser todo lo que obtengo sin intentar hacer una implementación de colección similar y ver por mí mismo. :( Gracias.
Chris Mazzola
2
Chris: lee el PDF del Tutorial de Java Generics, explicará por qué.
JeeBee
42
En realidad, es muy simple! Si add () tomara un objeto incorrecto, rompería la colección. ¡Contendría cosas que no debería! Ese no es el caso de remove (), o contiene ().
Kevin Bourrillion el
12
Por cierto, esa regla básica, que usa parámetros de tipo para evitar daños reales a la colección solamente, se sigue de manera absolutamente consistente en toda la biblioteca.
Kevin Bourrillion el
3
@KevinBourrillion: He estado trabajando con genéricos durante años (tanto en Java como en C #) sin siquiera darme cuenta de que la regla de "daño real" existe ... pero ahora que lo he visto directamente tiene un 100% de sentido. Gracias por el pescado! Excepto que ahora me siento obligado a volver y mirar mis implementaciones, para ver si algunos métodos pueden y, por lo tanto, deben ser desgenéricos. Suspiro.
corlettk
74

remove()( Maptanto como en Collection) no es genérico porque debería poder pasar cualquier tipo de objeto a remove(). El objeto eliminado no tiene que ser del mismo tipo que el objeto al que se pasa remove(); solo requiere que sean iguales. De la especificación de remove(), remove(o)elimina el objeto etal como (o==null ? e==null : o.equals(e))es true. Tenga en cuenta que no hay nada que requiera oy esea ​​del mismo tipo. Esto se deduce del hecho de que el equals()método toma un Objectparámetro as, no solo el mismo tipo que el objeto.

Aunque, puede ser comúnmente cierto que muchas clases se han equals()definido para que sus objetos solo puedan ser iguales a los objetos de su propia clase, ese no es siempre el caso. Por ejemplo, la especificación de List.equals()dice que dos objetos List son iguales si ambas son Listas y tienen el mismo contenido, incluso si son implementaciones diferentes de List. Volviendo al ejemplo en esta pregunta, es posible tener un Map<ArrayList, Something>y para que llame remove()con un LinkedListargumento como, y debería eliminar la clave, que es una lista con el mismo contenido. Esto no sería posible si remove()fuera genérico y restringiera su tipo de argumento.

newacct
fuente
1
Pero si tuviera que definir el Mapa como Mapa <Lista, Algo> (en lugar de ArrayList), habría sido posible eliminarlo usando un LinkedList. Creo que esta respuesta es incorrecta.
AlikElzin-kilaka
3
La respuesta parece ser correcta, pero incompleta. Solo se presta a preguntar por qué diablos no genéricos también el equals()método. Podría ver más beneficios para escribir la seguridad en lugar de este enfoque "libertario". Creo que la mayoría de los casos con implementación actual son para errores que entran en nuestro código en lugar de alegría con esta flexibilidad que remove()brinda el método.
Kellogs
2
@kellogs: ¿Qué quieres decir con "generizar el equals()método"?
newacct
55
@MattBall: "donde T es la clase declarante" Pero no existe tal sintaxis en Java. Tdebe declararse como un parámetro de tipo en la clase y Objectno tiene ningún parámetro de tipo. No hay forma de tener un tipo que se refiera a "la clase declarante".
newacct
3
Creo que Kellogs está diciendo lo que si la igualdad fuera una interfaz genérica Equality<T>con equals(T other). Entonces podrías tener remove(Equality<T> o)y oes solo un objeto que se puede comparar con otro T.
Weston
11

Porque si su parámetro de tipo es un comodín, no puede usar un método genérico de eliminación.

Creo recordar haber encontrado esta pregunta con el método get (Object) de Map. El método get en este caso no es genérico, aunque debería esperar razonablemente que se le pase un objeto del mismo tipo que el primer parámetro de tipo. Me di cuenta de que si estás pasando Mapas con un comodín como primer parámetro de tipo, entonces no hay forma de sacar un elemento del Mapa con ese método, si ese argumento es genérico. Los argumentos comodín no pueden satisfacerse realmente, porque el compilador no puede garantizar que el tipo sea correcto. Especulo que la razón por la que agregar es genérico es que se espera que garantices que el tipo es correcto antes de agregarlo a la colección. Sin embargo, al eliminar un objeto, si el tipo es incorrecto, no coincidirá con nada de todos modos.

Probablemente no lo expliqué muy bien, pero me parece bastante lógico.

Bob Gettys
fuente
1
¿Podrías dar más detalles sobre esto?
Thomas Owens
6

Además de las otras respuestas, hay otra razón por la cual el método debe aceptar un Object, que es predicados. Considere la siguiente muestra:

class Person {
    public String name;
    // override equals()
}
class Employee extends Person {
    public String company;
    // override equals()
}
class Developer extends Employee {
    public int yearsOfExperience;
    // override equals()
}

class Test {
    public static void main(String[] args) {
        Collection<? extends Person> people = new ArrayList<Employee>();
        // ...

        // to remove the first employee with a specific name:
        people.remove(new Person(someName1));

        // to remove the first developer that matches some criteria:
        people.remove(new Developer(someName2, someCompany, 10));

        // to remove the first employee who is either
        // a developer or an employee of someCompany:
        people.remove(new Object() {
            public boolean equals(Object employee) {
                return employee instanceof Developer
                    || ((Employee) employee).company.equals(someCompany);
        }});
    }
}

El punto es que el objeto que se pasa al removemétodo es responsable de definir el equalsmétodo. Construir predicados se vuelve muy simple de esta manera.

Hosam Aly
fuente
¿Inversor? (Relleno relleno relleno)
Matt R
3
La lista se implementa como yourObject.equals(developer), como se documenta en la API de Colecciones: java.sun.com/javase/6/docs/api/java/util/…
Hosam Aly
13
Esto me parece un abuso
RAYO
77
Es abuso ya que su objeto predicado rompe el contrato del equalsmétodo, es decir, la simetría. El método remove solo está sujeto a su especificación siempre que sus objetos cumplan con la especificación equals / hashCode, por lo que cualquier implementación sería libre de hacer la comparación al revés. Además, su objeto predicado no implementa el .hashCode()método (no puede implementarse de manera consistente a iguales), por lo tanto, la llamada a eliminar nunca funcionará en una colección basada en Hash (como HashSet o HashMap.keys ()). Que funcione con ArrayList es pura suerte.
Paŭlo Ebermann
3
(No estoy discutiendo la pregunta de tipo genérico, esto se respondió antes, solo su uso de iguales para predicados aquí). Por supuesto, HashMap y HashSet están verificando el código hash, y TreeSet / Map está usando el orden de los elementos . Aún así, se implementan completamente Collection.remove, sin romper su contrato (si el pedido es consistente con iguales). Y una variada ArrayList (o AbstractCollection, creo) con la llamada igual activada aún implementaría correctamente el contrato: es su culpa si no funciona como se esperaba, ya que está rompiendo el equalscontrato.
Paŭlo Ebermann
5

Suponga que uno tiene una colección de Cat, y algunas referencias a objetos de tipos Animal, Cat, SiameseCat, y Dog. Preguntar a la colección si contiene el objeto al que hace referencia la referencia Cato SiameseCatparece razonable. Preguntar si contiene el objeto al que hace Animalreferencia la referencia puede parecer dudoso, pero sigue siendo perfectamente razonable. El objeto en cuestión podría, después de todo, ser un Caty podría aparecer en la colección.

Además, incluso si el objeto Catno es un objeto , no hay problema para decir si aparece en la colección, simplemente responda "no, no lo hace". Una colección de "tipo de búsqueda" de algún tipo debería poder aceptar de manera significativa la referencia de cualquier supertipo y determinar si el objeto existe dentro de la colección. Si la referencia de objeto pasada es de un tipo no relacionado, no hay forma de que la colección pueda contenerla, por lo que la consulta no es significativa en algún sentido (siempre responderá "no"). No obstante, dado que no hay forma de restringir los parámetros a subtipos o supertipos, lo más práctico es simplemente aceptar cualquier tipo y responder "no" a cualquier objeto cuyo tipo no esté relacionado con el de la colección.

Super gato
fuente
1
"Si la referencia de objeto pasada es de un tipo no relacionado, no hay forma de que la colección pueda contenerla" Incorrecto. Solo tiene que contener algo igual a él; y los objetos de diferentes clases pueden ser iguales.
newacct
"puede parecer dudoso, pero sigue siendo perfectamente razonable" ¿Lo es? Considere un mundo en el que un objeto de un tipo no siempre se puede verificar por igualdad con un objeto de otro tipo, porque el tipo con el que puede ser igual está parametrizado (de forma similar a cómo Comparablese parametriza para los tipos con los que se puede comparar). Entonces no sería razonable permitir que las personas pasen algo de un tipo no relacionado.
newacct
@newacct: Hay una diferencia fundamental entre la comparación de magnitud y la comparación de igualdad: objetos dados Ay Bde un tipo, Xy Yde otro, de modo que A> By X> Y. Ya sea A> Yy Y< A, o X> By B< X. Esas relaciones solo pueden existir si las comparaciones de magnitud conocen ambos tipos. Por el contrario, el método de comparación de igualdad de un objeto puede simplemente declararse desigual a cualquier cosa de cualquier otro tipo, sin tener que saber nada sobre el otro tipo en cuestión. Un objeto de tipo Catpuede no tener idea de si es ...
supercat
... "mayor que" o "menor que" un objeto de tipo FordMustang, pero no debería tener dificultad para decir si es igual a dicho objeto (la respuesta, obviamente, es "no").
supercat
4

Siempre pensé que esto se debía a que remove () no tiene ninguna razón para importarle qué tipo de objeto le das. Sin embargo, es bastante fácil verificar si ese objeto es uno de los que contiene la Colección, ya que puede llamar a equals () en cualquier cosa. Es necesario verificar el tipo en add () para asegurarse de que solo contenga objetos de ese tipo.

ColinD
fuente
0

Fue un compromiso. Ambos enfoques tienen su ventaja:

  • remove(Object o)
    • Es más flexible. Por ejemplo, permite recorrer una lista de números y eliminarlos de una lista de longs.
    • el código que usa esta flexibilidad puede ser más fácilmente generado
  • remove(E e) brinda más seguridad de tipografía a lo que la mayoría de los programas quieren hacer al detectar errores sutiles en el momento de la compilación, como tratar de eliminar por error un número entero de una lista de cortos.

La compatibilidad con versiones anteriores siempre fue un objetivo importante al desarrollar la API de Java, por lo tanto, se eligió eliminar (Objeto o) porque facilitaba la generación de código existente. Si la compatibilidad con versiones anteriores NO hubiera sido un problema, supongo que los diseñadores habrían elegido eliminar (E e).

Stefan Feuerhahn
fuente
-1

Eliminar no es un método genérico, por lo que el código existente que utiliza una colección no genérica seguirá compilando y seguirá teniendo el mismo comportamiento.

Consulte http://www.ibm.com/developerworks/java/library/j-jtp01255.html para obtener más detalles.

Editar: Un comentarista pregunta por qué el método de agregar es genérico. [... eliminó mi explicación ...] El segundo comentarista respondió la pregunta de firebird84 mucho mejor que yo.

Jeff C
fuente
2
Entonces, ¿por qué el método add es genérico?
Bob Gettys
@ firebird84 remove (Object) ignora los objetos del tipo incorrecto, pero remove (E) causaría un error de compilación. Eso cambiaría el comportamiento.
noah
: encogimiento de hombros: - el comportamiento en tiempo de ejecución no cambia; error de compilación no es comportamiento de tiempo de ejecución . El "comportamiento" del método add cambia de esta manera.
Jason S
-2

Otra razón es por las interfaces. Aquí hay un ejemplo para mostrarlo:

public interface A {}

public interface B {}

public class MyClass implements A, B {}

public static void main(String[] args) {
   Collection<A> collection = new ArrayList<>();
   MyClass item = new MyClass();
   collection.add(item);  // works fine
   B b = item; // valid
   collection.remove(b); /* It works because the remove method accepts an Object. If it was generic, this would not work */
}
Patricio
fuente
Estás mostrando algo de lo que puedes salirte con la tuya porque remove()no es covariante. La pregunta, sin embargo, es si esto debería permitirse. ArrayList#remove()funciona a modo de igualdad de valor, no de igualdad de referencia. ¿Por qué esperarías que a Bfuera igual a an A? En su ejemplo, puede ser, pero es una expectativa extraña. Prefiero verte proporcionar una MyClassdiscusión aquí.
seh
-3

Porque rompería el código existente (anterior a Java5). p.ej,

Set stringSet = new HashSet();
// do some stuff...
Object o = "foobar";
stringSet.remove(o);

Ahora puede decir que el código anterior es incorrecto, pero suponga que o proviene de un conjunto heterogéneo de objetos (es decir, que contiene cadenas, números, objetos, etc.). Desea eliminar todas las coincidencias, lo cual era legal porque eliminar simplemente ignoraría las no cadenas porque no eran iguales. Pero si lo haces eliminar (String o), eso ya no funciona.

Noé
fuente
44
Si instancia una Lista <String> esperaría poder llamar a List.remove (someString); Si necesito admitir compatibilidad con versiones anteriores, usaría una Lista sin procesar - Lista <?> Y luego puedo llamar a list.remove (someObject), ¿no?
Chris Mazzola
55
Si reemplaza "eliminar" a "añadir" a continuación, que el código es tan roto por lo que en realidad se hizo en Java 5.
DJClayworth