Wann sollten generische Methoden und wann Platzhalter verwendet werden?

122

Ich lese über generische Methoden von OracleDocGenericMethod . Ich bin ziemlich verwirrt über den Vergleich, wenn es darum geht, wann Platzhalter und wann generische Methoden verwendet werden sollen. Zitieren aus dem Dokument.

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

Wir hätten hier stattdessen generische Methoden verwenden können:

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

[…] Dies sagt uns, dass das Typargument für den Polymorphismus verwendet wird; Die einzige Auswirkung besteht darin, dass verschiedene tatsächliche Argumenttypen an verschiedenen Aufrufstandorten verwendet werden können. In diesem Fall sollten Platzhalter verwendet werden. Platzhalter unterstützen die flexible Untertypisierung, die wir hier ausdrücken möchten.

Denken wir nicht, dass Wildcards wie (Collection<? extends E> c);auch Polymorphismus unterstützen? Warum wird die Verwendung generischer Methoden dann als nicht gut angesehen?

Weiter geht es, heißt es,

Mit generischen Methoden können Typparameter verwendet werden, um Abhängigkeiten zwischen den Typen eines oder mehrerer Argumente für eine Methode und / oder ihren Rückgabetyp auszudrücken. Wenn es keine solche Abhängigkeit gibt, sollte keine generische Methode verwendet werden.

Was bedeutet das?

Sie haben das Beispiel vorgestellt

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

[…]

Wir hätten die Signatur für diese Methode auch anders schreiben können, ohne Platzhalter zu verwenden:

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

Das Dokument entmutigt die zweite Deklaration und fördert die Verwendung der ersten Syntax? Was ist der Unterschied zwischen der ersten und der zweiten Erklärung? Beide scheinen dasselbe zu tun?

Kann jemand Licht in diesen Bereich bringen.

Benz
quelle

Antworten:

173

Es gibt bestimmte Stellen, an denen Platzhalter und Typparameter dasselbe tun. Es gibt aber auch bestimmte Stellen, an denen Sie Typparameter verwenden müssen.

  1. Wenn Sie eine Beziehung für die verschiedenen Arten von Methodenargumenten erzwingen möchten, können Sie dies nicht mit Platzhaltern tun. Sie müssen Typparameter verwenden.

Unter Ihrer Methode als Beispiel an , dass Sie sicherstellen möchten , dass die srcund die destListe übergeben copy()Methode der gleichen parametrisierte Typ sein sollten, können Sie es mit Typ - Parameter wie so tun können:

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

Hier wird sichergestellt, dass beide destund srcdenselben parametrisierten Typ für haben List. Es ist also sicher, Elemente von srcnach zu kopieren dest.

Wenn Sie jedoch die Methode zur Verwendung von Platzhaltern ändern:

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

es wird nicht wie erwartet funktionieren. Im 2. Fall können Sie List<Integer>und List<Float>als destund übergeben src. Das Verschieben von Elementen von srcnach destwäre also nicht mehr typsicher. Wenn Sie eine solche Beziehung nicht benötigen, können Sie keine Typparameter verwenden.

Einige andere Unterschiede zwischen der Verwendung von Platzhaltern und Typparametern sind:

  • Wenn Sie nur ein parametrisiertes Typargument haben, können Sie einen Platzhalter verwenden, obwohl der Typparameter auch funktioniert.
  • Typparameter unterstützen mehrere Grenzen, Platzhalter nicht.
  • Platzhalter unterstützen sowohl obere als auch untere Grenzen. Typparameter unterstützen nur obere Grenzen. Wenn Sie also eine Methode definieren möchten, die einen ListTyp Integeroder eine Superklasse annimmt , können Sie Folgendes tun:

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

    Sie können jedoch keinen Typparameter verwenden:

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

Verweise:

Rohit Jain
quelle
1
Das ist eine seltsame Antwort. Es erklärt nicht, warum Sie ?überhaupt verwenden müssen. Sie können es als `public static <T1 erweitert Number, T2 erweitert Number> void copy (List <T1> dest, List <T2> src) umschreiben und in diesem Fall wird klar, was los ist.
Kan
@kan. Nun, das ist das eigentliche Problem. Sie können den Typparameter verwenden, um denselben Typ zu erzwingen, dies ist jedoch mit Platzhaltern nicht möglich. Die Verwendung von zwei verschiedenen Typen für Typparameter ist eine andere Sache.
Rohit Jain
1
@benz. Sie können keine Untergrenzen in einem Listusing-Typparameter definieren. List<T super Integer>ist nicht gültig und wird nicht kompiliert.
Rohit Jain
2
@benz. Gern geschehen :) Ich empfehle Ihnen dringend, den Link durchzugehen, den ich am Ende gepostet habe. Das ist die beste Ressource für Generika, die Sie bekommen würden.
Rohit Jain
3
@ jorgen.ringen <T extends X & Y>-> mehrere Grenzen.
Rohit Jain
12

Betrachten Sie das folgende Beispiel aus der 4. Ausgabe von The Java Programming von James Gosling, in dem wir 2 SinglyLinkQueue zusammenführen möchten:

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
}

Beide oben genannten Methoden haben die gleiche Funktionalität. Was ist also vorzuziehen? Die Antwort ist die 2.. In den eigenen Worten des Autors:

"Die allgemeine Regel lautet, wenn möglich Platzhalter zu verwenden, da Code mit Platzhaltern im Allgemeinen besser lesbar ist als Code mit mehreren Typparametern. Wenn Sie entscheiden, ob Sie eine Typvariable benötigen, fragen Sie sich, ob diese Typvariable verwendet wird, um zwei oder mehr Parameter zu verknüpfen. oder um einen Parametertyp mit dem Rückgabetyp zu verknüpfen. Wenn die Antwort Nein lautet, sollte ein Platzhalter ausreichen. "

Hinweis: Im Buch wird nur die zweite Methode angegeben, und der Name des Typparameters lautet S anstelle von 'T'. Die erste Methode ist nicht im Buch enthalten.

Chammu
quelle
Ich habe für das Zitat eines Buches gestimmt, es ist direkt und prägnant
Kurapika
9

In Ihrer ersten Frage: Wenn eine Beziehung zwischen dem Typ des Parameters und dem Rückgabetyp der Methode besteht, verwenden Sie ein generisches.

Beispielsweise:

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

Hier extrahieren Sie einige der T nach bestimmten Kriterien. Wenn T ist, werden LongIhre Methoden zurückkehren Longund Collection<Long>; Der tatsächliche Rückgabetyp hängt vom Parametertyp ab. Daher ist es nützlich und ratsam, generische Typen zu verwenden.

Wenn dies nicht der Fall ist, können Sie Platzhaltertypen verwenden:

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

In diesen beiden Beispielen sind die Rückgabetypen unabhängig vom Typ der Elemente in den Sammlungen intund boolean.

In Ihren Beispielen:

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

Diese beiden Funktionen geben einen Booleschen Wert zurück, unabhängig von der Art der Elemente in den Sammlungen. Im zweiten Fall ist es auf Instanzen einer Unterklasse von E beschränkt.

Zweite Frage:

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

Mit diesem ersten Code können Sie einen heterogenen List<? extends T> srcParameter übergeben. Diese Liste kann mehrere Elemente verschiedener Klassen enthalten, solange sie alle die Basisklasse T erweitern.

wenn du hättest:

interface Fruit{}

und

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

du könntest es tun

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 

Andererseits

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

beschränken Sie List<S> srcsich auf eine bestimmte Klasse S, die eine Unterklasse von T ist. Die Liste kann nur Elemente einer Klasse (in diesem Fall S) und keiner anderen Klasse enthalten, selbst wenn sie auch T implementieren. Sie könnten mein vorheriges Beispiel nicht verwenden, aber Sie könnten:

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
quelle
1
List<? extends Fruit> basket = new ArrayList<? extends Fruit>();Ist keine gültige Syntax. Sie müssen die ArrayList ohne Grenzen instanziieren.
Arnold Pistorius
Im obigen Beispiel kann kein Apfel zum Warenkorb hinzugefügt werden, da der Warenkorb eine Liste von Birnen sein kann. Falsches Beispiel AFAIK. Und kompiliert nicht so gut.
Khanna111
1
@ArnoldPistorius Das hat mich verwirrt. Ich habe die API-Dokumentation von ArrayList überprüft und es ist ein Konstruktor signiert ArrayList(Collection<? extends E> c). Können Sie mir erklären, warum Sie das gesagt haben?
Kurapika
@Kurapika Könnte sein, dass ich eine alte Java-Version verwendet habe? Kommentar wurde vor fast 3 Jahren veröffentlicht.
Arnold Pistorius
2

Die Wildcard-Methode ist ebenfalls generisch - Sie können sie mit einer Reihe von Typen aufrufen.

Die <T>Syntax definiert einen Typvariablennamen. Wenn eine Typvariable eine Verwendung hat (z. B. in der Methodenimplementierung oder als Einschränkung für einen anderen Typ), ist es sinnvoll, sie ?als anonyme Variable zu benennen, andernfalls könnten Sie sie verwenden . Sieht also nur nach einer Abkürzung aus.

Darüber hinaus ist die ?Syntax nicht vermeidbar, wenn Sie ein Feld deklarieren:

class NumberContainer
{
 Set<? extends Number> numbers;
}
kan
quelle
3
Soll das nicht ein Kommentar sein?
Buhake Sindi
@BuhakeSindi Sorry, was ist unklar? Warum -1? Ich denke, es beantwortet die Frage.
Kan
2

Ich werde versuchen, Ihre Frage einzeln zu beantworten.

Denken wir nicht, dass Wildcards wie (Collection<? extends E> c);auch Polymorphismus unterstützen?

Nein. Der Grund dafür ist, dass der begrenzte Platzhalter keinen definierten Parametertyp hat. Es ist ein Unbekannter. Alles, was es "weiß", ist, dass das "Containment" von einem Typ ist E(was auch immer definiert ist). Daher kann nicht überprüft und begründet werden, ob der angegebene Wert mit dem begrenzten Typ übereinstimmt.

Es ist also nicht sinnvoll, polymorphe Verhaltensweisen auf Platzhaltern zu haben.

Das Dokument entmutigt die zweite Deklaration und fördert die Verwendung der ersten Syntax? Was ist der Unterschied zwischen der ersten und der zweiten Erklärung? Beide scheinen dasselbe zu tun?

Die erste Option ist in diesem Fall besser, da sie Timmer begrenzt ist und sourcedefinitiv Werte (von Unbekannten) aufweist, die in Unterklassen unterteilt sind T.

Angenommen, Sie möchten alle Zahlenlisten kopieren. Die erste Option ist

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

srcIm wesentlichen kann annehmen List<Double>, List<Float>usw. , da es eine obere Grenze ist auf die parametrierte Typ gefunden dest.

Die zweite Option zwingt Sie, Sfür jeden Typ, den Sie kopieren möchten, wie folgt zu binden

//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 Sist ein parametrisierter Typ, der gebunden werden muss.

Ich hoffe das hilft.

Buhake Sindi
quelle
Können Sie erklären, was Sie in Ihrem letzten Absatz meinen
Benz
Diejenige, die die 2. Option angibt, zwingt Sie, eine zu binden ...... können Sie
näher darauf
<S extends T>Gibt an, dass Ses sich um einen parametrisierten Typ handelt, der eine Unterklasse von Tist. Daher ist ein parametrisierter Typ (kein Platzhalter) erforderlich, der eine Unterklasse von ist T.
Buhake Sindi
2

Ein weiterer Unterschied, der hier nicht aufgeführt ist.

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

Das Folgende führt jedoch zu einem Fehler bei der Kompilierung.

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

Soweit ich weiß, gibt es nur einen Anwendungsfall, in dem ein Platzhalter unbedingt benötigt wird (dh etwas ausdrücken kann, das Sie nicht mit expliziten Typparametern ausdrücken können). In diesem Fall müssen Sie eine Untergrenze angeben.

Abgesehen davon dienen Platzhalter jedoch dazu, präziseren Code zu schreiben, wie in den folgenden Anweisungen in dem von Ihnen erwähnten Dokument beschrieben:

Mit generischen Methoden können Typparameter verwendet werden, um Abhängigkeiten zwischen den Typen eines oder mehrerer Argumente für eine Methode und / oder ihren Rückgabetyp auszudrücken. Wenn es keine solche Abhängigkeit gibt, sollte keine generische Methode verwendet werden.

[...]

Die Verwendung von Platzhaltern ist klarer und prägnanter als die Angabe expliziter Typparameter und sollte daher nach Möglichkeit bevorzugt werden.

[...]

Platzhalter haben auch den Vorteil, dass sie außerhalb von Methodensignaturen als Feldtypen, lokale Variablen und Arrays verwendet werden können.

Martin Maletinsky
quelle
0

Hauptsächlich -> Platzhalter erzwingen Generika auf Parameter- / Argumentebene einer nicht generischen Methode. Hinweis. Es kann standardmäßig auch in genericMethod ausgeführt werden, aber hier anstelle von? wir können T selbst verwenden.

Paket Generika;

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 hat seine spezifischen Anwendungsfälle wie diesen.

Arasn
quelle