Angelika Langer - Training & Consulting
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | CONTACT | Twitter | Lanyrd | Linkedin
 
HOME 

  OVERVIEW

  BY TOPIC
    JAVA
    C++

  BY COLUMN
    EFFECTIVE JAVA
    EFFECTIVE STDLIB

  BY MAGAZINE
    JAVA MAGAZIN
    JAVA SPEKTRUM
    JAVA WORLD
    JAVA SOLUTIONS
    JAVA PRO
    C++ REPORT
    CUJ
    OTHER
 

GENERICS 
LAMBDAS 
IOSTREAMS 
ABOUT 
CONTACT 
Java Generics - Einführung in generische und parametrisierte Typen in Java

Java Generics - Einführung in generische und parametrisierte Typen in Java
Java Generics: Einführung
Generische und parametrisierte Typen in Java

JavaSPEKTRUM, März 2007
Klaus Kreft & Angelika Langer

Dies ist das Manuskript eines Artikels, der im Rahmen einer Kolumne mit dem Titel "Effective Java" im JavaSPEKTRUM erschienen ist.  Die übrigen Artikel dieser Serie sind ebenfalls verfügbar ( click here ).

Mit den letzen beiden Ausgaben dieser Kolumne haben wir eine Reihe über neue Sprachmittel in Java 5.0 begonnen, die wir in dieser und den folgenden Ausgaben fortsetzen wollen. Dabei soll unser Thema das in Java 5.0 neue Sprachmittel der generischen Typen (Java Generics) sowie die daraus resultierenden parametrisierten Typen sein.

Parametrisierte Typen sind Typen mit Typargumenten.  Beispiele für parametrisierte Typen sind List<String>, List<Date>, Map<Date,String> und WeakReference<InputFileStream>.  Diese parametrisierten Typen entstehen aus generischen Typen, indem für die formalen Typparameter konkrete Typargumente eingesetzt werden. Beispiele für generische Typen sind List<E>, Map<K extends Comparable<K>,V> und WeakReference<T>, wobei E, K, V und T die formalen Typparameter sind.  Diese Typparameter sind Platzhalter und werden später bei der Instanziierung durch konkrete Typen wie String, Date, etc. ersetzt.

In diesem Beitrag wollen wir uns die Grundlagen der generischen und parametrisierten Typen ansehen.

Welchen Vorteil bieten parametrisierte Typen?

Traditionell, d.h. vor Java 5.0, hat man Collections ohne Typargumente verwendet. Hier ein typisches Beispiel:
LinkedList list = new LinkedList();
list.add(new Integer(0));
Integer i = (Integer) list.get(0);  // possible ClassCastException at runtime
In diesem Beispiel wird keine Information darüber gegeben, welche Art von Elementen in der LinkedList abgelegt werden sollen. Eine solche LinkedList ohne Typargumente ist eine Sequenz von Object-Referenzen und folglich können dort Objekte beliebigen Typs abgelegt sein.  Das bedeutet zum einen, dass in verschiedenen LinkedLists, Objekte unterschiedlichen Typs abgelegt werden können, zum Beispiel Strings in der einen LinkedList und Integers in der anderen. Andererseits ist es sogar möglich, in einer LinkedList sowohl Strings als auch Integers gemischt abzulegen. In beiden Fällen ist es nötig, dass beim Herausholen von Elementen aus der LinkedList ein Downcast auf den vermuteten Typ des Elements gemacht werden muß. Dieser Cast kann zur Laufzeit mit einer ClassCastException scheitern, falls die Vermutung über den Typ des Elements unzutreffend war. Dies kommt vor allem dann vor, wenn der Code, der die Collection füllt, relativ weit von dem Code entfernt ist, der die Elemente wieder aus der Collection herausnimmt.

Mit parametrisierten Typen sieht das Beispiel nun so aus:

LinkedList <Integer> list = new LinkedList <Integer> ();
list.add(new Integer(0));
Integer i = list.get(0);
Hier wird im Source-Code bereits festgelegt, dass die Liste eine Liste mit Integer-Elementen ist.  Der Compiler benutzt diese Typinformation und sorgt zur Übersetzungszeit durch entsprechende Typprüfungen dafür, daß nur Integer-Objekte in die LinkedList<Integer> eingefügt werden.  Dies geschieht dadurch, dass Methoden wie zum Beispiel add() so definiert sind, dass sie in einer LinkedList<Integer> den Argumenttyp Integer haben und in einer LinkedList<String> den Argumenttyp String.  Auf diese Weise ist sichergestellt, dass in einer LinkedList<Integer> nur Elemente vom Typ Integer enthalten sind.  Der Versuch, ein Objekt von einem anderen Typ einzufügen, würde bereits zur Übersetzungszeit scheitern.  Die Typprüfung zur Laufzeit wird also durch eine Typprüfung zur Übersetzungszeit ersetzt, wenn parametrisierte Typen verwendet werden. Natürlich können in einer solchen parametrisierten Collection auch Objekte von Subtypen des Elementtyps abgelegt werden. In unserem konkreten Beispiel kann es aber keine Subtypen geben, da beide Klassen, Integer und String,, final sind.

Beim Herausholen von Elementen aus einer Collection entfällt nun der lästige explizite Downcast. Wo nur Integers eingefügt werden dürfen, können nur Integers herauskommen. Die get()-Methode einer LinkedList<Integer> hat deshalb nicht den Returntyp Object, wie bei der nicht-parametrisierten LinkedList, sondern den Returntyp Integer.

Der Vollständigkeit halber muss man noch dazusagen, dass der Cast beim Herausholen aus der Collection weiterhin ausgeführt wird, da der Compiler ihn im Fall einer parametrisierten Collection entsprechend des Typparameters automatisch generiert. Dieser implizit generierte Cast mag auf Grund unseres Beispiels unsinnig erscheinen, rührt aber daher, dass nicht-parametrisierte Typen zu einem hohen Grad kompatibel zu parametrisierten sind. Zum Beispiel kann man eine LinkedList einer LinkedList<String> zuweisen. Man bekommt dann zwar eine Warnung vom Compiler, weil nicht gesichert ist, daß in der LinkedList tatsächlich Strings enthalten sind. Diese Kompatibilität zwischen parametrisierten und nicht-parametrisierten Typen ist sogar in hohem Maße erwünscht, damit sich auf diese Weise Legacy Code, in dem nicht parametrisierte Typen verwendet werden, zusammen mit neuem Code, in dem parametrisierte Typen verwendet werden, verwenden lässt. Die detaillierten Typbeziehungen zwischen parametrisierten und nicht-parametrisierten Typen sind ein relativ umfangreiches und komplexes Thema. Deshalb wollen wir hier nicht genauer darauf eingehen, sondern werden sie in einem der folgenden Artikel ausführlich behandeln.

Es bleibt aber festzuhalten, dass sich bei der Benutzung von parametrisierten Typen kein Performancevorteil beim Herausholen der Elemente ergibt, da der Downcast weiterhin implizit (d.h. compilergeneriert) da ist.  Man sieht ihn lediglich nicht mehr im Source-Code.

Generell kann man aber dennoch sagen, dass durch die Verwendung von parametrisierten Typen explizite Downcasts vermieden werden können und zusätzliche Typprüfungen durch den Compiler zur Übersetzungszeit dazu führen, dass Fehler früher im Entwicklungszyklus gefunden werden. Ein angenehmer Effekt dieser neuen ‚Typsicherheit’ ist zum Beispiel, dass IDEs, die Java 5.0 unterstützen (wie etwa Eclipse ab Version 3.1), nun bereits beim Editieren des Source-Codes wissen, welcher Typ von Objekten mit der add()-Methode in eine parametrisierte Collection eingefügt werden darf und welche Objekte gar nicht zulässig sind. Diese Prüfung erfolgt in klassischem Java-Code (ohne parametrisierten Collections) erst viel später, nämlich beim Test der Anwendung, wenn zur Laufzeit der Downcast beim Herausholen der Objekte ausgeführt wird.

Ein weiterer Vorteil der parametrisierten Typen ist die verbesserte Selbstdokumentation des Source-Codes.  Man kann nun einer Collection-Variable oder einem Collection-Parameter ansehen von welchem Type die enthaltenen Elemente sind. Das heißt, man weiß nun aus dem Source-Code, was die get()-Methode (bei einer List) liefert bzw. was man mit der add()-Methode hineintun darf. Dieser Vorteil kommt natürlich besonders dann zum tragen, wenn man sich in fremden Code einarbeiten muß, zum Beispiel weil man Interfaces oder Klassen nutzt, die von andern Kollegen implementiert worden sind, aber auch bei der Wartung eigenen Codes ist dies ein unschätzbarer Vorteil.

Wie werden generische Typen definiert?

Die Verwendung von vordefinierten generischen Typen ist relativ einfach.  Ein generischer Typ wie List<E> hat einen Typparameter, den man einfach durch einen konkreten Typ ersetzt. 1)   Das Ergebnis ist ein parametrisierter Typ wie List<String>, den man ganz normal wie jeden anderen Typ verwenden kann. 2)   Der Name des Typs ist etwas länger als sonst, nämlich "List<String>", weil das Typargument "String" Bestandteil des Typnamens ist. Das Typargument muss dabei nicht unbedingt eine Klasse sein, wie in unseren bisherigen Beispielen. Vielmehr kann es von irgendeinem Referenztyp sein, zum Beispiel ein Interface oder ein Array, wie etwa List<Runnable> oder List<int[]>. Primitive Typen (wie int, char, usw.) sind nicht als Typargumente zulässig.

Das Definieren von generischen Typen ist etwas anspruchsvoller und setzt voraus, dass der Entwickler die Sprachmittel für die Definition solcher generischen Typen hinreichend beherrscht.  Sehen wir uns ein Beispiel für die Definition eines einfachen generischen Typs an:

interface Iterator<A> {
  public A next ();
  public boolean hasNext ();
}
Das Interface Iterator ist ein generisches Interface und hat einen Typparameter A.  Dieser Parameter A ist ein Platzhalter für einen Typ, der innerhalb des generischen Interfaces ähnlich wie ein echter Typ verwendet wird.  In unserem Beispiel taucht er als Returntyp der next()-Methode auf.  Typparameter können auch als Argumenttypen von Methoden verwendet werden oder für die Deklaration von Variablen.
  1) Das ist ist etwas vereinfacht ausgedrückt.  Man kann auch sogenannte Wildcards einsetzen.  Dann entstehen Konstrukte wie List<?> oder List<? extends Number>.  Da Wildcards ein relativ umfangreiches Thema sind, wollen wir in diesem Artikel nicht weiter auf sie eingehen. Stattdessen werden sie im nächsten Artikel ausführlich diskutieren.
  2) Auch das ist vereinfacht ausgedrückt. Es gibt einige Einschränkungen für die Verwendung von parametrisierten Typen, die es für normale Typen nicht gibt.  Zum Beispiel ist es unmöglich, ein Array zu erzeugen, dessen Komponententyp ein parametrisierter Typ ist.  Das heißt, Array vom Typ WeakReference<String>[] ist unzulässig. Diese ‚Überraschungen’ werden wir in einem der folgenden Artikel ausführlich behandeln.

Typparameter müssen gelegentlich näher beschrieben werden, als wir es im obigen Beispiel getan haben.  Im generischen Interface Iterator<A> kann jeder beliebige konkrete Typ später für den Typplatzhalter A eingesetzt werden.  Das ist nicht immer so.  Es gibt Situationen, in denen nicht jeder beliebige Typ zulässig ist.  Ein Beispiel ist die Implementierung einer sortierten Collection.  Hier die verkürzte Implementierung einer TreeMap<K,V>:

public class TreeMap< Key extends Comparable<Key> ,Data> {
    static class Entry<K,V> {
      K key;
      V value;
      Entry<K,V> left = null;
      Entry<K,V> right = null;
      Entry<K,V> parent;
    }
    private transient Entry<Key,Data> root = null;
    …

    private Entry< Key,Data > getEntry(Key key) {
        Entry<Key,Data> p = root;
        Key k = key;
        while (p != null) {
            int cmp = k.compareTo(p.key) ;
            if (cmp == 0)
                return p;
            else if (cmp < 0)
                p = p.left;
            else
                p = p.right;
        }
        return null;
    }
    public boolean containsKey(Key key) {
        return getEntry(key) != null;
    }
    …
}

Der generische Typ TreeMap<K,V> hat zwei Typplatzhalter K und V für den Key-Typ und den Value-Typ der enthaltenen Key-Value-Paare. In der Implementierung der TreeMap<K,V> wird die compareTo()-Methode des Key-Typs aufgerufen.  Das ist problematisch, denn nicht alle Typen haben eine compareTo()-Methode.  Es wäre also falsch, wenn hier später jeder beliebige Typ eingesetzt werden könnte.  Es sind nur Typen erlaubt, die eine compareTo()-Methode haben.  Diese Anforderung läßt sich durch eine sogenannte Bounds-Klausel ausdrücken.

In der Deklaration der Typparameter ist der Parameter K nicht einfach als "K" deklariert, sondern als "K extends Comparable<K>".  Der Zusatz "extends Comparable<K>" ist die Bounds-Klausel und hat den Effekt, dass nur Typen den Platzhalter K ersetzen können, die das Interface Comparable<K> implementieren.  Beispielsweise könnte der Typ String verwendet werden, weil die Klasse String das Interface Comparable<String> implementiert.  Eine TreeMap<String,Date> wäre also zulässig, wohingegen eine TreeMap<Number,Date> nicht erlaubt wäre, weil der Typ Number nicht das Comparable<Number>-Interface implementiert.

In unserem Beispiel war der Bounds-Typ ein Interface, nämlich Comparable<K>. Der Bounds-Typ darf aber auch eine Klasse sein. Es ist sogar möglich, dass die Bounds-Klausel aus einer Klasse und einer Reihe von Interfaces besteht. Hier ist ein Beispiel:

class GenericClass <A extends MyClass & Comparable<A> & Cloneable> { … }

Die generische Klasse GenericClass hat einen Typparameter, der von MyClass abgeleitet sein muss und die Interfaces Comparable<A> und Cloneable implementiert. Dabei ist auch die Klasse MyClass als Typparameter zulässig ist, falls sie die Interfaces Comparable<MyClass> und Cloneable implementiert, d.h. der Typ, der später für den Typplatzhalter eingesetzt wird, muss nicht unbedingt ein echter Subtyp der Bounds-Klasse sein, sondern darf auch die Bounds-Klasse selbst sein.

Für Diskussionen hat immer wieder die Verwendung des Schlüsselwortes "extends" in der Bounds-Klausel gesorgt.  Meistens sind die Bounds nämlich Interfaces und deshalb wäre es naheliegend, "K implements Comparable<K>" zu sagen statt "K extends Comparable<K>".  Einen tieferen Grund für die Verwendung von "extends" statt "implements" gibt es nicht.  Die Sprachdesigner bei Sun haben es einfach so festgelegt, daß man immer "extends " sagen muß, egal ob die Bounds-Klausel eine Klasse oder ausschließlich Interfaces enthält.

Es gibt noch eine Einschränkung bei der Verwendung von Interfaces:  die Bounds-Klausel darf nicht verschiedene Instantierungen des gleichen generischen Interfaces enthalten. Das heißt die folgende Definition ist nicht zulässig:

class NewGenericClass <A extends MyClass & Comparable<A> & Comparable<String> > { … }  // falsch !!!

Um zu verstehen, warum das so ist, sei wieder auf den Folgeartikel verwiesen, in dem wir die Typebeziehungen zwischen parametrisierten Instanzen eines generischen Typs genauer untersuchen werden.
 
Zusammenfassend lässt sich sagen, dass die Bounds-Klausel einerseits dazu dient, Anforderungen, die die Implementierung des generischen Typs an den Typparameter stellt, auszudrücken.  Andererseits machen die Bounds genau die Eigenschaften des Typparameters in der generischen Klasse zugänglich, die in den Bounds beschrieben sind.  Durch die Bounds wird alles zugänglich gemacht, was in dem Bounds-Typ public ist 3) , d.h Methoden, Felder und eingeschachtelte Typen, mit Ausnahme von Konstruktoren.  Die Konstruktoren sind ausgenommen, weil sie nicht vererbt werden; eine Subklasse muß nicht die gleichen Konstruktoren haben wie ihre Superklasse. 4)

Die eigentliche Kunst bei der Implementierung eines eigenen generischen Typs besteht nun darin, die Typparameter und ihre zugehörigen Bounds richtig zu wählen. Das ist nicht immer so einfach, wie es klingt. In zukünftigen Artikeln werden einige instruktive Fallbeispiele dazu diskutieren.

  3) In bestimmten Situationen gehören dazu auch noch die Package-sichtbaren und protected Inhalte einer Bounds-Klasse, nämlich wenn die Bounds-Klasse und die generische Klasse, in deren Bounds-Klausel die Klasse vorkommt, im selben Package liegen.
  4) Bei den statischen Elementen ist ebenfalls Vorsicht geboten, weil es zu überraschenden Hiding-Effekten kommen kann, wenn die Subklasse statische Elemente der Superklasse redefiniert.  Da das aber ohnehin schlechter Programmierstil ist, wollen wir an dieser Stelle nicht näher darauf eingehen.  Mehr dazu findet sich in / FAQ105 /.

Generische Methoden

Nicht nur Typen sondern auch Methoden können generisch sein.  Hier ist ein Beispiel für eine generische Methode.  Es ist der Methode max()aus der Klasse java.util.Collections nachempfunden:
 public static
  <A extends Comparable<A>> A max (Collection<A> xs) {
    Iterator<A> xi = xs.iterator();
    A w = xi.next();
    while (xi.hasNext()) {
      A x = xi.next();
      if (w.compareTo(x) < 0) w = x;
    }
    return w;
  }
Diese Methode liefert das größte Element aus einer Collection. Voraussetzung, um überhaupt das größte Element definieren zu können, ist die Vergleichbarkeit der Elemente der Collection. Deshalb enthält die Definition der Methode die Bounds-Klausel "A extends Comparable<A>" für den Elementtyp der Collection.

Die Verwendung generischer Methoden ist deutlich einfacher als die Verwendung generischer Typen, weil die explizite Typparametrisierung entfällt.  Hier ist ein Beispiel für den Aufruf der max()-Methode:

LinkedList<Byte> byteList = new LinkedList<Byte>();
byteList.add((byte)0));
byteList.add((byte)1));

byte maxByte = Collections.max(byteList);

Man ruft die generische Methode wie eine ganz normale Methode auf und der Compiler macht den Rest.  Man muss, anders als bei den generischen Klassen, nicht explizit sagen, was für den Typparameter A eingesetzt werden soll.  Das überlegt sich der Compiler selbst.  Er macht eine sogenannte Type Inference, d.h. er schaut sich den Typ des an die Methode übergebenen Methodenarguments an, in unserem Beispiel den Typ von byteList, also LinkedList<Byte>, und zieht daraus Schlüsse über den Typ, der für den Platzhalter A einzusetzen ist.  In unserem Beispiel wird eine LinkedList<Byte> übergeben und es ist eine Collection<A> deklariert.  Dann folgert der Compiler, dass A durch Byte ersetzt werden muss.

Das Ganze geht automatisch und kann natürlich auch scheitern und zu Fehlermeldungen führen, z.B. wenn der per Type Inference ermittelte Typ die Bounds-Klausel gar nicht erfüllt.  Das wäre z.B. der Fall, wenn eine LinkedList<Object> an die max()-Methode übergeben würde.  Der Typ Object ist nicht Comparable<Object>, genügt also der Bounds-Klausel nicht.  Das führt zu einer Fehlermeldung.

Es gibt auch noch komplexere Situationen, in denen der Compiler die Type Inference gar nicht machen kann, z.B. wenn eine null-Referenz übergeben wird.  Die null-Referenz hat keinen Typ, also hat der Compiler keine Chance, aus der null-Referenz irgendwelche Schlüsse darüber zu ziehen, was wohl für den Typparameter A einzusetzen wäre.  Mehr Information darüber, was man in solchen Situation tun kann, findet man in / FAQ401 /.

Am Rande sei noch erwähnt, dass wir in dem Beispiel oben ein weiteres neues Feature aus dem JDK 5.0 verwendet haben: das automatische Boxing und Unboxing.  Boxing ist die Konvertierung von einem primitiven Typ wie int in seinen korrespondierenden Wrapper-Typ Integer.  Unboxing ist die umgekehrte Konvertierung, nämlich das Auspacken eines primitiven Typs aus seinem Wrapper-Typ, also die Konvertierung von Integer nach int.  Beide Konvertierungen sind seit Java 5.0 implizite Konvertierung, die vom Compiler automatisch gemacht werden.  Wir haben das z.B. benutzt, als wir das Ergebnis der max()-Methode einem byte zugewiesen haben.  Die max()-Methode liefert eigentlich eine Referenz vom Wrapper-Typ Byte, die per Auto-Unboxing ausgepackt wird und dann dem primitiven Typen byte zugewiesen wird.  Durch das Auto-Boxing und -Unboxing bleibt der Code recht übersichtlich. Das Feature selbst werden wir in einem zukünftigen Artikel noch ausführlicher diskutieren.

Generische Methoden und generische Klassen kombinieren

Beim Thema generische Methoden und generische Typen drängen sich die Fragen auf: kann ich generische Typen und generische Methoden kombinieren, d.h. kann ich einen generischen Typ definieren und in diesem Typ eine Methode mit neuen, eigenen Typparametern definieren?  - Ja, so etwas ist in Java zulässig.

Womit sich die Frage stellt: darf die generische Methode in der generischen Klasse auch der Konstruktor sein? – Ja, auch das ist möglich. Es gibt keinerlei Einschränkungen; alle statischen und nicht-statischen Methoden sowie die Konstruktoren können generisch sein und eigene Typparameter haben.

Wann kommt es zu einer solchen Kombination von generischem Typ und generischer Methode?  Das wollen wir an einem Beispiel erläutern:

final class Wrapper<T extends Cloneable> {
  private T content;
  public Wrapper(T arg) {
      content = ... Klon von arg ...
  }
  public Wrapper(Wrapper<T> other) {
      content = ... Klon von other.content ...
  }
  ... weitere Methoden ...
}
Diese Wrapper-Klasse hat zwei Konstruktoren.  Der zweite Konstruktor ist eine Art Copy-Konstruktor, der ein anderes Wrapper-Objekt als Argument akzeptiert und seinen Inhalt kopiert.  Dieser Konstruktor verlangt, dass der andere Wrapper von exakt demselben Typ sein muss.  Folgende Verwendung wäre daher  unmöglich:
Wrapper<Date> w1 = new Wrapper<Date>(new Date());
Wrapper<Cloneable> w2 = new Wrapper<Cloneable>(w1);
Es ist aber durchaus sinnvoll zuzulassen, dass ein Wrapper<Cloneable> sich das Date-Objekt aus einem Wrapper<Date> herauskopiert; schließlich ist der Type Date ein Subtyp von Cloneable.  Um dies zuzulassen, müßte man den Konstruktor so ändern, dass er Wrapper-Typen akzeptiert, die für einen Subtype von T instantiiert sind.  Das heißt, man braucht einen zweiten Typplatzhalter "S extends T", damit für den Konstruktor ein Argument vom Type Wrapper<S> deklariert werden kann.  Diesen zweiten Typplatzhalter könnte man der generischen Wrapper-Klasse hinzufügen.  Das sähe dann so aus:
final class Wrapper<T extends Cloneable, S extends T > {
  private T content;
    public Wrapper(T arg) {
      content = ... Klon von arg ...
    }
  public  Wrapper(Wrapper<S> other) {    // does not compiler
      content = ... Klon von other.content ...
  }
  ... weitere Methoden ...
}
Das ist aber Unsinn.  Die Klasse hätte nun zwei Typparameter; der zweite Typparameter wird aber nur in dem einen Konstruktor gebraucht.  Außerdem läßt sich der Code nicht mehr übersetzen, weil nun das Argument des fraglichen Konstruktors vom Typ Wrapper<T,S> sein müßte.  Zudem hätte man nun einen Wrapper<Date,Date> und einen Wrapper<Cloneable,Cloneable>, was völlig unnatürlich und redundant ist.  Offensichtlich ist diese Lösung nicht ideal.  Falsch ist an der Lösung, dass der zweite Typparameter an der generischen Klasse aufgehängt ist.

Generell sollte man die Parametrisierung so lokal wie möglich halten.  Statt die gesamte Klasse mit zusätzlichen Typparametern auszustatten, genügt es u.U., wenn nur eine einzelne Methoden einen Typparameter bekommt.  Wenn der zweite Typparameter S sowieso nur für den Konstruktor gebraucht wird, dann sollte man die Parametrisierung auch auf den Konstruktor beschränken.  Besser wäre also folgende Lösung:

final class Wrapper<T extends Cloneable> {
  private T content;
    public Wrapper(T arg) {
      content = ... Klon von arg ...
    }
  public <S extends T> Wrapper(Wrapper<S> other) {
      content = ... Klon von other.content ...
  }
  ... weitere Methoden ...
}
Der Konstruktor ist jetzt generisch und hat einen eigenen Typparameter S; die Klasse hat nach wie vor nur einen Typparameter T.  Das ist ein Beispiel für eine generische Methode in einer generischen Klasse.

Übrigens sind statische Methoden viel häufiger generisch als nicht-statische Methoden.  Das liegt daran, dass Typparameter einer Klasse nicht in statischen Methoden verwendet werden dürfen.  Warum das so ist, werden wir in einem zukünftigen Artikel erläutern.  Jedenfalls können statische Methoden ohnehin nicht auf die Typparametern der umgebenden Klasse zugreifen und haben deshalb eigene Typparameter.  Hier ist ein Beispiel:

final class Wrapper<T extends Cloneable> {
  private T content;
    public Wrapper(T arg) {
      content = makeCopy(arg);.
    }
  public <S extends T> Wrapper(Wrapper<S> other) {
      content = makeCopy(other.content);
  }
  private static <A extends Cloneable> A makeCopy(A arg) {
    try {
      return (A) arg.getClass().getMethod("clone").invoke(null);
    } catch (Exception e) {
      return null;
    }
  }
  ... weitere Methoden ...
}
Hier haben wir eine statische Hilfsmethode makeCopy() in unserer Wrapper-Klasse definiert, die Kopien von Objekten per Aufruf der clone()-Methode erzeugt.  Die Methode kann nicht auf den Typparameter T der umgebenden generischen Klasse zugreifen.  Sie hat deshalb ihren eigenen Typparameter A.  Das ist typisch für statische Methoden.  Das java.util Package ist voll von Beispielen solcher generischen Methoden.  Ein illustratives Beispiel ist die Klasse java.util.Collections.

Zusammenfassung und Ausblick

In diesem Artikel ging es uns darum, die Grundlagen zum Thema generische Typen und generische Methoden zu vermitteln. Wir haben die wesentlichen Sprachelemente für die Definition von generischen Typen und Methoden vorgestellt. Wie bereits erwähnt, werden wir in weiteren Artikeln vertiefend auf spezielle Aspekte des Themas eingehen. Zum Beispiel haben wir Wildcards noch gar nicht erläutert, oder Themen wie die Kompatibilität zwischen generischen und nicht-generischen Typen. Am Ende der Serie werden wir an einigen Fallstudien das typische Vorgehen sowie potentielle Fehler bei der Implementierung generischer Typen und Methoden diskutieren.
 

Literaturverweise und weitere Informationsquellen

/FAQ/ Java Generics FAQ
Angelika Langer
URL: http://www.AngelikaLanger.com/GenericsFAQ/JavaGenericsFAQ.html
/ FAQ105 / Java Generics FAQ: Does a bound that is a class type give access to all its public members?
URL: http://www.AngelikaLanger.com/GenericsFAQ/FAQSections/TypeParameters.html#FAQ105
/ FAQ401 / Java Generics FAQ: What is type argument inference?
URL: http://www.AngelikaLanger.com/GenericsFAQ/FAQSections/TechnicalDetails.html#FAQ401

Die gesamte Serie über Java Generics:

/GEN1/  Java Generics - Einführung
Klaus Kreft & Angelika Langer
Java Spektrum, März 2007
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/30.GenericsIntro/30.GenericsIntro.html
/GEN2/  Java Generics - Wildcards
Klaus Kreft & Angelika Langer
Java Spektrum, Mai 2007
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/31.Wildcards/31.Wildcards.html
/GEN3/ Java Generics - Type Erasure
Klaus Kreft & Angelika Langer
Java Spektrum, Juli 2007
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/32.TypeErasure/32.TypeErasure.html
/GEN4/ Java Generics - Type Erasure
Klaus Kreft & Angelika Langer
Java Spektrum, September 2007
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/33.TypeErasurePitfall/33.TypeErasurePitfall.html
/GEN5/ Java Generics - Type Erasure
Klaus Kreft & Angelika Langer
Java Spektrum, November 2007
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/34.GenericCreation/34.GenericCreation.html

If you are interested to hear more about this and related topics you might want to check out the following seminar:
Seminar
 
Effective Java - Java best practice programming techniques, common pitfalls, and off-the-beaten-path language features
4 day seminar ( open enrollment and on-site)
 

 
  © Copyright 1995-2016 by Angelika Langer.  All Rights Reserved.    URL: < http://www.AngelikaLanger.com/Articles/EffectiveJava/30.GenericsIntro/30.GenericsIntro.html  last update: 2 Aug 2016