¿Cuándo usar métodos genéricos y cuándo usar comodines?

122

Estoy leyendo sobre métodos genéricos de OracleDocGenericMethod . Estoy bastante confundido acerca de la comparación cuando dice cuándo usar comodines y cuándo usar métodos genéricos. Citando del documento.

interface Collection<E> {
    public boolean containsAll(Collection<?> c);
    public boolean addAll(Collection<? extends E> c);
}

Podríamos haber utilizado métodos genéricos aquí en su lugar:

interface Collection<E> {
    public <T> boolean containsAll(Collection<T> c);
    public <T extends E> boolean addAll(Collection<T> c);
    // Hey, type variables can have bounds too!
}

[…] Esto nos dice que el argumento de tipo se está utilizando para polimorfismo; su único efecto es permitir el uso de una variedad de tipos de argumentos reales en diferentes sitios de invocación. Si ese es el caso, se deben usar comodines. Los comodines están diseñados para admitir subtipos flexibles, que es lo que estamos tratando de expresar aquí.

¿No creemos que el comodín como (Collection<? extends E> c);también apoya una especie de polimorfismo? Entonces, ¿por qué el uso de métodos genéricos no se considera bueno en esto?

Continuando adelante, dice,

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.

¿Qué significa esto?

Han presentado el ejemplo

class Collections {
    public static <T> void copy(List<T> dest, List<? extends T> src) {
    ...
}

[…]

Podríamos haber escrito la firma para este método de otra manera, sin usar comodines en absoluto:

class Collections {
    public static <T, S extends T> void copy(List<T> dest, List<S> src) {
    ...
}

¿El documento desalienta la segunda declaración y promueve el uso de la primera sintaxis? ¿Cuál es la diferencia entre la primera y la segunda declaración? ¿Ambos parecen estar haciendo lo mismo?

¿Alguien puede poner luz en esta área?

benz
fuente

Respuestas:

173

Hay ciertos lugares donde los comodines y los parámetros de tipo hacen lo mismo. Pero también hay ciertos lugares donde tienes que usar parámetros de tipo.

  1. Si desea aplicar alguna relación en los diferentes tipos de argumentos de método, no puede hacerlo con comodines, debe usar parámetros de tipo.

Tomando su método como ejemplo, suponga que desea asegurarse de que la lista srcy destpasada al copy()método debe ser del mismo tipo parametrizado, puede hacerlo con parámetros de tipo así:

public static <T extends Number> void copy(List<T> dest, List<T> src)

Aquí, se asegura que ambos desty srctienen el mismo tipo parametrizado para List. Por lo tanto, es seguro copiar elementos de srca dest.

Pero, si continúa cambiando el método para usar comodines:

public static void copy(List<? extends Number> dest, List<? extends Number> src)

no funcionará como se esperaba. En el segundo caso, puede pasar List<Integer>y List<Float>como desty src. Por lo tanto, mover elementos de srca destya no sería seguro para tipos. Si no necesita ese tipo de relación, entonces es libre de no usar parámetros de tipo en absoluto.

Algunas otras diferencias entre el uso de comodines y parámetros de tipo son:

  • Si solo tiene un argumento de tipo parametrizado, puede usar comodines, aunque el parámetro de tipo también funcionará.
  • Los parámetros de tipo admiten varios límites, los comodines no.
  • Los comodines admiten límites superiores e inferiores, los parámetros de tipo solo admiten límites superiores. Entonces, si desea definir un método que toma un Listtipo de Integero es una superclase, puede hacer:

    public void print(List<? super Integer> list)  // OK

    pero no puedes usar el parámetro de tipo:

     public <T super Integer> void print(List<T> list)  // Won't compile

Referencias:

Rohit Jain
fuente
1
Esta es una respuesta extraña. No explica por qué necesita usarlo ?en absoluto. Puede reescribirlo como `public static <T1 extiende Número, T2 extiende Número> copia vacía (Lista <T1> dest, Lista <T2> src) y en este caso se vuelve obvio lo que está pasando.
kan
@kan. Bueno, ese es el verdadero problema. Puede utilizar el parámetro de tipo para aplicar el mismo tipo, pero no puede hacerlo con comodines. Usar dos tipos diferentes para el parámetro de tipo es algo diferente.
Rohit Jain
1
@benz. No puede definir límites inferiores en un Listparámetro de tipo de uso. List<T super Integer>no es válido y no se compilará.
Rohit Jain
2
@benz. De nada :) Le sugiero que visite el enlace que publiqué al final. Ese es el mejor recurso sobre genéricos que obtendría.
Rohit Jain
3
@ jorgen.ringen <T extends X & Y>-> límites múltiples.
Rohit Jain
12

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
voté a favor de la cita de un libro, es directo y conciso
Kurapika
9

En su primera pregunta: Significa que si existe una relación entre el tipo de parámetro y el tipo de retorno del método, utilice un archivo.

Por ejemplo:

public <T> T giveMeMaximum(Collection<T> items);
public <T> Collection<T> applyFilter(Collection<T> items);

Aquí estás extrayendo algunos de los T siguiendo ciertos criterios. Si T es, Longsus métodos volverán Longy Collection<Long>; el tipo de retorno real depende del tipo de parámetro, por lo que es útil y aconsejable utilizar tipos genéricos.

Cuando este no es el caso, puede utilizar tipos de comodines:

public int count(Collection<?> items);
public boolean containsDuplicate(Collection<?> items);

En estos dos ejemplos, cualquiera que sea el tipo de elementos de las colecciones, los tipos de devolución serán inty boolean.

En tus ejemplos:

interface Collection<E> {
    public boolean containsAll(Collection<?> c);
    public boolean addAll(Collection<? extends E> c);
}

esas dos funciones devolverán un booleano cualquiera sea el tipo de elementos de las colecciones. En el segundo caso, se limita a instancias de una subclase de E.

Segunda pregunta:

class Collections {
    public static <T> void copy(List<T> dest, List<? extends T> src) {
    ...
}

Este primer código le permite pasar un heterogéneo List<? extends T> srccomo parámetro. Esta lista puede contener múltiples elementos de diferentes clases siempre que todos extiendan la clase base T.

si tuvieras:

interface Fruit{}

y

class Apple implements Fruit{}
class Pear implements Fruit{}
class Tomato implements Fruit{}

Podrías hacerlo

List<? extends Fruit> basket = new ArrayList<? extends Fruit>();
basket.add(new Apple());
basket.add(new Pear());
basket.add(new Tomato());
List<Fruit> fridge = new ArrayList<Fruit>(); 

Collections.copy(fridge, basket);// works 

Por otra parte

class Collections {
    public static <T, S extends T> void copy(List<T> dest, List<S> src) {
    ...
}

Restringir List<S> srcpara ser de una clase particular S que es una subclase de T. La lista sólo puede contener elementos de una clase (en este caso S) y ninguna otra clase, incluso si implementan T también. No podría usar mi ejemplo anterior, pero podría hacer:

List<Apple> basket = new ArrayList<Apple>();
basket.add(new Apple());
basket.add(new Apple());
basket.add(new Apple());
List<Fruit> fridge = new ArrayList<Fruit>();

Collections.copy(fridge, basket); /* works since the basket is defined as a List of apples and not a list of some fruits. */
le-doude
fuente
1
List<? extends Fruit> basket = new ArrayList<? extends Fruit>();No es una sintaxis válida. Tienes que crear una instancia de ArrayList sin límites.
Arnold Pistorius
No se puede agregar una manzana a la canasta en el ejemplo anterior, ya que la canasta podría ser una lista de peras. Ejemplo incorrecto AFAIK. Y no se compila también.
Khanna111
1
@ArnoldPistorius Eso me confundió. Revisé la documentación de la API de ArrayList y tiene un constructor firmado ArrayList(Collection<? extends E> c). ¿Puede explicarme por qué ha dicho eso?
Kurapika
@Kurapika ¿Podría ser que estaba usando una versión antigua de Java? El comentario fue publicado hace casi 3 años.
Arnold Pistorius
2

El método comodín también es genérico; puede llamarlo con algún rango de tipos.

La <T>sintaxis define un nombre de variable de tipo. Si una variable de tipo tiene algún uso (por ejemplo, en la implementación de un método o como una restricción para otro tipo), entonces tiene sentido nombrarla; de lo contrario, podría usarla ?como variable anónima. Entonces, parece solo un atajo.

Además, la ?sintaxis no se puede evitar cuando declaras un campo:

class NumberContainer
{
 Set<? extends Number> numbers;
}
kan
fuente
3
¿No se supone que esto es un comentario?
Buhake Sindi
@BuhakeSindi Lo siento, ¿qué no está claro? ¿Por qué -1? Creo que responde a la pregunta.
kan
2

Intentaré responder a su pregunta, una por una.

¿No creemos que el comodín como (Collection<? extends E> c);también apoya una especie de polimorfismo?

No. La razón es que el comodín acotado no tiene un tipo de parámetro definido. Es un desconocido. Todo lo que "sabe" es que la "contención" es de un tipo E(cualquiera que sea su definición). Por lo tanto, no puede verificar y justificar si el valor proporcionado coincide con el tipo acotado.

Entonces, no es sensato tener comportamientos polimórficos en comodines.

¿El documento desalienta la segunda declaración y promueve el uso de la primera sintaxis? ¿Cuál es la diferencia entre la primera y la segunda declaración? ¿Ambos parecen estar haciendo lo mismo?

La primera opción es mejor en este caso, ya Tque siempre está limitada y sourcedefinitivamente tendrá valores (de incógnitas) que subclases T.

Entonces, suponga que desea copiar toda la lista de números, la primera opción será

Collections.copy(List<Number> dest, List<? extends Number> src);

src, Esencialmente, puede aceptar List<Double>, List<Float>, etc., ya que hay un límite superior para el tipo parametrizado encuentra en dest.

La segunda opción te obligará a vincular Stodos los tipos que quieras copiar, así

//For double 
Collections.copy(List<Number> dest, List<Double> src); //Double extends Number.

//For int
Collections.copy(List<Number> dest, List<Integer> src); //Integer extends Number.

Como Ses un tipo parametrizado que necesita vinculante.

Espero que esto ayude.

Buhake Sindi
fuente
¿Puede explicar qué quiere decir en su último párrafo?
Benz
El que dice la segunda opción te obligará a unir uno ... ¿puedes explicarlo con más detalle?
benz
<S extends T>establece que Ses un tipo parametrizado que es una subclase de T, por lo que requiere un tipo parametrizado (sin comodines) que sea una subclase de T.
Buhake Sindi
2

Otra diferencia que no se menciona aquí.

static <T> void fromArrayToCollection(T[] a, Collection<T> c) {
    for (T o : a) {
        c.add(o); // correct
    }
}

Pero lo siguiente resultará en un error de tiempo de compilación.

static <T> void fromArrayToCollection(T[] a, Collection<?> c) {
    for (T o : a) {
        c.add(o); // compile time error
    }
}
Vivek Kumar
fuente
0

Por lo que tengo entendido, solo hay un caso de uso en el que el comodín es estrictamente necesario (es decir, puede expresar algo que no puede expresar utilizando parámetros de tipo explícitos). Aquí es cuando necesita especificar un límite inferior.

Aparte de eso, sin embargo, los comodines sirven para escribir código más conciso, como se describe en las siguientes declaraciones en el documento que menciona:

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

Principalmente -> Los comodines hacen cumplir los genéricos en el nivel de parámetro / argumento de un método no genérico. Nota. También se puede realizar en genericMethod de forma predeterminada, pero aquí en lugar de? podemos usar la T misma.

paquete de genéricos;

public class DemoWildCard {


    public static void main(String[] args) {
        DemoWildCard obj = new DemoWildCard();

        obj.display(new Person<Integer>());
        obj.display(new Person<String>());

    }

    void display(Person<?> person) {
        //allows person of Integer,String or anything
        //This cannnot be done if we use T, because in that case we have to make this method itself generic
        System.out.println(person);
    }

}

class Person<T>{

}

SO wildcard tiene sus casos de uso específicos como este.

Arasn
fuente