CodeGym /Kurse /JAVA 25 SELF /Einführung in Generics

Einführung in Generics

JAVA 25 SELF
Level 16 , Lektion 4
Verfügbar

1. Einführung

Stellen Sie sich vor, Sie haben eine universelle Schachtel, in die man alles Mögliche legen kann: einen Apfel, ein Buch oder sogar ein Spielzeug. In Java ist eine solche „universelle Schachtel“ eine Klasse, die Daten des allgemeinsten Typs, Object, speichert. Dieser Typ ist der Elterntyp aller anderen Klassen in Java, daher kann man in eine Variable vom Typ Object beliebige Objekte legen.

Stellen Sie sich nun ein Lager mit solchen Schachteln vor. Wenn die Schachteln keine Etiketten haben, können Sie zwar alles hineinlegen, aber wenn es Zeit ist, etwas herauszunehmen, müssen Sie die Schachtel öffnen und raten, was sich darin befindet. Mit Generics ähnelt die Situation einem Lager mit klaren Beschriftungen: „Nur Äpfel“, „Nur Bücher“, „Nur Werkzeuge“. Jetzt wissen Sie immer, was in jeder Schachtel liegt, und Sie können nicht aus Versehen ein Buch in die Schachtel für Äpfel legen.

Auf den ersten Blick scheint das Speichern in Object praktisch: Es ist nicht nötig, separate Klassen für unterschiedliche Datentypen zu erstellen. In der Praxis wird diese „Universalität“ jedoch zum Problem:

  1. Fehler passieren leicht. Sie können versehentlich nicht das Objekt in die Schachtel legen, das Sie erwartet haben.
  2. Der Compiler erkennt das Problem nicht. Er erlaubt Ihnen einfach, alles hineinzulegen, weil der Typ Object das zulässt.
  3. Man muss manuell „auspacken“. Wenn Sie ein Objekt aus einer solchen Schachtel herausnehmen, hat es wieder den Typ Object, und Sie müssen es selbst auf den benötigten Typ umwandeln (das nennt man Typumwandlung oder cast). Und wenn Sie sich beim Typ vertun, beendet das Programm einfach seine Arbeit mit einem Fehler!

Schauen wir uns das an einem einfachen Beispiel an.

class Box {
    private Object value;

    public void set(Object value) {
        this.value = value;
    }

    public Object get() {
        return value;
    }
}

Nun verwenden wir diese Schachtel:

Box box = new Box();
box.set("Hallo"); // Eine Zeichenkette eingelegt
String s = (String) box.get(); // Zeichenkette herausgeholt, alles gut

box.set(123); // Eine Zahl eingelegt
// Der Compiler sieht kein Problem...
String t = (String) box.get(); // Laufzeitfehler!

Wie Sie sehen, hat der Compiler den Code anstandslos durchgelassen, der letztlich zu einem Fehler führte. Wir haben vom Problem erst erfahren, als das Programm gestartet wurde und „abstürzte“.

2. Die Lösung – Generics

Generics (Generika) sind eine Möglichkeit, dieses Problem zu lösen. Es ist eine spezielle Syntax, die erlaubt, eine Klasse oder Methode bereits zur Compile-Zeit an einen konkreten Datentyp zu binden. Vereinfacht gesagt ist es wie ein Aufkleber auf der Schachtel, der sagt: „In dieser Schachtel dürfen nur Strings liegen“ oder „In dieser Schachtel dürfen nur Zahlen liegen“.

Damit erhalten wir Typsicherheit: Der Compiler lässt uns kein falsches Objekt in die „Schachtel“ legen. Er prüft unseren Code und weist auf Fehler hin, bevor wir das Programm starten.

Dieselbe Klasse Box, aber jetzt mit Generics:

class Box<Type> {
    private Type value;

    public void set(Type value) {
        this.value = value;
    }

    public Type get() {
        return value;
    }
}

Hier ist Type ein Typparameter. Das ist ein Platzhaltername, den wir selbst wählen (üblich sind T, E, K, V), und er sagt: „Wenn wir eine Box erstellen, geben wir an, mit welchem Typ sie arbeitet, und ich verwende diesen Typ überall dort, wo im Code Type steht.“

3. Verwendung von Generics

Wenn wir ein Objekt einer Klasse mit Generics erzeugen, geben wir den konkreten Typ in spitzen Klammern <...> an.

// Eine Schachtel erstellt, die nur mit Strings arbeitet
Box<String> stringBox = new Box<>();
stringBox.set("Hallo, Welt!"); // OK, String eingelegt
String s = stringBox.get(); // String ohne Cast entnommen

stringBox.set(123); // Kompilierfehler! Der Compiler lässt das nicht zu.

Wenn wir eine Box<Integer> erstellen, achtet der Compiler darauf, dass nur Zahlen hineingelegt werden:

// Eine Schachtel erstellt, die nur mit Zahlen arbeitet
Box<Integer> intBox = new Box<>();
intBox.set(42); // OK, Zahl eingelegt
Integer number = intBox.get(); // Zahl ohne Cast entnommen

intBox.set("Hallo"); // Kompilierfehler!

Jetzt weiß der Compiler genau, welcher Datentyp in jeder Schachtel liegen soll, und schützt uns zuverlässig vor Fehlern.

4. So funktioniert das an Beispielen

Generics kann man nicht nur in Klassen, sondern auch in Methoden verwenden. Das erlaubt sehr flexiblen und universellen Code.

Beispiel 1 – Klasse mit Generics

Angenommen, wir brauchen eine Klasse Pair, die zwei Objekte desselben Typs speichert. Mit Generics sieht das so aus:

class Pair<T> {
    private T first;
    private T second;

    public Pair(T first, T second) {
        this.first = first;
        this.second = second;
    }

    public T getFirst() {
        return first;
    }

    public T getSecond() {
        return second;
    }
}

Verwendung:

Pair<String> greetings = new Pair<String>("Hallo", "Welt");
System.out.println(greetings.getFirst() + " " + greetings.getSecond());

Pair<Integer> numbers = new Pair<Integer>(10, 20);
System.out.println(numbers.getFirst() + numbers.getSecond());

Im ersten Fall ist sich der Compiler sicher, dass in greetings Strings liegen, im zweiten – Zahlen.

Beispiel 2 – generische Methode

Wir können eine Methode schreiben, die mit jedem Datentyp arbeitet.

class Utils {
    // <T> vor void sagt, dass die Methode mit einem Typparameter T arbeitet
    public static <T> void printTwice(T value) {
        System.out.println(value);
        System.out.println(value);
    }
}

Nun können wir diese Methode mit jedem Datentyp aufrufen, und sie verhält sich gleich:

Utils.printTwice("Java");
Utils.printTwice(123);
Utils.printTwice(3.14);

5. Vorteile von Generics

Generics sind nicht nur syntaktischer Zucker, sondern ein mächtiges Werkzeug, das reale Probleme in der Entwicklung löst. Sehen wir uns drei zentrale Vorteile an.

Typsicherheit ist der wichtigste Vorteil von Generics. Ohne sie kann der Compiler nicht prüfen, ob Sie Datentypen in Ihren „universellen“ Klassen und Methoden korrekt verwenden. Fehler wie der Versuch, einen String in eine „Zahlenschachtel“ zu legen, werden erst zur Laufzeit entdeckt, wenn das Programm plötzlich mit einer Ausnahme ClassCastException „abstürzt“. Mit Generics achtet der Compiler strikt auf Typen. Er prüft, dass Sie in Box<String> nur Strings und in Box<Integer> nur Zahlen legen. Wenn Sie etwas Falsches versuchen, weist er sofort darauf hin, und Sie können es beheben, bevor das Programm startet. Das macht Ihren Code wesentlich zuverlässiger und vorhersehbarer.

Sauberer Code (ohne überflüssige Casts). Erinnern Sie sich, wie der Code mit unserer „universellen“ Schachtel ohne Generics aussah: Box box = new Box(); box.set("Hallo"); String s = (String) box.get(); Jedes Mal, wenn Sie ein Objekt aus der Schachtel nahmen, mussten Sie (String), (Integer) usw. schreiben. Mit Generics entfällt diese Notwendigkeit. Der Compiler weiß bereits, welcher Typ darin liegt, und führt das für Sie automatisch durch: Box<String> stringBox = new Box<>(); stringBox.set("Hallo"); String s = stringBox.get(); Das macht den Code nicht nur kürzer, sondern verbessert die Lesbarkeit und Handhabbarkeit erheblich.

Flexibilität und Wiederverwendbarkeit des Codes. Generics erlauben es, wirklich universelle Klassen und Methoden zu erstellen, die mit unterschiedlichen Datentypen arbeiten, ohne dabei Typsicherheit zu verlieren. Zum Beispiel kann die Klasse Box<T> zum Speichern von Strings, Zahlen, Ihren eigenen Klassen (Student, Car) – für alles Mögliche – verwendet werden! Sie müssen nicht separate Klassen StringBox, IntegerBox und StudentBox schreiben. Sie schreiben eine einzige universelle Klasse Box<T> und geben beim Erzeugen des Objekts einfach den benötigten Typ an. Das reduziert die Code-Menge, vermeidet Duplikate und macht Ihr Programm modularer und flexibler.

6. Einschränkungen von Generics

Trotz aller Vorteile haben Generics einige wichtige Einschränkungen, die man kennen sollte.

Primitive (primitives). Sie können primitive Typen (wie int, double, boolean usw.) nicht als Typparameter verwenden. Zum Beispiel führt der Code Box<int> intBox = new Box<>(); zu einem Kompilierfehler. Stattdessen müssen Sie deren „Wrapper-Klassen“ verwenden (Integer, Double, Boolean). Box<Integer> intBox = new Box<>();. Der Java-Compiler kann Primitive automatisch in Wrapper-Klassen und zurück umwandeln (int in Integer und umgekehrt) – dieser Mechanismus heißt Autoboxing/Unboxing.

Typlöschung (Type Erasure). Das ist ein Kernelement der Implementierung von Generics in Java. Die Idee ist, dass der Java-Compiler die Informationen über Generics nur zur Compile-Zeit verwendet. Nachdem er Ihren Code geprüft und seine Typsicherheit gewährleistet hat, löscht er alle Informationen über Typparameter. Zum Beispiel sehen Box<String> und Box<Integer> im kompilierten Code (im Bytecode) einfach wie Box aus. Das bedeutet, dass sie für die Java Virtual Machine (JVM) derselbe Typ werden.

Was bedeutet das für Sie? Sie können kein Array von Generics erstellen, etwa new Box<String>[10], und Sie können instanceof nicht mit Generics verwenden, um zu prüfen, ob ein Objekt instanceof Box<String> ist. Das ist ein komplexeres Thema, aber sein Verständnis ist wichtig für ein tieferes Studium von Java. An dieser Stelle reicht es zu merken, dass Generics im Grunde ein „Aufkleber“ für den Compiler sind, der ihm hilft, den Code zu prüfen, der Aufkleber jedoch entfernt wird, wenn das Programm startbereit ist.

1
Umfrage/Quiz
Verschachtelte und innere Klassen, Level 16, Lektion 4
Nicht verfügbar
Verschachtelte und innere Klassen
Verschachtelte und innere Klassen
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION