Java: Begrenzte Platzhalter oder gebundene Typparameter?

83

Kürzlich habe ich diesen Artikel gelesen: http://download.oracle.com/javase/tutorial/extra/generics/wildcards.html

Meine Frage ist, anstatt eine Methode wie diese zu erstellen:

public void drawAll(List<? extends Shape> shapes){
    for (Shape s: shapes) {
        s.draw(this);
    }
}

Ich kann eine Methode wie diese erstellen, und sie funktioniert einwandfrei:

public <T extends Shape> void drawAll(List<T> shapes){
    for (Shape s: shapes) {
        s.draw(this);
    }
}

Welchen Weg soll ich benutzen? Ist ein Platzhalter in diesem Fall nützlich?

Tony Le
quelle
? ist eine Kurzschreibweise; Der interne Compiler ersetzt es ohnehin durch einen Typparameter. Wenn ein Compilerfehler auftritt, wird anstelle des? -Parameters der Ersatztyp-Parameter angezeigt.
Unbestreitbarer
9
Korrektur: Mein vorheriger Kommentar ist FALSCH. Wildcard ist anspruchsvoller als ich dachte.
unwiderlegbar
@irreputable Obwohl es in der Tat komplizierter ist, ist Ihr Punkt, es durch einen Typparameter zu ersetzen, völlig gültig. Es ist nur eine, die Sie nicht deklarieren können.
Eugene
Wenn wir nur diese beiden Methoden so betrachten, wie sie sind, gibt es keinen Unterschied und nur eine Frage des Stils. Andernfalls (wenn Sie einige allgemeinere Parameter hinzufügen) ändern sich die Dinge
Eugene

Antworten:

133

Es hängt davon ab , was Sie brauchen zu tun. Sie müssen den Parameter für den begrenzten Typ verwenden, wenn Sie Folgendes tun möchten:

public <T extends Shape> void addIfPretty(List<T> shapes, T shape) {
    if (shape.isPretty()) {
       shapes.add(shape);
    }
}

Hier haben wir ein List<T> shapesund ein T shape, deshalb können wir sicher shapes.add(shape). Wenn es deklariert wurde List<? extends Shape>, können Sie NICHT sicher adddarauf zugreifen (weil Sie möglicherweise ein List<Square>und ein haben Circle).

Indem wir einem Parameter mit begrenztem Typ einen Namen geben, haben wir die Möglichkeit, ihn an anderer Stelle in unserer generischen Methode zu verwenden. Diese Informationen sind natürlich nicht immer erforderlich. Wenn Sie also nicht so viel über den Typ wissen müssen (z. B. IhrendrawAll ) , ist nur ein Platzhalter ausreichend.

Auch wenn Sie nicht erneut auf den Parameter für den begrenzten Typ verweisen, ist ein Parameter für den begrenzten Typ erforderlich, wenn Sie mehrere Grenzen haben. Hier ist ein Zitat aus den Java Generics-FAQs von Angelika Langer

Was ist der Unterschied zwischen einer Wildcard-Bindung und einer Typparameter-Bindung?

Ein Platzhalter kann nur eine Grenze haben, während ein Typparameter mehrere Grenzen haben kann. Ein Platzhalter kann eine Unter- oder Obergrenze haben, während es für einen Typparameter keine Untergrenze gibt.

Platzhaltergrenzen und Typparametergrenzen werden häufig verwechselt, da beide als Grenzen bezeichnet werden und teilweise eine ähnliche Syntax aufweisen. […]

Syntax :

  type parameter bound     T extends Class & Interface1 & … & InterfaceN

  wildcard bound  
      upper bound          ? extends SuperType
      lower bound          ? super   SubType

Ein Platzhalter kann nur eine Grenze haben, entweder eine untere oder eine obere Grenze. Eine Liste der Platzhaltergrenzen ist nicht zulässig.

Ein Typparameter kann im Gegensatz dazu mehrere Grenzen haben, aber es gibt keine Untergrenze für einen Typparameter.

Zitate aus Effective Java 2nd Edition, Punkt 28: Verwenden Sie begrenzte Platzhalter, um die API-Flexibilität zu erhöhen :

Verwenden Sie für maximale Flexibilität Platzhaltertypen für Eingabeparameter, die Hersteller oder Verbraucher darstellen. […] PECS steht für Producer- extends, Consumer-super […]

Verwenden Sie keine Platzhaltertypen als Rückgabetypen . Anstatt Ihren Benutzern zusätzliche Flexibilität zu bieten, würden sie gezwungen, Platzhaltertypen im Clientcode zu verwenden. Bei ordnungsgemäßer Verwendung sind Platzhaltertypen für Benutzer einer Klasse nahezu unsichtbar. Sie veranlassen Methoden, die Parameter zu akzeptieren, die sie akzeptieren sollten, und diejenigen abzulehnen, die sie ablehnen sollten. Wenn der Benutzer der Klasse über Platzhaltertypen nachdenken muss, stimmt wahrscheinlich etwas mit der API der Klasse nicht .

Unter Anwendung des PECS-Prinzips können wir nun zu unserem addIfPrettyBeispiel zurückkehren und es flexibler gestalten, indem wir Folgendes schreiben:

public <T extends Shape> void addIfPretty(List<? super T> list, T shape) { … }

Jetzt können wir addIfPrettysagen, a Circle, zu a List<Object>. Dies ist offensichtlich typsicher, und dennoch war unsere ursprüngliche Erklärung nicht flexibel genug, um dies zuzulassen.

Verwandte Fragen


Zusammenfassung

  • Verwenden Sie Parameter / Platzhalter mit begrenztem Typ, um die Flexibilität Ihrer API zu erhöhen
  • Wenn der Typ mehrere Parameter erfordert, haben Sie keine andere Wahl, als einen begrenzten Typparameter zu verwenden
  • Wenn für den Typ eine Untergrenze erforderlich ist, haben Sie keine andere Wahl, als einen begrenzten Platzhalter zu verwenden
  • "Produzenten" haben Obergrenzen, "Verbraucher" Untergrenzen
  • Verwenden Sie bei Rückgabetypen keinen Platzhalter
Polygenschmierstoffe
quelle
Vielen Dank, Ihre Erklärung ist sehr klar und lehrreich
Tony Le
1
@Tony: Das Buch enthält auch eine kurze Diskussion darüber, wie Sie zwischen Platzhalter- und Typparametern wählen können, wenn keine Bindung besteht. Wenn der Typparameter in einer Methodendeklaration nur einmal vorkommt, verwenden Sie im Wesentlichen einen Platzhalter. Siehe auch das reverse(List<?>)Beispiel von JLS java.sun.com/docs/books/jls/third_edition/html/…
Polygenelubricants
Ich erhalte die UnsupportedOperationException für den folgenden Code: <code> public static <T erweitert Number> void add (List <? Super T> list, T num) {list.add (num); } </ code>
Samra
1
Außerdem können Sie nicht ?als Methodenparameter übergeben, doWork(? type)da Sie diesen Parameter dann nicht in der Methode verwenden können. Sie müssen den Typparameter verwenden.
Tomasz Mularczyk
1
@VivekVardhan Sie können eine solche Methode nicht mit Platzhaltern erstellen, das ist der Punkt. Bei der Entscheidung, welche verwendet werden soll, ist dies eine der besten Erklärungsquellen.
Eugene
6

In Ihrem Beispiel müssen Sie T nicht wirklich verwenden, da Sie diesen Typ nirgendwo anders verwenden.

Aber wenn Sie so etwas gemacht haben:

public <T extends Shape> T drawFirstAndReturnIt(List<T> shapes){
    T s = shapes.get(0);
    s.draw(this);
    return s;
}

oder wie Polygenschmierstoffe sagten, wenn Sie den Typparameter in der Liste mit einem anderen Typparameter abgleichen möchten:

public <T extends Shape> void mergeThenDraw(List<T> shapes1, List<T> shapes2) {
    List<T> mergedList = new ArrayList<T>();
    mergedList.addAll(shapes1);
    mergedList.addAll(shapes2);
    for (Shape s: mergedList) {
        s.draw(this);
    }
}

Im ersten Beispiel erhalten Sie etwas mehr Typensicherheit, als nur Shape zurückzugeben, da Sie das Ergebnis dann an eine Funktion übergeben können, die möglicherweise ein Kind von Shape übernimmt. Zum Beispiel können Sie eine übergebenList<Square> an meine Methode übergeben und dann das resultierende Quadrat an eine Methode übergeben, die nur Quadrate verwendet. Wenn Sie '?' Sie müssten die resultierende Form auf Quadrat umwandeln, was nicht typsicher wäre.

Im zweiten Beispiel stellen Sie sicher, dass beide Listen denselben Typparameter haben (was Sie mit '?' Nicht tun können, da jedes '?' Unterschiedlich ist), damit Sie eine Liste erstellen können, die alle Elemente aus beiden enthält .

Andrei Fierbinteanu
quelle
Ich glaube Shape s = ...sollte sein T s = ..., sonst return s;sollte nicht kompilieren.
Polygenschmierstoffe
1

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 gibt es nicht im Buch.

Chammu
quelle
1

Soweit ich weiß, ermöglicht der Platzhalter einen präziseren Code in Situationen, in denen ein Typparameter nicht erforderlich ist (z. B. weil auf mehrere Stellen verwiesen wird oder weil mehrere Grenzen erforderlich sind, wie in anderen Antworten beschrieben).

In dem Link, den Sie angeben, habe ich (unter "Generische Methoden") die folgenden Aussagen gelesen, die in diese Richtung weisen:

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

Der zweite Weg ist etwas ausführlicher, aber Sie können sich darauf beziehen T:

for (T shape : shapes) {
    ...
}

Soweit ich weiß, ist das der einzige Unterschied.

Nikita Rybak
quelle