06.02 Generics

Seit Java 5 ist es möglich, Generics (generische Typen) in Java zu verwenden. Bei der Verwendung von Generics werden Typen (Klassen, Interfaces) zum Zeitpunkt der Implementierung noch nicht festgelegt. Dieser variable Typ wird erst bei der Arbeit mit den Klassen, Schnittstellen oder Methoden konkretisiert. Eine Methode einer Klasse, die ein Objekt einer beliebigen Klasse zurück gibt (da diese bei der Implementierung der Methode noch nicht festgelegt wurde), musste vor Java 5 immer ein Objekt der Klasse Object zurückgeben. Die aufrufende Klasse musste den Rückgabewert also in den gewünschten Datentyp casten. Durch Generics kann die Klasse des zurückgegebenen Objekts allerdings noch nach der Implementierung unserer Methode festgelegt werden, so dass auf Casting verzichtet werden kann, und die Klasse somit typsicher wird.

Java selbst beinhaltet bereits einige Klassen, die mit Generics ausgestattet sind. Dazu zählen z. B. HashMap oder ArrayList, welche Sie später kennenlernen werden. Um einer generischen Klasse einen konkreten Typ zuzuweisen, wird bei der Deklaration und Initialisierung der Klasse der gewünschte Typ in spitzen Klammern angegeben.

GenerischeKlasse<GenerischerTyp> gk = new GenerischeKlasse<>();

Zu Testzwecken werden Sie jetzt selbst eine Klasse mit Generics programmieren und einsetzen. Diese Klasse wird zuerst lediglich Getter- und Settermethoden für ihren generischen Typen beinhalten.

Bei generischen Klassen wird die Deklaration des generischen Typs in den Kopf der Klasse innerhalb von spitzen Klammern eingeschlossen.

public class GenerischeKlasse<Generic> {

Mit dem Namen des variablen Typs (hier Generic) wird im weiteren Programmverlauf der später konkretisierte Typ assoziiert. Wir können also mit diesem Namen eine Variable mitsamt deren Gettern und Settern definieren:

package de.test.generics;

public class GenerischeKlasse<Generic> {

  private Generic aObject = null;

  public Generic getAObject() {
    return this.aObject;
  }

  public void setAObject(Generic object) {
    this.aObject = object;
  }
}

Erstellen wir nun eine weitere Klasse inkl. Main-Methode um unsere Klasse zu testen. Dafür erzeugen wir zwei Objekte der Klasse GenerischeKlasse und übergeben einmal als generischen Typ einen String und das andere Mal einen Integer.

public static void main(String[] args) {
		
  GenerischeKlasse<String> stringGeneric = new GenerischeKlasse<>();
  GenerischeKlasse<Integer> integerGeneric = new GenerischeKlasse<>();
		
  stringGeneric.setAObject("Ich bin ein String");
  integerGeneric.setAObject(42);
		
  System.out.println(stringGeneric.getAObject().substring(12));
  int ergebnis = integerGeneric.getAObject() / 2;
  System.out.println(ergebnis);
}

Würden Sie jetzt versuchen, dem integerGeneric einen String zu übergeben, würden Sie vom Compiler eine entsprechende Fehlermeldung erhalten. Durch Generics können Sie also ausschließen, dass ein anderer Typ über- oder zurückgegeben wird, den Sie nicht erwarten. Wie oben erwähnt, macht das Ihren Code typsicher, so dass Sie den Rückgabewert der Methode genauso wie ein Objekt des generischen Typs behandeln können.

Ohne die Verwendung von Generics würde Ihre Klasse vermutlich so aussehen:

package de.test.generics;

public class NormaleKlasse {

  private Object aObject = null;

  public Object getAObject() {
    return this.aObject;
  }

  public void setAObject(Object object) {
    this.aObject = object;
  }
}

Ihre Main-Methode müsste dementsprechend angepasst werden:

public static void main(String[] args) {

  NormaleKlasse stringGeneric = new NormaleKlasse();
  NormaleKlasse integerGeneric = new NormaleKlasse();
		
  stringGeneric.setAObject("Ich bin ein String");
  integerGeneric.setAObject(42);
		
  if (stringGeneric.getAObject() instanceof String) {
    System.out.println(((String)stringGeneric.getAObject()).substring(12));
  }
  if (integerGeneric.getAObject() instanceof Integer) {
    int ergebnis = (Integer)integerGeneric.getAObject() / 2;
    System.out.println(ergebnis);
  }
}

Wie Sie sehen, sind Generics eine echte Erleichterung für Sie als Programmierer.

Selbstverständlich können Sie auch mehrere generische Typen verwenden. Dies könnte bspw. so aussehen:

public class GenerischeKlasse<Generic, Generic2, Generic3> {
...
}
...
GenerischeKlasse<String, Integer, Double> multiGeneric = new GenerischeKlasse<String, Integer, Double>();

Beachten Sie, dass Sie einer generischen Klasse nicht zwingend Typen zuweisen müssen. Sie können eine generische Klasse auch wie eine gewöhnliche Klasse initialisieren.

GenerischeKlasse objectGeneric = new GenerischeKlasse();

Diese Initialisierung entspricht etwa der Initialisierung mit dem Typ Object, nur dass Ihr Compiler (abhängig vom verwendeten Compiler und gewählten Einstellungen) ggf. ein oder mehrere Warnmeldungen ausgeben wird. Dies hat natürlich auch zur Folge, dass Sie den Rückgabewert von getAObject wieder in den gewünschten Typ casten müssen.

Wie eingangs erwähnt, sind Generics nicht nur auf Klassen beschränkt. Sie können auch ohne Problemen für Methoden oder Konstruktoren definiert werden.

Generische Methode, die ihren generischen Typ zurückgibt:

public <G> G genericMethod(G anObjectFromG) {
  ...
  return anObjectFormG;
}

Generischer Konstruktor, der seinen generischen Typ als Parameter erwartet:

public <G> GenerischeKlasse(G typ) {
  System.out.println(typ.getClass());
}

Generics können allerdings nicht komplett wie gewöhnliche Klassen verwendet werden. Es ergeben sich u. a. folgende Einschränkungen:

  • Es ist nicht möglich Arrays von generischen Klassen zu erstellen =>
GenerischeKlasse<String>[] ar = new GenerischeKlasse<String>[5];
  • Es ist nicht möglich ein Array des generischen Typs zu erstellen =>
public class GenerischeKlasse<Generic> {
...
  Generic[] g = new Generic[5];
}
  • Es ist aber sehr wohl möglich die Arrays außerhalb der generischen Klasse zu erstellen und dann zu übergeben =>
public class GenerischeKlasse<Generic> {
...
  public void doSomething(Generic[] g) {}
}
  • Übergabeparameter können nicht anhand ihres generischen Typs unterschieden werden =>
public void doSomething(GenerischeKlasse<Integer> i) {}
public void doSomething(GenerischeKlasse<String> s) {}
  • Keine Überprüfung mit instanceof =>
GenerischeKlasse<String> str = new GenerischeKlasse<>();
System.out.println(str instanceof GenerischeKlasse<String>);

oder

System.out.println(anObject instanceof Generic);
  • Es können keine neuen Instanzen des generischen Typs auf gewöhnliche Art erstellt werden =>
Generic gen = new Generic();

Type Erasure

Die Generics werden – zu Gunsten der Abwärtskompatibilität – nur vom Compiler behandelt und verschwinden zur Laufzeit. Das heißt, der Compiler entfernt beim Kompilieren alle Informationen, die durch Generics definiert wurden. So wird eine Kompatibilität zu älteren Java-Code gewährleistet, der keine Generics einsetzt (siehe Kapitel 07.06 Legacy Code).

Eine GenerischeKlasse<Xyz> klasse; wird zu GenerischeKlasse klasse; und stellt dann den so genannten Raw Type dieser Klasse da. Ein Raw Type ist eine generische Klasse ohne deren generische Typen. Das bedeutet, dass man zur Laufzeit nicht bestimmen kann, welcher generischer Typ verwendet wurde. Unsere generische Klasse würde danach ungefähr so, wie unsere Klasse NormaleKlasse aus dem vorhergehenden Beispiel aussehen.

Empfehlungen Bezeichnungen für Generics

Sun gibt Empfehlungen aus, wie Sie Ihre generischen Typen benennen sollten. Anbei eine kleine Auflistung:

Bezeichnung Bedeutung
E Ein Element der Klasse
K Ein Schlüssel (key)
N Eine Nummer
T Ein Typ
V Ein Wert (value)
S, (T), U, V, … Weitere Typen => S zweiter Typ, (T erster Typ,) U dritter Typ, V vierter Typ, …

Im nächsten Kapitel lernen Sie etwas über die Verwendung von Generics mit Wildcards und beschränkten Typ-Parametern. Außerdem finden Sie dort auch ein ausführliches, praktisches Beispiel zur Vertiefung der Theorie.

Unter Umständen haben Sie sich in diesem Kapitel gewundert, warum Sie einen Integer genauso wie einen int verwenden können, und wo überhaupt der Unterschied liegt. Dies lernen Sie im Kapitel über Autoboxing und Wrapper-Klassen.

2 Replies to “06.02 Generics”

  1. asdf

    „Um einer generischen Klasse einen konkreten Typ zuzuweisen, wird bei der Deklaration und Initialisierung der Klasse der gewünschte Typ in eckigen Klammern angegeben.“ stimmt so nicht ganz, eckige Klammern sehen so aus: ‚[‚ ‚]‘ gemeint ist aber “

Schreibe einen Kommentar

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.