JavaRush /Java-Blog /Random-DE /Die Theorie der Generika in Java oder wie man Klammern in...
Viacheslav
Level 3

Die Theorie der Generika in Java oder wie man Klammern in der Praxis setzt

Veröffentlicht in der Gruppe Random-DE

Einführung

Ab JSE 5.0 wurden Generika zum Java-Spracharsenal hinzugefügt.
Die Theorie der Generika in Java oder wie man Klammern in der Praxis setzt - 1

Was sind Generika in Java?

Generics (Verallgemeinerungen) sind spezielle Mittel der Java-Sprache zur Implementierung verallgemeinerter Programmierung: ein spezieller Ansatz zur Beschreibung von Daten und Algorithmen, der es Ihnen ermöglicht, mit verschiedenen Datentypen zu arbeiten, ohne deren Beschreibung zu ändern. Auf der Oracle-Website ist den Generika ein eigenes Tutorial gewidmet: „ Lektion: Generics “.

Um Generika zu verstehen, müssen Sie zunächst verstehen, warum sie überhaupt benötigt werden und was sie bieten. Im Tutorial im Abschnitt „ Warum Generics verwenden ?“ Es wird gesagt, dass einer der Zwecke eine stärkere Typprüfung zur Kompilierungszeit und die Eliminierung der Notwendigkeit einer expliziten Umwandlung ist.
Die Theorie der Generika in Java oder wie man Klammern in der Praxis setzt - 2
Bereiten wir unseren Lieblings- Tutorialpoint-Online-Java-Compiler für Experimente vor . Stellen wir uns diesen Code vor:
import java.util.*;
public class HelloWorld{
	public static void main(String []args){
		List list = new ArrayList();
		list.add("Hello");
		String text = list.get(0) + ", world!";
		System.out.print(text);
	}
}
Dieser Code wird einwandfrei funktionieren. Aber was wäre, wenn sie zu uns kämen und sagen würden, dass der Satz „Hallo Welt!“ geschlagen und du kannst nur zurückgeben Hallo? Entfernen wir die Verkettung mit der Zeichenfolge aus dem Code ", world!". Was könnte harmloser sein? Tatsächlich erhalten wir jedoch WÄHREND DER KOMPILIERUNG eine Fehlermeldung : error: incompatible types: Object cannot be converted to String In unserem Fall speichert List eine Liste von Objekten vom Typ Object. Da String ein Nachkomme von Object ist (da alle Klassen implizit von Object in Java geerbt werden), ist eine explizite Umwandlung erforderlich, die wir nicht durchgeführt haben. Und beim Verketten wird die statische Methode String.valueOf(obj) für das Objekt aufgerufen, die letztendlich die toString-Methode für das Objekt aufruft. Das heißt, unsere Liste enthält ein Objekt. Es stellt sich heraus, dass wir die Typumwandlung selbst durchführen müssen, wenn wir einen bestimmten Typ und nicht Object benötigen:
import java.util.*;
public class HelloWorld{
	public static void main(String []args){
		List list = new ArrayList();
		list.add("Hello!");
		list.add(123);
		for (Object str : list) {
		    System.out.println((String)str);
		}
	}
}
In diesem Fall jedoch, weil List akzeptiert eine Liste von Objekten und speichert nicht nur String, sondern auch Integer. Aber das Schlimmste ist, dass der Compiler in diesem Fall nichts Falsches erkennt. Und hier erhalten wir WÄHREND DER AUSFÜHRUNG einen Fehler (man sagt auch, dass der Fehler „zur Laufzeit“ empfangen wurde). Der Fehler wird sein: java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String Einverstanden, nicht das angenehmste. Und das alles, weil der Compiler keine künstliche Intelligenz ist und nicht alles erraten kann, was der Programmierer meint. Um dem Compiler mehr darüber zu sagen, welche Typen wir verwenden werden, hat Java SE 5 Generics eingeführt . Korrigieren wir unsere Version, indem wir dem Compiler mitteilen, was wir wollen:
import java.util.*;
public class HelloWorld {
	public static void main(String []args){
		List<String> list = new ArrayList<>();
		list.add("Hello!");
		list.add(123);
		for (Object str : list) {
		    System.out.println(str);
		}
	}
}
Wie wir sehen, benötigen wir die Umwandlung in String nicht mehr. Darüber hinaus gibt es jetzt spitze Klammern, die generische Bezeichnungen einrahmen. Jetzt lässt der Compiler nicht zu, dass die Klasse kompiliert wird, bis wir den Zusatz 123 zur Liste entfernen, weil das ist Ganzzahl. Er wird es uns sagen. Viele Leute bezeichnen Generika als „syntaktischen Zucker“. Und sie haben Recht, denn Generika werden bei der Zusammenstellung tatsächlich zu denselben Kasten. Schauen wir uns den Bytecode der kompilierten Klassen an: mit manueller Umwandlung und Verwendung von Generika:
Die Theorie der Generika in Java oder wie man Klammern in der Praxis setzt - 3
Nach der Kompilierung werden alle Informationen zu Generika gelöscht. Dies wird als „Type Erasure“ oder „ Type Erasure “ bezeichnet. Typlöschung und Generika sind darauf ausgelegt, Abwärtskompatibilität mit älteren Versionen des JDK zu gewährleisten und gleichzeitig dem Compiler die Unterstützung bei der Typinferenz in neueren Java-Versionen zu ermöglichen.
Die Theorie der Generika in Java oder wie man Klammern in der Praxis setzt - 4

Rohtypen oder Rohtypen

Wenn wir über Generika sprechen, haben wir immer zwei Kategorien: typisierte Typen (Generic Types) und „Rohtypen“ (Raw Types). Rohtypen sind Typen ohne Angabe der „Qualifikation“ in spitzen Klammern:
Die Theorie der Generika in Java oder wie man Klammern in der Praxis setzt - 5
Typisierte Typen sind das Gegenteil, mit der Angabe „Klärung“:
Die Theorie der Generika in Java oder wie man Klammern in der Praxis setzt - 6
Wie wir sehen können, haben wir ein ungewöhnliches Design verwendet, das im Screenshot mit einem Pfeil markiert ist. Dies ist eine spezielle Syntax, die in Java SE 7 hinzugefügt wurde und „ der Diamant “ heißt , was Diamant bedeutet. Warum? Sie können eine Analogie zwischen der Form einer Raute und der Form von geschweiften Klammern ziehen: Die <> Rautensyntax ist auch mit dem Konzept der „ Typinferenz “ oder Typinferenz verbunden. Schließlich schaut der Compiler, der rechts <> sieht, auf die linke Seite, wo sich die Deklaration des Typs der Variablen befindet, der der Wert zugewiesen wird. Und von diesem Teil aus versteht er, um welchen Typ es sich bei dem Wert auf der rechten Seite handelt. Wenn auf der linken Seite ein Generikum angegeben und auf der rechten Seite nicht angegeben wird, kann der Compiler tatsächlich auf den Typ schließen:
import java.util.*;
public class HelloWorld{
	public static void main(String []args) {
		List<String> list = new ArrayList();
		list.add("Hello World");
		String data = list.get(0);
		System.out.println(data);
	}
}
Dies wäre jedoch eine Mischung aus dem neuen Stil mit Generika und dem alten Stil ohne diese. Und das ist äußerst unerwünscht. Beim Kompilieren des obigen Codes erhalten wir die Meldung: Note: HelloWorld.java uses unchecked or unsafe operations. Tatsächlich scheint unklar, warum wir hier überhaupt Diamanten hinzufügen müssen. Aber hier ist ein Beispiel:
import java.util.*;
public class HelloWorld{
	public static void main(String []args) {
		List<String> list = Arrays.asList("Hello", "World");
		List<Integer> data = new ArrayList(list);
		Integer intNumber = data.get(0);
		System.out.println(data);
	}
}
Wie wir uns erinnern, verfügt ArrayList auch über einen zweiten Konstruktor, der eine Sammlung als Eingabe verwendet. Und hier liegt die Täuschung. Ohne Diamond-Syntax versteht der Compiler nicht, dass er getäuscht wird, mit Diamond jedoch schon. Deshalb Regel Nr. 1 : Verwenden Sie immer die Diamantsyntax, wenn wir typisierte Typen verwenden. Andernfalls besteht die Gefahr, dass wir die Stelle übersehen, an der wir den Rohtyp verwenden. Um Warnungen im Protokoll zu vermeiden, die „ungeprüfte oder unsichere Vorgänge verwenden“, können Sie eine spezielle Anmerkung zur verwendeten Methode oder Klasse angeben: @SuppressWarnings("unchecked") Unterdrücken wird mit „unterdrücken“ übersetzt, was wörtlich bedeutet, Warnungen zu unterdrücken. Aber denken Sie darüber nach, warum Sie sich entschieden haben, es anzugeben? Denken Sie an Regel Nummer eins und vielleicht müssen Sie noch die Eingabe hinzufügen.
Die Theorie der Generika in Java oder wie man Klammern in der Praxis setzt - 7

Generische Methoden

Mit Generika können Sie Methoden eingeben. Dieser Funktion ist im Oracle-Tutorial ein eigener Abschnitt gewidmet: „ Generische Methoden “. Aus diesem Tutorial ist es wichtig, sich die Syntax zu merken:
  • enthält eine Liste typisierter Parameter in spitzen Klammern;
  • Die Liste der typisierten Parameter steht vor der zurückgegebenen Methode.
Schauen wir uns ein Beispiel an:
import java.util.*;
public class HelloWorld{

    public static class Util {
        public static <T> T getValue(Object obj, Class<T> clazz) {
            return (T) obj;
        }
        public static <T> T getValue(Object obj) {
            return (T) obj;
        }
    }

    public static void main(String []args) {
		List list = Arrays.asList("Author", "Book");
		for (Object element : list) {
		    String data = Util.getValue(element, String.class);
		    System.out.println(data);
		    System.out.println(Util.<String>getValue(element));
		}
    }
}
Wenn Sie sich die Util-Klasse ansehen, sehen wir darin zwei typisierte Methoden. Mit der Typinferenz können wir die Typdefinition direkt dem Compiler bereitstellen oder sie selbst angeben. Im Beispiel werden beide Möglichkeiten dargestellt. Übrigens ist die Syntax recht logisch, wenn man darüber nachdenkt. Wenn wir eine Methode eingeben, geben wir das Generikum VOR der Methode an, denn wenn wir das Generikum nach der Methode verwenden, kann Java nicht herausfinden, welcher Typ verwendet werden soll. Deshalb geben wir zunächst bekannt, dass wir generisches T verwenden werden, und sagen dann, dass wir dieses generische T zurückgeben werden. Natürlich Util.<Integer>getValue(element, String.class)wird es mit einem Fehler fehlschlagen incompatible types: Class<String> cannot be converted to Class<Integer>. Wenn Sie typisierte Methoden verwenden, sollten Sie immer an das Löschen von Typen denken. Schauen wir uns ein Beispiel an:
import java.util.*;
public class HelloWorld {

    public static class Util {
        public static <T> T getValue(Object obj) {
            return (T) obj;
        }
    }

    public static void main(String []args) {
		List list = Arrays.asList(2, 3);
		for (Object element : list) {
		    System.out.println(Util.<Integer>getValue(element) + 1);
		}
    }
}
Es wird großartig funktionieren. Aber nur, solange der Compiler versteht, dass die aufgerufene Methode einen Integer-Typ hat. Ersetzen wir die Konsolenausgabe durch die folgende Zeile: System.out.println(Util.getValue(element) + 1); Und wir erhalten die Fehlermeldung: Ungültige Operandentypen für Binäroperator „+“, erster Typ: Objekt, zweiter Typ: int Das heißt, die Typen wurden gelöscht. Der Compiler erkennt, dass niemand den Typ angegeben hat, der Typ ist als Objekt angegeben und die Codeausführung schlägt mit einem Fehler fehl.
Die Theorie der Generika in Java oder wie man Klammern in der Praxis setzt - 8

Generische Typen

Sie können nicht nur Methoden, sondern auch Klassen selbst eingeben. Oracle hat diesem Thema in seinem Handbuch einen Abschnitt „ Generische Typen “ gewidmet. Schauen wir uns ein Beispiel an:
public static class SomeType<T> {
	public <E> void test(Collection<E> collection) {
		for (E element : collection) {
			System.out.println(element);
		}
	}
	public void test(List<Integer> collection) {
		for (Integer element : collection) {
			System.out.println(element);
		}
	}
}
Hier ist alles einfach. Wenn wir eine Klasse verwenden, wird das Generikum nach dem Klassennamen aufgeführt. Lassen Sie uns nun eine Instanz dieser Klasse in der Hauptmethode erstellen:
public static void main(String []args) {
	SomeType<String> st = new SomeType<>();
	List<String> list = Arrays.asList("test");
	st.test(list);
}
Es wird gut funktionieren. Der Compiler sieht, dass es eine Liste mit Zahlen und eine Sammlung vom Typ String gibt. Aber was wäre, wenn wir die Generika löschen und Folgendes tun:
SomeType st = new SomeType();
List<String> list = Arrays.asList("test");
st.test(list);
Wir erhalten die Fehlermeldung: java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer Geben Sie Erasure erneut ein. Da die Klasse kein Generikum mehr hat, entscheidet der Compiler, dass eine Methode mit List<Integer> besser geeignet ist, da wir eine Liste übergeben haben. Und wir fallen mit einem Fehler. Daher Regel Nr. 2: Wenn eine Klasse typisiert ist, geben Sie den Typ immer im generischen an .

Einschränkungen

Wir können eine Einschränkung auf in Generics angegebene Typen anwenden. Beispielsweise möchten wir, dass der Container nur „Zahl“ als Eingabe akzeptiert. Diese Funktion wird im Oracle-Tutorial im Abschnitt „Bounded Type Parameters“ beschrieben . Schauen wir uns ein Beispiel an:
import java.util.*;
public class HelloWorld{

    public static class NumberContainer<T extends Number> {
        private T number;

        public NumberContainer(T number)  { this.number = number; }

        public void print() {
            System.out.println(number);
        }
    }

    public static void main(String []args) {
		NumberContainer number1 = new NumberContainer(2L);
		NumberContainer number2 = new NumberContainer(1);
		NumberContainer number3 = new NumberContainer("f");
    }
}
Wie Sie sehen können, haben wir den generischen Typ auf die Number-Klasse/Schnittstelle und ihre Nachkommen beschränkt. Interessanterweise können Sie nicht nur eine Klasse, sondern auch Schnittstellen angeben. Zum Beispiel: public static class NumberContainer<T extends Number & Comparable> { Generics haben auch das Konzept von Wildcards https://docs.oracle.com/javase/tutorial/java/generics/wildcards.html Sie werden wiederum in drei Typen unterteilt: Für Wildcards gilt das sogenannte Get Put-Prinzip . Sie können in folgender Form ausgedrückt werden:
Die Theorie der Generika in Java oder wie man Klammern in der Praxis setzt - 9
Dieses Prinzip wird auch PECS-Prinzip (Producer Extends Consumer Super) genannt. Weitere Informationen zu Habré finden Sie im Artikel „ Verwendung generischer Platzhalter zur Verbesserung der Benutzerfreundlichkeit der Java-API “ sowie in der hervorragenden Diskussion zum Stackoverflow: „ Verwendung von Platzhaltern in Generics Java “. Hier ist ein kleines Beispiel aus der Java-Quelle – die Collections.copy-Methode:
Die Theorie der Generika in Java oder wie man Klammern in der Praxis setzt - 10
Nun, ein kleines Beispiel dafür, wie es NICHT funktionieren wird:
public static class TestClass {
	public static void print(List<? extends String> list) {
		list.add("Hello World!");
		System.out.println(list.get(0));
	}
}

public static void main(String []args) {
	List<String> list = new ArrayList<>();
	TestClass.print(list);
}
Aber wenn Sie „extends“ durch „super“ ersetzen, ist alles in Ordnung. Da wir die Liste vor der Ausgabe mit einem Wert füllen, ist sie für uns ein Verbraucher, also ein Verbraucher. Deshalb verwenden wir super.

Nachlass

Es gibt noch ein weiteres ungewöhnliches Merkmal von Generika – ihre Vererbung. Die Vererbung von Generika wird im Oracle-Tutorial im Abschnitt „ Generika, Vererbung und Subtypen “ beschrieben. Die Hauptsache ist, sich Folgendes zu merken und zu realisieren. Das können wir nicht machen:
List<CharSequence> list1 = new ArrayList<String>();
Weil die Vererbung bei Generika anders funktioniert:
Die Theorie der Generika in Java oder wie man Klammern in der Praxis setzt - 11
Und hier ist ein weiteres gutes Beispiel, das mit einem Fehler scheitern wird:
List<String> list1 = new ArrayList<>();
List<Object> list2 = list1;
Auch hier ist alles einfach. List<String> ist kein Nachkomme von List<Object>, obwohl String ein Nachkomme von Object ist.

Finale

Also haben wir unsere Erinnerung an Generika aufgefrischt. Werden sie selten in voller Kraft genutzt, fallen manche Details aus dem Gedächtnis. Ich hoffe, dass diese kurze Rezension Ihnen hilft, Ihr Gedächtnis aufzufrischen. Und um bessere Ergebnisse zu erzielen, empfehle ich Ihnen dringend, die folgenden Materialien zu lesen: #Wjatscheslaw
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION