Java: ¿comodines acotados o parámetro de tipo acotado?

82

Recientemente, leí este artículo: http://download.oracle.com/javase/tutorial/extra/generics/wildcards.html

Mi pregunta es, en lugar de crear un método como este:

public void drawAll(List<? extends Shape> shapes){
    for (Shape s: shapes) {
        s.draw(this);
    }
}

Puedo crear un método como este y funciona bien:

public <T extends Shape> void drawAll(List<T> shapes){
    for (Shape s: shapes) {
        s.draw(this);
    }
}

¿Qué forma debo usar? ¿Es útil el comodín en este caso?

Tony Le
fuente
? es una notación abreviada; El compilador internamente lo reemplaza con un parámetro de tipo de todos modos; cuando hay un error del compilador, verá el parámetro de tipo sustituto, en lugar del?
irrefutable
9
corrección: mi comentario anterior es INCORRECTO. El comodín es más sofisticado de lo que pensaba.
irrefutable
@ irreputable aunque es más complicado, su punto de reemplazarlo con un parámetro de tipo es completamente válido; es solo uno que no puedes declarar.
Eugene
si miramos sólo estos dos métodos, ya que son - no hay diferencia y sólo es cuestión de estilo; de lo contrario (si agrega algunos parámetros más genéricos), las cosas cambiarán
Eugene

Respuestas:

133

Depende de lo que necesites hacer. Debe utilizar el parámetro de tipo acotado si desea hacer algo como esto:

public <T extends Shape> void addIfPretty(List<T> shapes, T shape) {
    if (shape.isPretty()) {
       shapes.add(shape);
    }
}

Aquí tenemos un List<T> shapesy un T shape, por lo tanto, podemos hacerlo con seguridad shapes.add(shape). Si fue declarado List<? extends Shape>, NO puede addhacerlo con seguridad (porque puede tener un List<Square>y un Circle).

Entonces, al darle un nombre a un parámetro de tipo acotado, tenemos la opción de usarlo en otro lugar de nuestro método genérico. Esta información no siempre es necesaria, por supuesto, por lo que si no necesita saber mucho sobre el tipo (por ejemplo, su drawAll), entonces basta con utilizar comodines.

Incluso si no se está refiriendo al parámetro de tipo delimitado nuevamente, se requiere un parámetro de tipo delimitado si tiene varios límites. Aquí hay una cita de las preguntas frecuentes sobre genéricos de Java de Angelika Langer

¿Cuál es la diferencia entre un enlace comodín y un enlace de parámetro de tipo?

Un comodín solo puede tener un límite, mientras que un parámetro de tipo puede tener varios límites. Un comodín puede tener un límite inferior o superior, mientras que no existe un límite inferior para un parámetro de tipo.

Los límites comodín y los límites de los parámetros de tipo a menudo se confunden, porque ambos se denominan límites y tienen en parte una sintaxis similar. […]

Sintaxis :

  type parameter bound     T extends Class & Interface1 & … & InterfaceN

  wildcard bound  
      upper bound          ? extends SuperType
      lower bound          ? super   SubType

Un comodín solo puede tener un límite, ya sea un límite inferior o superior. No se permite una lista de límites comodín.

Un parámetro de tipo, en contraposición, puede tener varios límites, pero no existe un límite inferior para un parámetro de tipo.

Citas de Effective Java 2nd Edition, artículo 28: Utilice comodines delimitados para aumentar la flexibilidad de la API :

Para una máxima flexibilidad, utilice tipos de comodines en los parámetros de entrada que representan a los productores o consumidores. […] PECS significa productor extends, consumidor, super[…]

No utilice tipos de comodines como tipos de devolución . En lugar de proporcionar flexibilidad adicional a sus usuarios, los obligaría a utilizar tipos de comodines en el código del cliente. Si se utilizan correctamente, los tipos de comodines son casi invisibles para los usuarios de una clase. Hacen que los métodos acepten los parámetros que deberían aceptar y rechacen los que deberían rechazar. Si el usuario de la clase tiene que pensar en tipos de comodines, probablemente haya algo mal con la API de la clase .

Aplicando el principio PECS, ahora podemos volver a nuestro addIfPrettyejemplo y hacerlo más flexible escribiendo lo siguiente:

public <T extends Shape> void addIfPretty(List<? super T> list, T shape) { … }

Ahora podemos addIfPretty, decir, a Circle, a a List<Object>. Obviamente, esto es seguro para los tipos y, sin embargo, nuestra declaración original no era lo suficientemente flexible como para permitirlo.

Preguntas relacionadas


Resumen

  • Utilice parámetros / comodines de tipo acotado, aumentan la flexibilidad de su API
  • Si el tipo requiere varios parámetros, no tiene más remedio que utilizar el parámetro de tipo acotado
  • si el tipo requiere un límite inferior, no tiene más remedio que utilizar un comodín acotado
  • Los "productores" tienen límites superiores, los "consumidores" tienen límites inferiores
  • No utilice comodines en tipos de devolución
poligenelubricantes
fuente
Muchas gracias, su explicación es muy clara e instructiva
Tony Le
1
@Tony: El libro también tiene una breve discusión sobre cómo elegir entre comodín o parámetro de tipo cuando no hay límite. Básicamente, si el parámetro de tipo aparece solo una vez en una declaración de método, use comodines. Vea también el reverse(List<?>)ejemplo de JLS java.sun.com/docs/books/jls/third_edition/html/…
polygenelubricants
Recibo UnsupportedOperationException para el siguiente código, <code> public static <T amplía el número> void add (List <? Super T> list, T num) {list.add (num); } </code>
Samra
1
Además, no puede pasar ?como un parámetro de método doWork(? type)porque entonces no puede usar ese parámetro en el método. Necesita usar el parámetro de tipo.
Tomasz Mularczyk
1
@VivekVardhan no puede crear un método de este tipo utilizando comodines, ese es el punto. Al decidir cuál usar, esta es una de las mejores fuentes para explicarlo.
Eugene
6

En su ejemplo, realmente no necesita usar T, ya que no usa ese tipo en ningún otro lugar.

Pero si hicieras algo como:

public <T extends Shape> T drawFirstAndReturnIt(List<T> shapes){
    T s = shapes.get(0);
    s.draw(this);
    return s;
}

o como dijo polygenlubricants, si desea hacer coincidir el parámetro de tipo en la lista con otro parámetro de tipo:

public <T extends Shape> void mergeThenDraw(List<T> shapes1, List<T> shapes2) {
    List<T> mergedList = new ArrayList<T>();
    mergedList.addAll(shapes1);
    mergedList.addAll(shapes2);
    for (Shape s: mergedList) {
        s.draw(this);
    }
}

En el primer ejemplo, obtiene un poco más de seguridad de tipos y luego devuelve solo Shape, ya que luego puede pasar el resultado a una función que puede tomar un hijo de Shape. Por ejemplo, puede pasar un List<Square>a mi método y luego pasar el Cuadrado resultante a un método que solo toma Cuadrados. Si usó '?' tendría que convertir la forma resultante en cuadrado, lo que no sería seguro para los tipos.

En el segundo ejemplo, se asegura de que ambas listas tengan el mismo parámetro de tipo (lo que no puede hacer con '?', Ya que cada '?' Es diferente), de modo que pueda crear una lista que contenga todos los elementos de ambos .

Andrei Fierbinteanu
fuente
Creo que Shape s = ...debería ser T s = ..., de lo contrario return s;no debería compilar.
polygenelubricants
1

Considere el siguiente ejemplo de The Java Programming por James Gosling 4ta edición a continuación donde queremos fusionar 2 SinglyLinkQueue:

public static <T1, T2 extends T1> void merge(SinglyLinkQueue<T1> d, SinglyLinkQueue<T2> s){
    // merge s element into d
}

public static <T> void merge(SinglyLinkQueue<T> d, SinglyLinkQueue<? extends T> s){
        // merge s element into d
}

Ambos métodos anteriores tienen la misma funcionalidad. Entonces, ¿cuál es preferible? La respuesta es la segunda. En palabras del propio autor:

"La regla general es usar comodines cuando sea posible porque el código con comodines es generalmente más legible que el código con varios parámetros de tipo. Cuando decida si necesita una variable de tipo, pregúntese si esa variable de tipo se usa para relacionar dos o más parámetros. o relacionar un tipo de parámetro con el tipo de retorno. Si la respuesta es no, un comodín debería ser suficiente ".

Nota: En el libro, solo se proporciona el segundo método y el nombre del parámetro de tipo es S en lugar de 'T'. El primer método no está en el libro.

chammu
fuente
1

Por lo que tengo entendido, el comodín permite un código más conciso en situaciones en las que no se requiere un parámetro de tipo (por ejemplo, porque se hace referencia a él en varios lugares o porque se requieren múltiples límites como se detalla en otras respuestas).

En el enlace indicas que leo (bajo "Métodos genéricos") las siguientes declaraciones que apuntan en esta dirección:

Los métodos genéricos permiten utilizar parámetros de tipo para expresar dependencias entre los tipos de uno o más argumentos de un método y / o su tipo de retorno. Si no existe tal dependencia, no se debe utilizar un método genérico.

[...]

El uso de comodines es más claro y conciso que declarar parámetros de tipo explícitos y, por lo tanto, debe preferirse siempre que sea posible.

[...]

Los comodines también tienen la ventaja de que se pueden usar fuera de las firmas de métodos, como los tipos de campos, variables locales y matrices.

Martín Maletinsky
fuente
0

La segunda forma es un poco más detallada, pero te permite hacer referencia a Tella:

for (T shape : shapes) {
    ...
}

Esa es la única diferencia, hasta donde tengo entendido.

Nikita Rybak
fuente