Quando utilizzare metodi generici e quando utilizzare caratteri jolly?


122

Sto leggendo dei metodi generici da OracleDocGenericMethod . Sono piuttosto confuso riguardo al confronto quando dice quando usare i caratteri jolly e quando usare metodi generici. Citando dal documento.

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

Avremmo potuto utilizzare metodi generici qui invece:

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

[…] Questo ci dice che l'argomento type è utilizzato per il polimorfismo; il suo unico effetto è quello di consentire l'utilizzo di una varietà di tipi di argomenti effettivi in ​​diversi siti di invocazione. In tal caso, è necessario utilizzare i caratteri jolly. I caratteri jolly sono progettati per supportare la sottotipizzazione flessibile, che è ciò che stiamo cercando di esprimere qui.

Non pensiamo che anche il carattere jolly (Collection<? extends E> c);supporti il ​​polimorfismo? Allora perché l'uso del metodo generico è considerato non buono in questo?

Proseguendo, afferma,

I metodi generici consentono di utilizzare parametri di tipo per esprimere le dipendenze tra i tipi di uno o più argomenti da un metodo e / o dal suo tipo restituito. Se non esiste una tale dipendenza, non dovrebbe essere utilizzato un metodo generico.

Cosa significa questo?

Hanno presentato l'esempio

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

[...]

Avremmo potuto scrivere la firma per questo metodo in un altro modo, senza utilizzare i caratteri jolly:

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

Il documento scoraggia la seconda dichiarazione e promuove l'uso della prima sintassi? Qual è la differenza tra la prima e la seconda dichiarazione? Entrambi sembrano fare la stessa cosa?

Qualcuno può illuminare quest'area.

Risposte:


173

Ci sono alcuni luoghi in cui i caratteri jolly e i parametri di tipo fanno la stessa cosa. Ma ci sono anche alcuni posti in cui devi usare i parametri di tipo.

  1. Se vuoi imporre una relazione sui diversi tipi di argomenti del metodo, non puoi farlo con i caratteri jolly, devi usare i parametri di tipo.

Prendendo il tuo metodo come esempio, supponi di voler assicurarti che l' elenco srce destpassato al copy()metodo debba essere dello stesso tipo parametrizzato, puoi farlo con parametri di tipo in questo modo:

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

Qui si è certi che entrambi deste srchanno lo stesso tipo parametrizzato per List. Quindi, è sicuro copiare elementi da srca dest.

Ma se continui a modificare il metodo per utilizzare il carattere jolly:

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

non funzionerà come previsto. Nel 2 ° caso, puoi passare List<Integer>e List<Float>come deste src. Quindi, spostare elementi da srca destnon sarebbe più sicuro per i tipi. Se non hai bisogno di questo tipo di relazione, sei libero di non usare affatto i parametri di tipo.

Alcune altre differenze tra l'utilizzo di caratteri jolly e parametri di tipo sono:

  • Se si dispone di un solo argomento di tipo parametrizzato, è possibile utilizzare caratteri jolly, sebbene funzionerà anche il parametro di tipo.
  • I parametri di tipo supportano più limiti, i caratteri jolly no.
  • I caratteri jolly supportano i limiti superiore e inferiore, i parametri di tipo supportano solo i limiti superiori. Quindi, se vuoi definire un metodo che accetta un Listtipo Integero è una super classe, puoi fare:

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

    ma non puoi usare il parametro di tipo:

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

Riferimenti:


1
Questa è una risposta strana. Non spiega affatto perché è necessario utilizzarlo ?. Potresti riscriverlo come `public static <T1 extends Number, T2 extends Number> void copy (List <T1> dest, List <T2> src) e in questo caso diventa ovvio cosa sta succedendo.
kan

@kan. Bene, questo è il vero problema. Puoi utilizzare il parametro di tipo per applicare lo stesso tipo, ma non puoi farlo con i caratteri jolly. L'uso di due tipi diversi per il parametro di tipo è una cosa diversa.
Rohit Jain

1
@benz. Non è possibile definire limiti inferiori in un Listparametro di tipo using. List<T super Integer>non è valido e non verrà compilato.
Rohit Jain

2
@benz. Prego :) Vi consiglio vivamente di passare attraverso il link che ho postato alla fine. Questa è la migliore risorsa sui generici che potresti ottenere.
Rohit Jain

3
@ jorgen.ringen <T extends X & Y>-> più limiti.
Rohit Jain

12

Considera il seguente esempio tratto da The Java Programming di James Gosling, quarta edizione di seguito, dove vogliamo unire 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
}

Entrambi i metodi sopra hanno la stessa funzionalità. Allora quale è preferibile? La risposta è la seconda. Nelle parole dell'autore:

"La regola generale è usare i caratteri jolly quando possibile perché il codice con caratteri jolly è generalmente più leggibile del codice con più parametri di tipo. Quando decidi se hai bisogno di una variabile di tipo, chiediti se quella variabile di tipo è usata per correlare due o più parametri, o per correlare un tipo di parametro con il tipo restituito. Se la risposta è no, dovrebbe essere sufficiente un carattere jolly. "

Nota: nel libro viene fornito solo il secondo metodo e il nome del parametro di tipo è S invece di 'T'. Il primo metodo non è presente nel libro.


Ho votato per la citazione di un libro, è diretto e conciso
Kurapika

9

Nella tua prima domanda: significa che se esiste una relazione tra il tipo del parametro e il tipo restituito del metodo, usa un generico.

Per esempio:

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

Qui stai estraendo alcune delle T seguendo un certo criterio. Se T è, i Longtuoi metodi torneranno Longe Collection<Long>; il tipo restituito effettivo dipende dal tipo di parametro, quindi è utile e consigliato utilizzare tipi generici.

Quando questo non è il caso, puoi utilizzare i tipi di caratteri jolly:

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

In questi due esempi, qualunque sia il tipo di elementi nelle collezioni, i tipi restituiti saranno inte boolean.

Nei tuoi esempi:

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

queste due funzioni restituiranno un valore booleano qualunque siano i tipi di elementi nelle collezioni. Nel secondo caso è limitato alle istanze di una sottoclasse di E.

Seconda domanda:

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

Questo primo codice ti permette di passare un eterogeneo List<? extends T> srccome parametro. Questo elenco può contenere più elementi di classi diverse purché estendano tutti la classe base T.

se tu avessi:

interface Fruit{}

e

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

potresti fare

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 

D'altro canto

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

vincolare List<S> srcper essere di una particolare classe S che è una sottoclasse di T. La lista può contenere solo elementi di una classe (in questo caso S) e nessun'altra classe, anche se implementano anche T. Non saresti in grado di usare il mio esempio precedente ma potresti fare:

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. */

1
List<? extends Fruit> basket = new ArrayList<? extends Fruit>();Non è una sintassi valida. È necessario creare un'istanza di ArrayList senza limiti.
Arnold Pistorius

Impossibile aggiungere una mela al carrello nell'esempio sopra poiché il carrello potrebbe essere un elenco di pere. Esempio errato AFAIK. E non si compila anche.
Khanna111

1
@ArnoldPistorius Questo mi ha confuso. Ho controllato la documentazione API di ArrayList e ha un costruttore firmato ArrayList(Collection<? extends E> c). Puoi spiegarmi perché l'hai detto?
Kurapika

@Kurapika Potrebbe essere che stavo usando una vecchia versione di Java? Il commento è stato pubblicato quasi 3 anni fa.
Arnold Pistorius

2

Anche il metodo con caratteri jolly è generico: potresti chiamarlo con una serie di tipi.

La <T>sintassi definisce un nome di variabile di tipo. Se una variabile di tipo ha qualche utilità (ad esempio nell'implementazione del metodo o come vincolo per un altro tipo), allora ha senso nominarla, altrimenti potresti usare ?, come variabile anonima. Quindi, sembra solo una scorciatoia.

Inoltre, la ?sintassi non è evitabile quando dichiari un campo:

class NumberContainer
{
 Set<? extends Number> numbers;
}

3
Questo non dovrebbe essere un commento?
Buhake Sindi

@BuhakeSindi Scusa, cosa non è chiaro? Perché -1? Penso che risponda alla domanda.
kan

2

Proverò a rispondere alla tua domanda, uno per uno.

Non pensiamo che anche il carattere jolly (Collection<? extends E> c);supporti il ​​polimorfismo?

No. Il motivo è che il carattere jolly delimitato non ha un tipo di parametro definito. È uno sconosciuto. Tutto quello che "sa" è che il "contenimento" è di un tipo E(qualunque sia definito). Pertanto, non può verificare e giustificare se il valore fornito corrisponde al tipo limitato.

Quindi, non è sensato avere comportamenti polimorfici sui caratteri jolly.

Il documento scoraggia la seconda dichiarazione e promuove l'uso della prima sintassi? Qual è la differenza tra la prima e la seconda dichiarazione? Entrambi sembrano fare la stessa cosa?

La prima opzione è migliore in questo caso in quanto Tè sempre limitata e sourceavrà sicuramente valori (di incognite) che sottoclassi T.

Quindi, supponiamo di voler copiare tutto l'elenco di numeri, la prima opzione sarà

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

src, essenzialmente, può accettare List<Double>, List<Float>ecc. poiché esiste un limite superiore al tipo parametrizzato che si trova in dest.

La seconda opzione ti costringerà a legare Sper ogni tipo che desideri copiare, in questo modo

//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.

As Sè un tipo parametrizzato che necessita di associazione.

Spero che aiuti.


Puoi spiegare cosa intendi nel tuo ultimo paragrafo
benz

Quello che afferma la seconda opzione ti costringerà a vincolarne uno ... puoi approfondirlo
benz

<S extends T>afferma che Sè un tipo parametrizzato che è una sottoclasse di T, quindi richiede un tipo parametrizzato (nessun carattere jolly) che è una sottoclasse di T.
Buhake Sindi

2

Un'altra differenza che non è elencata qui.

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

Ma quanto segue comporterà un errore in fase di compilazione.

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

0

Per quanto ho capito, c'è solo un caso d'uso in cui il carattere jolly è strettamente necessario (cioè può esprimere qualcosa che non puoi esprimere usando parametri di tipo esplicito). Questo è quando è necessario specificare un limite inferiore.

A parte questo, tuttavia, i caratteri jolly servono per scrivere codice più conciso, come descritto dalle seguenti dichiarazioni nel documento che menzioni:

I metodi generici consentono di utilizzare parametri di tipo per esprimere le dipendenze tra i tipi di uno o più argomenti da un metodo e / o dal suo tipo restituito. Se non esiste una tale dipendenza, non dovrebbe essere utilizzato un metodo generico.

[...]

L'uso di caratteri jolly è più chiaro e conciso rispetto alla dichiarazione di parametri di tipo espliciti e dovrebbe pertanto essere preferito quando possibile.

[...]

I caratteri jolly hanno anche il vantaggio di poter essere utilizzati al di fuori delle firme dei metodi, come i tipi di campi, le variabili locali e gli array.


0

Principalmente -> I caratteri jolly applicano i generici a livello di parametro / argomento di un metodo non generico. Nota. Può anche essere eseguito in genericMethod per impostazione predefinita, ma qui invece di? possiamo usare lo stesso T.

pacchetti generici;

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 jolly ha i suoi casi d'uso specifici come questo.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.