Der Zweck dieses Artikels ist es, Java-Generika einzuführen, damit jeder ein endgültiges, klares und genaues Verständnis für alle Aspekte von Java-Generika verbringen kann und auch die Grundlage für den nächsten Artikel "Wiederverwaltung von Java Reflection".
Einführung
Generika sind ein sehr wichtiger Wissenspunkt in Java. Generika werden in Java Collection Frameworks häufig verwendet. In diesem Artikel werden wir uns mit dem Design von Java -Generika von Grund auf neu befassen, bei dem die Wildcard -Verarbeitungs- und Bedrängnislöschung beinhaltet.
Generische Grundlagen
Generische Klassen
Lassen Sie uns zunächst eine einfache Boxklasse definieren:
öffentliche Klassenbox {private String -Objekt; public void set (String -Objekt) {this.Object = Object; } public String get () {return object; }}Dies ist die häufigste Praxis. Eines der Nachteile davon ist, dass nur String-Elemente in die Box geladen werden können. Wenn wir in Zukunft andere Arten von Elementen wie Ganzzahl laden müssen, müssen wir auch eine andere Box neu schreiben. Der Code kann nicht wiederverwendet werden und die Verwendung von Generika kann dieses Problem gut lösen.
öffentliche Klassenbox <T> {// t steht für "Typ" privat t t; public void set (t t) {this.t = t; } public t get () {return t; }}Auf diese Weise kann unsere Boxklasse wiederverwendet werden, und wir können T durch jeden gewünschten Typ ersetzen:
Box <Neger> IntegerBox = New Box <Integer> (); Box <Double> doubleBox = New Box <Double> (); Box <String> Stringbox = New Box <String> ();
Generische Methoden
Lassen Sie uns nach dem Lesen der generischen Klasse über generische Methoden erfahren. Eine generische Methode zu deklarieren ist einfach. Fügen Sie dem Rückgabetyp einfach ein Formular hinzu, das <k, v> ähnlich ist:
public class util {public static <k, v> boolean compare (pair <k, v> p1, pair <k, v> p2) {return p1.getkey (). Equals (p2.getkey ()) && p1.getValue (). Equals (p2.getValue ()); }} public classpaar <k, v> {private k key; privater V -Wert; public pair (k key, v value) {this.key = key; this.Value = Wert; } public void setKey (k key) {this.key = key; } public void setValue (V -Wert) {this.Value = value; } public k getKey () {return key; } public v getValue () {Rückgabewert; }}Wir können allgemeine Methoden wie diese aufrufen:
Paar <Integer, String> p1 = neues Paar <> (1, "Apple"); Paar <Integer, String> p2 = neues Paar <> (2, "birne"); boolean gleich = util. <Integer, String> Compare (p1, p2);
Oder verwenden Sie Typ -Inferenz in Java 1.7/1.8, um Java automatisch die entsprechenden Typparameter abzuleiten:
Paar <Integer, String> p1 = neues Paar <> (1, "Apple"); Paar <Integer, String> p2 = neues Paar <> (2, "birne"); boolean gleich = util.comPare (p1, p2);
Grenzsymbol
Jetzt möchten wir eine solche Funktion implementieren, um die Anzahl der Elemente in einem generischen Array zu finden, die größer als ein bestimmtes Element sind. Wir können es so implementieren:
public static <t> int countGreaterthan (t [] anarray, t elem) {int count = 0; für (t e: anarray) if (e> elem) // Compiler -Fehler ++ Count; Rückgabezahl;}Dies ist jedoch offensichtlich falsch, denn außer bei primitiven Typen wie kurz, int, doppelten, langen, float, byte, char usw. verwenden andere Klassen möglicherweise nicht unbedingt Operatoren>, daher meldet der Compiler einen Fehler. Wie löst ich dieses Problem? Die Antwort ist, das Grenzsymbol zu verwenden.
public interface vergleichbar <t> {public int vergleicheto (t o);}Erstellen Sie eine ähnliche Deklaration wie folgt, die dem Compiler entspricht, dass der Typ -Parameter t Klassen darstellt, die die vergleichbare Schnittstelle implementieren, was dem Compiler entspricht, dass sie alle zumindest die Vergleichsmethode implementieren.
public static <t erweitert vergleichbar <t >> int countGreaterthan (t [] anarray, t elem) {int count = 0; für (t e: anarray) if (e.comPareto (elem)> 0) ++ count; Rückgabezahl;}Wildcard
Vor dem Verständnis der Wildcards müssen wir zunächst ein Konzept klären oder die oben definierte Boxklasse ausleihen. Angenommen, wir fügen eine Methode wie diese hinzu:
public void boxtest (Box <Nummer> n) { / * ... * /}Welche Art von Parametern erlaubt das Akzeptieren von Kästchen <Nummer> n? Können wir in Box <Ganzzahl> oder Box <Double> übergeben? Die Antwort ist nein. Obwohl Ganzzahl und Double Zahlenunterklassen sind, gibt es keine Beziehung zwischen Box <Ganzzahl> oder Box <Double> und Box <Nummer> in den Generika. Dies ist sehr wichtig und wir werden ein vollständiges Beispiel verwenden, um unser Verständnis zu vertiefen.
Erstens definieren wir einige einfache Klassen und werden sie unten verwenden:
Klassenfrucht {} Klasse Apfel erweitert Frucht {} Klasse Orange erweitert Frucht {}Im folgenden Beispiel erstellen wir einen generischen Klassenleser und dann in F1 (), wenn wir Frucht f = Fruchtreader.readExact (Äpfel) probieren. Der Compiler meldet einen Fehler, da es keine Beziehung zwischen List <fruit> und List <Apfel> gibt.
public class GenericReading {statische Liste <Apfel> apples = arrays.aslist (New Apple ()); statische Liste <fruit> fruit = arrays.aslist (New Fruit ()); statischer Klassenleser <T> {t ReadExact (Liste <T> Liste) {return list.get (0); }} static void f1 () {reader <fruit> fruitReader = new reader <fruit> (); // Fehler: List <Früte> kann nicht auf die Liste <Apfel> angewendet werden. // frucht f = fruitreader.readexact (apples); } public static void main (String [] args) {f1 (); }}Nach unseren üblichen Denkgewohnheiten muss es jedoch eine Verbindung zwischen Apfel und Obst geben, aber der Compiler kann es nicht erkennen. Wie kann ich dieses Problem im generischen Code lösen? Wir können dieses Problem mit Wildcards lösen:
Statische Klasse CovarianTreader <T> {t ReadCovariant (Liste <? Erweitert T> Liste) {return list.get (0); }} statische Void f2 () {CovarianTreader <fruit> Fruitreader = neuer CovarianTreader <fruit> (); Frucht F = Fruchtreader.readCovariante (Frucht); Frucht A = FruitReader.readCovariant (Äpfel);} öffentliche statische void main (String [] args) {f2 ();}Dies ähnelt dem mitgeteiltem Compiler sehr, dass die von der Readcovariante -Methode des Fruitslteer akzeptierten Parameter so lang sind wie die Unterklasse, die Früchte (einschließlich Frucht selbst) erfüllt, so dass die Beziehung zwischen der Unterklasse und der Elternklasse ebenfalls zugeordnet ist.
PECS -Prinzipien
Wir haben eine ähnliche Verwendung wie <? erweitert t> oben. Mit der Verwendung können wir Elemente aus der Liste erhalten. Können wir also Elemente in die Liste hinzufügen? Versuchen wir es:
public class Generics undCovarianz {public static void main (String [] args) {// Wildcards erlauben Kovarianz: Liste <? erweitert Frucht> Flist = New ArrayList <Apfel> (); // Fehler kompilieren: Fügen Sie keine Art von Objekt hinzu: // flist.add (neuer Apple ()) // flist.add (new Orange () // flist.add (new fruit () // flist.add (new Object ()) flist.add (null); // legal, aber uninteressant // Wir wissen, dass es mindestens Frucht zurückgibt: Frucht F = Flist.get (0); }}Die Antwort lautet Nein, der Java -Compiler erlaubt uns nicht, dies zu tun. Warum? Wir könnten dieses Problem aus der Perspektive des Compilers genauso gut berücksichtigen. Weil Liste <? Erweitert Obst> Flist kann viele Bedeutungen haben:
Liste <? erweitert Frucht> flist = new ArrayList <fruit> (); Liste <? erweitert fruit> flist = new ArrayList <Apfel> (); Liste <? erweitert Frucht> flist = new ArrayList <Orange> ();
Daher für Sammelklassen, die <? Erweitert T>, sie können nur als Produzent angesehen werden, das (Get) Element nach außen zur Verfügung stellt, und können nicht als Verbraucher verwendet werden, um Elemente nach außen zu erhalten.
Was sollen wir tun, wenn wir das Element hinzufügen wollen? Sie können <? Super t>:
public class genericwriting {static list <Apple> apples = new ArrayList <Apfel> (); statische Liste <fruit> fruit = new ArrayList <fruit> (); static <T> void writeExact (Liste <T> list, t item) {list.add (item); } static void f1 () {writeExact (apples, new Apple ()); WriteExact (Frucht, neuer Apfel ()); } static <T> void writeWithWildCard (Liste <? Super t> liste, t item) {list.add (item)} static void f2 () {writeWithWildCard (apples, new Apple ()); WriteWithWildCard (Frucht, neuer Apple ()); } public static void main (String [] args) {f1 (); f2 (); }}Auf diese Weise können wir dem Container Elemente hinzufügen, aber der Nachteil der Verwendung von Super ist, dass wir in Zukunft keine Elemente im Container erhalten können. Der Grund ist sehr einfach. Wir betrachten dieses Problem weiterhin aus der Perspektive des Compilers. Für die Liste <? Super Apple> Liste, sie kann die folgenden Bedeutungen haben:
Liste <? Super Apple> list = new ArrayList <Apfel> (); Liste <? Super Apple> list = new ArrayList <Frünte> (); Liste <? Super Apple> list = new ArrayList <Object> ();
Wenn wir versuchen, einen Apfel über die Liste zu bringen, erhalten wir möglicherweise eine Frucht, die andere Fruchtarten wie Orange sein kann.
Basierend auf dem obigen Beispiel können wir eine Regel "Produzent erweitert, Verbraucher Super" zusammenfassen:
Nachdem wir einige Java -Sammlungs -Quellcode gelesen haben, können wir feststellen, dass wir die beiden normalerweise zusammen verwenden, z. B. folgende:
öffentliche Klassensammlungen {public static <T> void Copy (Liste <? Super t> dest, list <? erweitert t> src) {für (int i = 0; i <src.size (); i ++) dest.set (i, src.get (i)); }}Geben Sie Löschen ein
Das vielleicht beunruhigendste an Java Generics ist die Typ -Löschung, insbesondere für Programmierer mit C ++ - Erfahrung. Das Typ -Löschen bedeutet, dass Java -Generika während der Kompilierung nur für die statische Überprüfung verwendet werden kann, und dann löscht der vom Compiler generierte Code die entsprechenden Typinformationen. Auf diese Weise kennt der JVM während des Laufs tatsächlich den vom Generikum dargestellten spezifischen Typ. Der Zweck davon ist, dass Java -Generika nach 1,5 eingeführt wurden. Um die Abwärtskompatibilität aufrechtzuerhalten, können Sie nur das Löschen durchführen, um mit früheren nicht generischen Code kompatibel zu sein. Wenn Sie den Quellcode des Java -Sammlungs -Frameworks lesen, können Sie feststellen, dass einige Klassen Generika nicht tatsächlich unterstützen.
Was bedeutet generisches Löschen so viel? Schauen wir uns zunächst das folgende einfache Beispiel an:
public class Node <t> {private t data; private Knoten <t> Weiter; öffentlicher Knoten (t Data, Knoten <T> Weiter)} this.data = data; this.Next = Weiter; } public t getData () {returndaten; } // ...}Nachdem der Compiler die entsprechende Typprüfung abgeschlossen hat, wird der obige Code tatsächlich konvertiert in:
öffentliche Klasse -Knoten {private Objektdaten; Privatknoten als nächstes; öffentlicher Knoten (Objektdaten, Knoten als nächstes) {this.data = data; this.Next = Weiter; } öffentliches Objekt getData () {returndaten; } // ...}Dies bedeutet, dass die JVM während der Laufzeit, unabhängig davon, ob wir Knoten <string> oder Knoten <Gefteger> deklarieren. Gibt es eine Möglichkeit, dieses Problem zu lösen? Dies erfordert, dass wir die Grenzen selbst zurücksetzen und den obigen Code an Folgendes ändern:
öffentliche Klasse -Knoten <T erweitert vergleichbar <t >> {private t data; private Knoten <t> Weiter; public node (t data, node <t> next) {this.data = data; this.Next = Weiter; } public t getData () {returndaten; } // ...}Auf diese Weise ersetzt der Compiler die Stelle, an der T mit vergleichbar anstelle des Standardobjekts erscheint:
public class Node {private vergleichbare Daten; Privatknoten als nächstes; öffentlicher Knoten (vergleichbare Daten, Knoten als nächstes) {this.data = data; this.Next = Weiter; } public vergleichbar getData () {returndaten; } // ...}Das obige Konzept mag einfacher zu verstehen sein, aber in der Tat bringt das generische Löschen weit mehr Probleme. Schauen wir uns als nächstes einen systematischen Blick auf einige der Probleme, die durch Typlöschung mitgebracht wurden. Einige Probleme werden möglicherweise nicht in C ++ - Generika auftreten, aber Sie müssen in Java besonders vorsichtig sein.
Frage 1
Generische Arrays sind in Java nicht erlaubt. Wenn der Compiler so etwas wie folgt macht, wird ein Fehler angegeben:
List <Ganzzahl> [] arrayoflists = new List <Integer> [2]; // Compile-Zeit-Fehler
Warum unterstützt der Compiler die obige Praxis nicht? Verwenden Sie weiterhin umgekehrtes Denken, wir betrachten dieses Problem aus der Perspektive des Compilers.
Schauen wir uns zunächst das folgende Beispiel an:
Object [] Strings = New String [2]; Strings [0] = "Hi"; // okstrings [1] = 100; // Eine ArrayStoreException wird geworfen.
Der obige Code ist leicht zu verstehen. String -Arrays können keine ganzzahligen Elemente speichern, und solche Fehler müssen häufig entdeckt werden, bis der Code ausgeführt wird, und der Compiler kann sie nicht erkennen. Schauen wir uns als nächstes an, was passieren wird, wenn Java die Schaffung generischer Arrays unterstützt:
Object [] stringlists = new List <string> []; // Compiler -Fehler, aber so tun Sie vor, dass StringLists [0] = New ArrayList <String> () erlaubt ist; // ok // eine ArrayStoreException sollte geworfen werden, aber die Laufzeit kann es nicht erkennen.
Angenommen, wir unterstützen die Schaffung generischer Arrays. Da die Typinformationen während der Laufzeit gelöscht wurden, kennt der JVM den Unterschied zwischen New ArrayList <String> () und New ArrayList <GanzEger> () überhaupt nicht. Wenn solche Fehler in praktischen Anwendungsszenarien auftreten, ist es sehr schwer zu erkennen.
Wenn Sie dem immer noch skeptisch sind, können Sie versuchen, den folgenden Code auszuführen:
öffentliche Klasse ERUSDYTYPEEQUIVALECE {public static void main (String [] args) {Klasse c1 = new ArrayList <string> (). getClass (); Klasse C2 = New ArrayList <Ganzzahl> (). GetClass (); System.out.println (C1 == C2); // WAHR }}Frage 2
Verwenden Sie weiterhin unsere Knotenklasse oben wieder. Für generische Code hilft uns der Java -Compiler heimlich heimlich eine Brückenmethode.
public class Node <t> {public t Data; public node (t data) {this.data = data; } public void setData (t data) {System.out.println ("node.setData"); this.data = Daten; }} public class myNode erweitert den Knoten <Integer> {public myNode (Integer -Daten) {Super (Daten); } public void setData (Integer -Daten) {System.out.println ("mynode.setData"); Super.setData (Daten); }}Nachdem Sie die obige Analyse gelesen haben, denken Sie möglicherweise, dass der Compiler nach dem Löschen des Typs den Knoten und MyNode in Folgendes verwandelt:
öffentliche Klasse Node {öffentliche Objektdaten; öffentlicher Knoten (Objektdaten) {this.data = data; } public void setData (Objektdaten) {System.out.println ("node.setData"); this.data = Daten; }} public class myNode erweitert den Knoten {public myNode (Integer data) {Super (Data); } public void setData (Integer -Daten) {System.out.println ("mynode.setData"); Super.setData (Daten); }}Tatsächlich ist dies nicht der Fall. Schauen wir uns zunächst den folgenden Code an. Wenn dieser Code ausgeführt wird, wird eine ClassCastException geworfen, und fordert die String nicht in Ganzzahl um.
Mynode mn = neuer mynode (5); Knoten n = mn; // ein roher Typ - Compiler wirft einen ungeprüften Warningn.setData ("Hallo") aus; // führt dazu, dass eine ClassCastException geworfen wird.Wenn wir dem oben generierten Code folgen, sollten wir beim Ausführen von Zeile 3 keinen Fehler melden (beachten Sie, dass ich Zeile 4 kommentiert habe), da die Methode SetData (String Data) in MyNode nicht vorhanden ist, sodass wir nur die SETDATA -Methode (Objektdaten) des übergeordneten Klassenknotens aufrufen können. Auf diese Weise sollte der obige Code der obigen Zeile 3 keinen Fehler melden, da natürlich Zeichenfolge in Objekt konvertiert werden kann. Wie wird die ClassCastException geworfen?
Tatsächlich behandelt der Java -Compiler automatisch den obigen Code:
Klasse myNode erweitert den Knoten {// Brückenmethode, das vom Compiler Public void setData (Objektdaten) {setData ((Integer) -Daten) generiert wird; } public void setData (Integer -Daten) {System.out.println ("mynode.setData"); Super.setData (Daten); } // ...}Aus diesem Grund wird der obige Fehler gemeldet. Bei SetData ((Integer) Daten; String kann nicht in Ganzzahl konvertiert werden. Wenn der Compiler in Zeile 2 oben nicht überprüft wird, können wir ihn nicht ignorieren, sonst müssen wir bis zur Laufzeit warten, um die Ausnahme zu finden. Es wäre großartig, wenn wir zu Beginn den Knoten <Integer> n = mn hinzufügen, damit der Compiler uns helfen kann, Fehler im Voraus zu finden.
Frage 3
Wie oben erwähnt, kann Java Generics nur in hohem Maße eine statische Überprüfung durchführen, und dann werden die Typinformationen gelöscht, sodass der Compiler nicht die folgende Methode zur Verwendung von Typparametern zum Erstellen von Instanzen übergibt:
public static <e> void append (list <e> list) {e elem = new e (); // Compile-Zeit-Fehlerliste.Add (Elem);}Aber was sollten wir tun, wenn wir in bestimmten Szenarien Instanzen mit Typparametern erstellen möchten? Reflexion kann verwendet werden, um dieses Problem zu lösen:
public static <e> void append (list <e> list, class <e> cls) löst Ausnahme aus {e elem = cls.newinstance (); // ok list.add (elem);}Wir können es so nennen:
Liste <String> ls = new ArrayList <> (); append (ls, string.class);
In der Tat können Sie für das obige Problem auch Werks- und Vorlagenentwurfsmuster verwenden, um es zu lösen. Interessierte Freunde möchten sich vielleicht die Erklärung der Erstellung von Instanz von Typen in Kapitel 15 in Java ansehen. Wir werden hier nicht darauf eingehen.
Frage 4
Wir können das Schlüsselwort des Instanzschlüssels nicht direkt für den generischen Code verwenden, da der Java -Compiler beim Generieren des Codes alle relevanten generischen Typinformationen löscht, so wie die oben überprüften JVM den Unterschied zwischen ArrayList <Gector> und ArrayList <String <string> während der Laufzeit nicht erkennen können:
public static <e> void rtti (list <e> list) {if (listinstanceOf arrayList <Gesinger>) {// Compile-Time-Fehler // ...}} => {ArrayList <Ganzeger>, ArrayList <String <string>, LinkedList <Scharakter>, ...}Wie oben können wir Platzhalter verwenden, um Grenzen zurückzusetzen, um dieses Problem zu lösen:
public static void rtti (list <?> list) {if (listinstanceof arrayList <>) {// ok; Instanz benötigt einen typischen Typ // ...}}Zusammenfassen
In diesem Artikel geht es darum, Java-Generika wiederzuverständlich. Ich hoffe, es wird für alle hilfreich sein. Interessierte Freunde können weiterhin auf diese Seite verweisen:
Detaillierte Erklärung der Grundlagen von Java Array
Die Grundlagen der Java -Programmierung: Nachahmung der Nutzungs -Legin -Code -Freigabe von Benutzern
Die Grundlagen der Java-Netzwerkprogrammierung: Einweg-Kommunikation
Wenn es Mängel gibt, hinterlassen Sie bitte eine Nachricht, um darauf hinzuweisen. Vielen Dank an Freunde für Ihre Unterstützung für diese Seite!