web-dev-qa-db-fra.com

Quand utiliser des méthodes génériques et quand utiliser wild-card?

Je lis sur les méthodes génériques de OracleDocGenericMethod . Je suis assez confus à propos de la comparaison quand il est dit quand utiliser un joker et quand utiliser des méthodes génériques. Citant du document.

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

Nous aurions pu utiliser des méthodes génériques ici:

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

[…] Cela nous indique que l'argument type est utilisé pour le polymorphisme; Son seul effet est de permettre l'utilisation d'une variété de types d'arguments réels sur différents sites d'invocation. Si tel est le cas, il convient d'utiliser des caractères génériques. Les caractères génériques sont conçus pour prendre en charge les sous-types flexibles, ce que nous essayons d'exprimer ici.

Ne pensons-nous pas que les cartes de joker sont comme (Collection<? extends E> c); supporte-t-il aussi une sorte de polymorphisme? Alors, pourquoi l’utilisation de méthodes génériques n’est-elle pas considérée comme bonne?

Continuant devant, il dit,

Les méthodes génériques permettent d'utiliser des paramètres de type pour exprimer des dépendances entre les types d'un ou plusieurs arguments d'une méthode et/ou de son type de retour. S'il n'y a pas une telle dépendance, une méthode générique ne doit pas être utilisée.

Qu'est-ce que ça veut dire?

Ils ont présenté l'exemple

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

[…]

Nous aurions pu écrire la signature de cette méthode d'une autre manière, sans utiliser de caractère générique:

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

Le document décourage la seconde déclaration et encourage l'utilisation de la première syntaxe? Quelle est la différence entre la première et la deuxième déclaration? Les deux semblent faire la même chose?

Quelqu'un peut-il éclairer cette zone?.

110
benz

Il existe certains endroits, où les caractères génériques et les paramètres de type font la même chose. Mais il existe également certains endroits où vous devez utiliser des paramètres de type.

  1. Si vous souhaitez appliquer une relation sur les différents types d'arguments de méthode, vous ne pouvez pas le faire avec des caractères génériques, vous devez utiliser des paramètres de type.

En prenant votre méthode comme exemple, supposons que vous vouliez vous assurer que la liste src et dest transmise à la méthode copy() soit du même type paramétré, vous pouvez le faire avec le type paramètres tels que:

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

Ici, vous êtes assuré que dest et src ont le même type paramétré pour List. Donc, il est prudent de copier des éléments de src vers dest.

Mais si vous continuez à changer de méthode pour utiliser un caractère générique:

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

cela ne fonctionnera pas comme prévu. Dans le second cas, vous pouvez passer List<Integer> et List<Float> comme dest et src. Ainsi, déplacer des éléments de src à dest ne serait plus du type sûr. Si vous n'avez pas besoin de ce type de relation, vous êtes libre de ne pas utiliser de paramètres de type.

Une autre différence entre l'utilisation de caractères génériques et les paramètres de type est la suivante:

  • Si vous n'avez qu'un seul argument de type paramétré, vous pouvez utiliser un caractère générique, bien que paramètre de type fonctionne également.
  • Les paramètres de type prennent en charge plusieurs limites, contrairement aux caractères génériques.
  • Les caractères génériques prennent en charge les limites supérieure et inférieure, les paramètres de type ne prennent en charge que les limites supérieures. Donc, si vous voulez définir une méthode qui prend un List de type Integer ou sa super classe, vous pouvez faire:

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

    mais vous ne pouvez pas utiliser le paramètre type:

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

Références:

156
Rohit Jain

Dans votre première question: Cela signifie que s'il existe une relation entre le type du paramètre et le type de retour de la méthode, utilisez un nom générique.

Par exemple:

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

Ici, vous extrayez une partie du T en fonction de certains critères. Si T est Long vos méthodes renverront Long et Collection<Long>; Le type de retour réel dépend du type de paramètre. Il est donc utile et conseillé d'utiliser des types génériques.

Si ce n'est pas le cas, vous pouvez utiliser des types de caractères génériques:

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

Dans ces deux exemples, quel que soit le type des éléments des collections, les types de retour seront int et boolean.

Dans vos exemples:

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

ces deux fonctions renverront un booléen quel que soit le type des éléments des collections. Dans le second cas, il est limité aux instances d'une sous-classe de E.

Deuxième question:

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

Ce premier code vous permet de passer un List<? extends T> src en tant que paramètre. Cette liste peut contenir plusieurs éléments de différentes classes à condition qu’ils élargissent tous la classe de base T.

si tu avais:

interface Fruit{}

et

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

vous pourriez faire

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'autre part

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

contraindre List<S> src appartenir à une classe particulière S qui est une sous-classe de T. La liste ne peut contenir que des éléments d’une classe (ici S) et aucune autre classe, même s’ils implémentent également T. Vous ne pourriez pas utiliser mon exemple précédent mais vous pourriez faire:

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

Vous pouvez suivre l'exemple de la Java Programmation par James Gosling, 4ème édition ci-dessous, où nous souhaitons fusionner 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
}

Les deux méthodes ci-dessus ont les mêmes fonctionnalités. Alors, lequel est préférable? La réponse est 2e. Dans les propres mots de l'auteur:

"La règle générale est d'utiliser des caractères génériques quand vous le pouvez car le code avec des caractères génériques est généralement plus lisible que le code avec plusieurs paramètres de type. Lorsque vous décidez si vous avez besoin d'une variable de type, demandez-vous si cette variable de type est utilisée pour relier deux ou plusieurs paramètres, ou pour associer un type de paramètre au type de retour. Si la réponse est non, un caractère générique devrait suffire. "

Remarque: Dans le livre seulement, la deuxième méthode est indiquée et le nom du paramètre de type est S au lieu de 'T'. La première méthode n'est pas là dans le livre.

10
chammu

Je vais essayer de répondre à votre question, un par un.

Ne pensons-nous pas que les cartes de joker sont comme (Collection<? extends E> c); supporte-t-il aussi une sorte de polymorphisme?

La raison en est que le caractère générique délimité n'a pas de type de paramètre défini. C'est un inconnu. Tout ce qu'il "sait", c'est que le "confinement" est de type E (quelle que soit sa définition). Ainsi, il ne peut pas vérifier et justifier si la valeur fournie correspond au type lié.

Il n’est donc pas judicieux d’avoir des comportements polymorphes sur des caractères génériques.

Le document décourage la seconde déclaration et encourage l'utilisation de la première syntaxe? Quelle est la différence entre la première et la deuxième déclaration? Les deux semblent faire la même chose?

La première option est meilleure dans ce cas car T est toujours lié, et source aura certainement des valeurs (inconnues) que les sous-classes T.

Donc, supposons que vous vouliez copier toute la liste de nombres, la première option sera

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

src peut accepter essentiellement List<Double>, List<Float>, etc., car il existe une limite supérieure au type paramétrable qui se trouve dans dest.

La 2ème option vous obligera à lier S pour chaque type que vous voulez copier, comme si

//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 est un type paramétré qui nécessite une liaison.

J'espère que ça aide.

2
Buhake Sindi

Une autre différence qui n'est pas listée ici.

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

Mais ce qui suit entraînera une erreur de temps de compilation.

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

La méthode générique est également générique - vous pouvez l'appeler avec une gamme de types.

Le <T> La syntaxe définit un nom de variable de type. Si une variable de type a une utilité quelconque (par exemple, dans l’implémentation de méthode ou en tant que contrainte pour un autre type), il est logique de la nommer, sinon vous pouvez utiliser ?, en tant que variable anonyme. Donc, ça ressemble à un raccourci.

De plus, le ? _ la syntaxe n’est pas évitable lorsque vous déclarez un champ:

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

Pour autant que je sache, il n’existe qu’un seul cas d’utilisation pour lequel un caractère générique est strictement nécessaire (c’est-à-dire que vous pouvez exprimer quelque chose que vous ne pouvez pas exprimer à l’aide de paramètres de type explicite). C'est à ce moment que vous devez spécifier une limite inférieure.

En dehors de cela, les caractères génériques servent à écrire un code plus concis, comme décrit dans les déclarations suivantes du document que vous mentionnez:

Les méthodes génériques permettent d'utiliser des paramètres de type pour exprimer des dépendances entre les types d'un ou plusieurs arguments d'une méthode et/ou de son type de retour. S'il n'y a pas une telle dépendance, une méthode générique ne doit pas être utilisée.

[...]

L'utilisation de caractères génériques est plus claire et concise que la déclaration de paramètres de type explicites et doit donc être privilégiée dans la mesure du possible.

[...]

Les caractères génériques ont également l'avantage de pouvoir être utilisés en dehors des signatures de méthodes, comme les types de champs, les variables locales et les tableaux.

0
Martin Maletinsky