Quando usar métodos genéricos e quando usar curinga?

122

Estou lendo sobre métodos genéricos do OracleDocGenericMethod . Estou bastante confuso sobre a comparação quando diz quando usar curingas e quando usar métodos genéricos. Citando do documento.

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

Poderíamos ter usado métodos genéricos aqui:

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!
}

[…] Isso nos diz que o argumento de tipo está sendo usado para polimorfismo; seu único efeito é permitir que vários tipos de argumentos reais sejam usados ​​em diferentes sites de chamada. Se for esse o caso, deve-se usar curingas. Os curingas são projetados para oferecer suporte à subtipagem flexível, que é o que estamos tentando expressar aqui.

Não achamos que o curinga (Collection<? extends E> c);também suporta um tipo de polimorfismo? Então, por que o uso genérico do método não é considerado bom nisso?

Continuando adiante, afirma,

Métodos genéricos permitem que parâmetros de tipo sejam usados ​​para expressar dependências entre os tipos de um ou mais argumentos para um método e / ou seu tipo de retorno. Se não houver essa dependência, um método genérico não deve ser usado.

O que isto significa?

Eles apresentaram o exemplo

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

[...]

Poderíamos ter escrito a assinatura desse método de outra maneira, sem usar caracteres curinga:

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

O documento desencoraja a segunda declaração e promove o uso da primeira sintaxe? Qual é a diferença entre a primeira e a segunda declaração? Ambos parecem estar fazendo a mesma coisa?

Alguém pode colocar luz nesta área.

benz
fonte

Respostas:

173

Existem certos lugares em que curingas e parâmetros de tipo fazem a mesma coisa. Mas também existem certos lugares onde você deve usar parâmetros de tipo.

  1. Se você deseja impor algum relacionamento nos diferentes tipos de argumentos do método, não pode fazer isso com caracteres curinga, é necessário usar parâmetros de tipo.

Tomando seu método como exemplo, suponha que você deseje garantir que a lista srce destpassada para o copy()método sejam do mesmo tipo parametrizado, você pode fazê-lo com os parâmetros de tipo da seguinte maneira:

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

Aqui, você garante que ambos deste srctenham o mesmo tipo de parâmetro para List. Portanto, é seguro copiar elementos de srcpara dest.

Mas, se você continuar alterando o método para usar o curinga:

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

não funcionará como esperado. No segundo caso, você pode passar List<Integer>e List<Float>como deste src. Portanto, mover elementos de srcpara destnão seria mais um tipo seguro. Se você não precisar desse tipo de relação, estará livre para não usar parâmetros de tipo.

Algumas outras diferenças entre o uso de curingas e parâmetros de tipo são:

  • Se você tiver apenas um argumento de tipo com parâmetros, poderá usar curinga, embora o parâmetro type também funcione.
  • Os parâmetros de tipo suportam vários limites, os curingas não.
  • Os curingas suportam limites superior e inferior; os parâmetros de tipo apenas suportam limites superiores. Portanto, se você deseja definir um método que usa um Listtipo Integerou sua super classe, você pode:

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

    mas você não pode usar o parâmetro type:

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

Referências:

Rohit Jain
fonte
1
Essa é uma resposta estranha. Não explica por que você precisa usar ?. Você pode reescrevê-lo como `public static <T1 extends Number, T2 extends Number> void copy (Lista <T1> dest, List <T2> src) e, neste caso, fica óbvio o que está acontecendo.
kan
@kan. Bem, esse é o verdadeiro problema. Você pode usar o parâmetro type para aplicar o mesmo tipo, mas não pode fazer isso com curingas. Usar dois tipos diferentes para o parâmetro type é algo diferente.
Rohit Jain
1
@benz. Você não pode definir limites inferiores em um Listparâmetro de tipo using. List<T super Integer>não é válido e não será compilado.
Rohit Jain
2
@benz. De nada :) Eu sugiro que você acesse o link que eu publiquei no final. Esse é o melhor recurso sobre genéricos que você obteria.
Rohit Jain
3
@ jorgen.ringen <T extends X & Y>-> múltiplos limites.
Rohit Jain
12

Considere o seguinte exemplo de The Java Programming de James Gosling 4th edition abaixo, onde queremos mesclar 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 os métodos acima têm a mesma funcionalidade. Então, qual é preferível? A resposta é a segunda. Nas próprias palavras do autor:

"A regra geral é usar curingas quando possível, porque o código com curingas geralmente é mais legível que o código com vários parâmetros de tipo. Ao decidir se você precisa de uma variável de tipo, pergunte-se se essa variável de tipo é usada para relacionar dois ou mais parâmetros, ou relacionar um tipo de parâmetro com o tipo de retorno. Se a resposta for não, um curinga deve ser suficiente ".

Nota: No livro, apenas o segundo método é fornecido e o tipo de nome do parâmetro é S em vez de 'T'. O primeiro método não existe no livro.

chammu
fonte
eu votei para a citação de um livro, é direta e concisa
Kurapika
9

Na sua primeira pergunta: significa que, se houver uma relação entre o tipo do parâmetro e o tipo de retorno do método, use um genérico.

Por exemplo:

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

Aqui você está extraindo parte do T seguindo um determinado critério. Se T for, Longseus métodos retornarão Longe Collection<Long>; o tipo de retorno real depende do tipo de parâmetro, portanto, é útil e recomendado usar tipos genéricos.

Quando não é esse o caso, você pode usar tipos de curingas:

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

Neste dois exemplos, seja qual for o tipo dos itens nas coleções, os tipos de retorno serão inte boolean.

Nos seus exemplos:

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

essas duas funções retornarão um booleano, independentemente dos tipos de itens nas coleções. No segundo caso, é limitado a instâncias de uma subclasse de E.

Segunda questão:

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

Este primeiro código permite que você passe um heterogêneo List<? extends T> srccomo parâmetro. Essa lista pode conter vários elementos de diferentes classes, desde que todos eles estendam a classe base T.

se você tinha:

interface Fruit{}

e

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

você poderia fazer

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 outro lado

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

restringe List<S> srca pertencer a uma classe S específica que é uma subclasse de T. A lista pode conter apenas elementos de uma classe (nesse caso, S) e nenhuma outra classe, mesmo que eles também implementem T. Você não seria capaz de usar meu exemplo anterior, mas poderia:

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
fonte
1
List<? extends Fruit> basket = new ArrayList<? extends Fruit>();Não é uma sintaxe válida. Você precisa instanciar o ArrayList sem limites.
precisa
Não é possível adicionar uma maçã à cesta no exemplo acima, pois a cesta pode ser uma lista de peras. Exemplo incorreto de AFAIK. E não compila também.
precisa saber é o seguinte
1
@ArnoldPistorius Isso me confundiu. Eu verifiquei a documentação da API do ArrayList e ele tem um construtor assinado ArrayList(Collection<? extends E> c). Você pode me explicar por que você disse isso?
Kurapika
@ Kurapika Pode ser que eu estivesse usando uma versão antiga do Java? O comentário foi publicado há quase 3 anos.
Arnold Pistorius
2

O método curinga também é genérico - você pode chamá-lo com alguns tipos de tipos.

A <T>sintaxe define um nome de variável de tipo. Se uma variável de tipo tiver alguma utilidade (por exemplo, na implementação de métodos ou como uma restrição para outro tipo), faz sentido nomeá-la, caso contrário você pode usar ?como variável anônima. Então, parece apenas um atalho.

Além disso, a ?sintaxe não é evitável quando você declara um campo:

class NumberContainer
{
 Set<? extends Number> numbers;
}
kan
fonte
3
Isso não deveria ser um comentário?
Buhake Sindi
@BuhakeSindi Desculpe, o que não está claro? Por que -1? Eu acho que responde à pergunta.
kan
2

Vou tentar responder à sua pergunta, uma por uma.

Não achamos que o curinga (Collection<? extends E> c);também suporta um tipo de polimorfismo?

Não. O motivo é que o curinga limitado não possui um tipo de parâmetro definido. Isso é desconhecido. Tudo o que "sabe" é que a "contenção" é de um tipo E(o que for definido). Portanto, não é possível verificar e justificar se o valor fornecido corresponde ao tipo limitado.

Portanto, não é sensato ter comportamentos polimórficos em caracteres curinga.

O documento desencoraja a segunda declaração e promove o uso da primeira sintaxe? Qual é a diferença entre a primeira e a segunda declaração? Ambos parecem estar fazendo a mesma coisa?

A primeira opção é melhor nesse caso, como Tsempre é delimitada, e sourcedefinitivamente terá valores (de incógnitas) que subclasses T.

Portanto, suponha que você queira copiar toda a lista de números, a primeira opção será

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

src, essencialmente, pode aceitar List<Double>, List<Float>etc., pois há um limite superior para o tipo parametrizado encontrado em dest.

A segunda opção forçará você a vincular Stodos os tipos que você deseja copiar, assim

//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 Sé um tipo parametrizado que precisa de ligação.

Eu espero que isso ajude.

Buhake Sindi
fonte
Pode explicar o que quer dizer em seu último parágrafo
benz
Aquele que afirma segunda opção irá forçá-lo a ligar um ...... você pode elaborar sobre ele
benz
<S extends T>declara que Sé um tipo parametrizado que é uma subclasse de T, portanto, requer um tipo parametrizado (sem curinga) que é uma subclasse de T.
Buhake Sindi
2

Outra diferença que não está listada aqui.

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

Mas o seguinte resultará em erro no tempo de compilação.

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

Tanto quanto eu entendo, há apenas um caso de uso quando o curinga é estritamente necessário (ou seja, pode expressar algo que você não pode expressar usando parâmetros de tipo explícito). É quando você precisa especificar um limite inferior.

Além disso, os curingas servem para escrever um código mais conciso, conforme descrito pelas seguintes instruções no documento que você menciona:

Métodos genéricos permitem que parâmetros de tipo sejam usados ​​para expressar dependências entre os tipos de um ou mais argumentos para um método e / ou seu tipo de retorno. Se não houver essa dependência, um método genérico não deve ser usado.

[...]

O uso de curingas é mais claro e conciso do que declarar parâmetros de tipo explícito e, portanto, deve ser preferido sempre que possível.

[...]

Os curingas também têm a vantagem de poderem ser usados ​​fora das assinaturas de métodos, como os tipos de campos, variáveis ​​locais e matrizes.

Martin Maletinsky
fonte
0

Principalmente -> Curingas aplicam genéricos no nível de parâmetro / argumento de um método Não Genérico. Nota. Também pode ser executado em genericMethod por padrão, mas aqui em vez de? nós podemos usar o próprio T.

genéricos de pacotes;

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>{

}

O curinga SO tem seus casos de uso específicos como este.

Arasn
fonte