Generische Programmierung in Java 5.0

Generische Programmierung in Java 5.0

Generische Programmierung wird in Java durch so genannte Generics ermöglicht. Der Begriff steht synonym für „parametrisierte Typen“. Die Idee dahinter ist zusätzliche Variablen für Typen, sog. Typ-Variablen, einzuführen. Diese repräsentieren zum Zeitpunkt der Implementierung unbekannte Typen. Erst bei der Verwendung der Klassen, Schnittstellen und Methoden werden diese Typ-Variablen durch konkrete Typen ersetzt. Damit kann typsichere Programmierung gewährleistet werden.

Inhaltsverzeichnis

Das Konzept

Ab Version 5.0 ("Tiger", 2004 veröffentlicht) steht auch in der Programmiersprache Java mit den Generics ein Sprachmittel für die generische Programmierung zur Verfügung. Damit lassen sich Klassen und Methoden (Methoden auch unabhängig von ihren Klassen) mit Typen parametrisieren. Damit werden der Sprache einige ähnliche Möglichkeiten eröffnet, die sich vergleichbar bei den Templates in C++ bieten.

Prinzipiell gibt es aber durchaus wesentliche Unterschiede. Während in C++ über die Schnittstelle der Typparameter parametrisiert wird, wird in Java direkt über den Typ des Typparameters selbst parametrisiert.

Beispielsweise bietet die Funktion std::sort in C++ die Möglichkeit, alle Container zu sortieren, die bestimmte Methoden anbieten (hier speziell begin() und end(), die jeweils einen iterator liefern) und deren Typparameter den 'operator<' implementiert (oder explizit eine andere Vergleichsfunktion angegeben wurde). Ein Nachteil, der sich durch dieses System ergibt, ist die (für den Programmierer!) schwierigere Übersetzung. Der Compiler hat keine andere Möglichkeit, als den Typparameter in jedem Fall durch den geforderten konkreten Typ zu ersetzen und den ganzen Code erneut zu compilieren.

Sehr leicht können bei unpassenden Typparametern und anderen Problemen komplizierte und unverständliche Compiler-Meldungen entstehen, was einfach mit der Tatsache zusammenhängt, dass die konkreten Anforderungen an den Typparametern unbekannt sind. Die Arbeit mit C++-Templates erfordert deshalb eine lückenlose Dokumentation der Anforderungen an einen Typparameter. Durch Template-Metaprogrammierung können die meisten Anforderungen (Basisklasse, Vorhandensein von Methoden, Kopierbarkeit, Zuweisbarkeit etc.) auch in speziellen Konstrukten abgefragt werden, wodurch sich lesbarere Fehlermeldungen ergeben. Obgleich sie standardkonform sind, werden diese Konstrukte jedoch nicht von allen Compilern unterstützt.

Dagegen sind den generischen Klassen und Methoden in Java die Anforderungen (engl. constraints) an ihre eigenen Typparameter bekannt. Um eine Collection zu sortieren, müssen die enthaltenen Elemente vom Typ Comparable sein, also dieses Interface implementiert haben¹. Der Compiler muss lediglich prüfen, ob der Typparameter einem Comparable entspricht und kann damit schon sicherstellen, dass der Code korrekt ist. Weiterhin wird ein und derselbe Code für alle konkreten Typen verwendet und nicht zigmal dupliziert. Dadurch ergibt sich allerdings auch eine geringere Spezialisierung des erzeugten Codes auf die einzelnen Typen, was sich in der Perfomance bemerkbar machen kann.

¹ Dies ist nicht ganz korrekt; es besteht auch die Möglichkeit, einen generischen Comparator zu übergeben. Als Beispiel soll das aber mal gelten.

Praktische Beispiele

Ein Programm verwendet eine ArrayList, um eine Liste von JButtons zu speichern.

Bisher war die ArrayList auf den Typ Object fixiert:

 ArrayList al = new ArrayList();
 al.add(new JButton("Button 1"));
 al.add(new JButton("Button 2"));
 al.add(new JButton("Button 3"));
 al.add(new JButton("Button 4"));
 al.add(new JButton("Button 5"));
 for (int i = 0; i < al.size(); i++) {
     JButton button = (JButton)al.get(i);
     button.setBackground(Color.white);
 }

Man beachte die notwendige explizite Typumwandlung (auch "Cast" genannt) sowie die Typunsicherheit, die damit verbunden ist. Man könnte versehentlich ein Objekt in der ArrayList speichern, das keine Instanz der Klasse JButton ist. Die Information über den genauen Typ geht beim Einfügen in die Liste verloren, der Compiler kann also nicht verhindern, dass zur Laufzeit bei der expliziten Typumwandlung von JButton eine ClassCastException auftritt.

Mit generischen Typen ist in Java Folgendes möglich:

 ArrayList<JButton> al = new ArrayList<JButton>();
 al.add(new JButton("Button 1"));
 al.add(new JButton("Button 2"));
 al.add(new JButton("Button 3"));
 al.add(new JButton("Button 4"));
 al.add(new JButton("Button 5"));
 for (int i = 0; i < al.size(); i++) {
     al.get(i).setBackground(Color.white);
 }

Beim Auslesen ist nun keine explizite Typumwandlung mehr notwendig, beim Speichern ist es nur noch möglich, JButtons in der ArrayList al abzulegen.

Interessant ist dann auch noch die Kombination von generischen Typen mit den erweiterten For-Schleifen. Obiges Beispiel lässt sich somit kurz fassen:

 ArrayList<JButton> al = new ArrayList<JButton>();
 al.add(new JButton("Button 1"));
 al.add(new JButton("Button 2"));
 al.add(new JButton("Button 3"));
 al.add(new JButton("Button 4"));
 al.add(new JButton("Button 5"));
 for (JButton b : al) {
     b.setBackground(Color.white);
 }

Ein Beispiel für eine generische Klasse, die zwei Objekte von beliebigem, aber einander gleichem Typ beinhaltet, liefert der folgende Beispielcode:

  public class DoubleObject<T> {
    private T object1;
    private T object2;
    public DoubleObject(T object1,T object2) {
      this.object1=object1;
      this.object2=object2;
    }
    public String toString() {
      return ""+this.object1+","+this.object2;
    }
    public static void main(String[] args) {
      DoubleObject<String> s=new DoubleObject<String>("abc","def");
      DoubleObject<Integer> i=new DoubleObject<Integer>(123,456);
      System.out.println("DoubleObject<String> s="+s);
      System.out.println("DoubleObject<Integer> i="+i);
    }
  }

Varianzfälle

In Java können die nachfolgenden Varianzfälle unterschieden werden. Sie bieten jeweils eine völlig eigenständige Flexibilität beim Umgang mit generischen Typen und sind jeweils absolut statisch typsicher.

Invarianz

Bei Invarianz ist der Typparameter eindeutig. Damit bietet Invarianz die größtmögliche Freiheit bei der Benutzung des Typparameters. Beispielsweise sind für die Elemente einer ArrayList<Integer> alle Aktionen erlaubt, die auch bei der direkten Benutzung eines einzelnen Integers erlaubt sind (inklusive Autoboxing). Beispiel:

 ArrayList<Integer> list = new ArrayList<Integer>();
 ...
 Integer x = list.get(index);
 list.get(index).methodeVonInteger();
 list.set(index, 98347);     // Autoboxing, entspricht Integer.valueOf(98347)
 int y = list.get(index);    // Auto-Unboxing

Diese Möglichkeiten werden mit wenig Flexibilität bei der Zuweisung von Objekten der generischen Klasse selbst erkauft. Beispielsweise ist folgendes nicht erlaubt:

 ArrayList<Number> list = new ArrayList<Integer>();

und das, obwohl Integer von Number abgeleitet ist. Der Grund liegt darin, dass der Compiler hier nicht mehr sicherstellen kann, dass keine Typfehler auftreten. Mit Arrays, die eine solche Zuweisung erlauben, hat man schlechte Erfahrungen gemacht:

 Number[] array = new Integer[10];    // OK, Integer[] ist abgeleitet von Number[]
 array[0] = new Double(5.0);          // ArrayStoreException zur Laufzeit: Double -> Integer
                                      // sind nicht zuweisungskompatibel

Kovarianz

Man bezeichnet Arrays als kovariant, was besagt:

Aus T extends V folgt: T[] extends V[]

oder allgemeiner:

Aus T extends V folgt: GenerischerTyp<T> extends GenerischerTyp<V>

Es verhält sich also der Array-Typ bzgl. der Vererbungshierarchie genauso wie der Typparameter. Kovarianz ist auch mit generischen Typen möglich, allerdings nur mit Einschränkungen, so dass Typfehler zur Compilierzeit ausgeschlossen werden können.

Referenzen müssen mit der Syntax ? extends T explizit als kovariant gekennzeichnet werden. T heißt upper typebound, also der allgemeinste Typparameter, der erlaubt ist.

 ArrayList<? '''extends''' Number> list;
 list = new ArrayList<Integer>();
 list = new ArrayList<Double>();
 list = new ArrayList<Long>();
 list.set(index, myInteger);         // Fehler: nicht typsicher

Das Ablegen von Elementen in diesen Listen ist nicht möglich, da dies, wie oben beschrieben, nicht typsicher ist (Ausnahme: null kann abgelegt werden). Bereits zur Compilierzeit tritt ein Fehler auf. Allgemeiner gesagt, ist die Zuweisung

?? extends T

nicht erlaubt.

Möglich dagegen ist das Auslesen von Elementen:

 Number x = list.get(index);   // OK
 Integer x = list.get(index);  // Fehler: Es muss sich bei
                               // '? extends Number' nicht
                               // um ein Integer handeln.

Die Zuweisung

? extends TT (oder Basisklasse)

ist also erlaubt, nicht aber die Zuweisung

? extends Tabgeleitet von T

Generics bieten also wie Arrays kovariantes Verhalten, verbieten aber alle Operationen, die typunsicher sind.

Kontravarianz

Kontravarianz bezeichnet das Verhalten der Vererbungshierarchie des generischen Typs entgegen der Hierarchie seines Typparameters. Übertragen auf das obige Beispiel würde das bedeuten: Eine Liste<Number> wäre "abgeleitet" von (zuweisungskompatibel zu) einer Liste<Double>! Dies wird folgendermaßen bewerkstelligt:

 ArrayList<? super Double> list;
 list = new ArrayList<Number>();
 list = new ArrayList<Double>();
 list = new ArrayList<Object>();

Ein Objekt, das sich kontravariant verhält, darf keine Annahmen darüber machen, inwiefern ein Element vom Typ V von T abgeleitet ist, wobei T der lower Typebound ist (im Beispiel von '? super Double' ist T 'Double'). Deshalb kann aus den obigen Listen nicht gelesen werden:

 Number x = list.get(index);   // Fehler: 'list' könnte vom Typ List<Object> sein
 Double x = list.get(index);   // Fehler: 'list' könnte List<Object> oder List<Number> sein
 Object x = list.get(index);   // Die einzige Ausnahme: Objects sind auf jeden Fall in der Liste

Nicht erlaubt, da nicht typsicher, ist also die Zuweisung ? super T → (abgeleitet von Object)

Unschwer zu erraten: Im Gegenzug kann in eine solche Liste ein Element abgelegt werden:

 ArrayList<? super Number> list;
 list.add(myDouble);           // OK: 'list' hat immer den Typ List<Number>
                               // oder List<Basisklasse von Number>. Damit
                               // ist die Zuweisung Double -> T immer erlaubt.

Bivarianz

Zu guter Letzt bieten Generics noch bivariantes Verhalten an. Hierbei kann keinerlei Aussage über die Typparameter gemacht werden, denn es wird in beide Richtungen keine Grenze angegeben.

 ArrayList<?> list;
 list = new ArrayList<Integer>();
 list = new ArrayList<Object>();
 list = new ArrayList<String>();
 ...

Der Typparameter selbst kann hierbei nicht genutzt werden, da keine Aussage möglich ist. Lediglich die Zuweisung T → Object ist erlaubt, da T auf jeden Fall ein Object ist.

Nützlich kann so etwas sein, wenn man nur mit dem generischen Typ arbeitet:

 // Keine Informationen über den Typparameter nötig,
 // kann ''beliebige'' Listen aufnehmen.
 int readSize(List<?> list)
 {
     return list.size();
 }

Weblinks

Siehe auch


Wikimedia Foundation.

Игры ⚽ Поможем сделать НИР

Schlagen Sie auch in anderen Wörterbüchern nach:

  • Generische Programmierung in Java — wird durch sog. Generics ermöglicht. Der Begriff steht synonym für „parametrisierte Typen“. Die Idee dahinter ist, zusätzliche Variablen für Typen einzuführen. Diese Typ Variablen repräsentieren zum Zeitpunkt der Implementierung unbekannte Typen …   Deutsch Wikipedia

  • Generische Programmierung — ist ein Verfahren zur Entwicklung wiederverwendbarer Software Bibliotheken. Dabei werden Funktionen möglichst allgemein entworfen, um für unterschiedliche Datentypen und Datenstrukturen verwendet werden zu können. Die Implementierung erfolgt bei… …   Deutsch Wikipedia

  • Java (Technologie) — Java Logo Java Technik (englisch Java Technology) ist eine hauptsächlich von Sun Microsystems entwickelte Sammlung von Spezifikationen, die einerseits die Programmiersprache Java und andererseits verschiedene Laufzeitumgebungen für… …   Deutsch Wikipedia

  • Java (Framework) — Java Objektorientierte Programmiersprache Basisdaten Paradigmen: Objektorientierte Programmiersprache Aktuelle  …   Deutsch Wikipedia

  • Java (Softwaretechnologie) — Java Objektorientierte Programmiersprache Basisdaten Paradigmen: Objektorientierte Programmiersprache Aktuelle  …   Deutsch Wikipedia

  • Java-Syntax — Duke, das Java Maskottchen Die Syntax der Programmiersprache Java ist in der Java Language Specification definiert, ebenso wie die Semantik von Java. Dieser Artikel gibt einen Überblick über die Java Syntax und stellt einige ihrer Besonderheiten… …   Deutsch Wikipedia

  • Programmiersprache Java — Java Objektorientierte Programmiersprache Basisdaten Paradigmen: Objektorientierte Programmiersprache Aktuelle  …   Deutsch Wikipedia

  • Java Language Specification — Die Java Language Specification (kurz JLS, deutsch „Spezifikation der Sprache Java“) ist die Spezifikation der Semantik und Syntax der Programmiersprache Java. Sie wird von Sun Microsystems definiert und ist aktuell in der dritten Version… …   Deutsch Wikipedia

  • Java (Technik) — Java Logo Die Java Technik (englisch Java Technology) ist eine ursprünglich von Sun entwickelte Sammlung von Spezifikationen, die einerseits die Programmiersprache Java und andererseits verschiedene Laufzeitumgebungen für Computerprogramme… …   Deutsch Wikipedia

  • Vererbung (objektorientierte Programmierung) — Vererbung dargestellt mittels UML. Die abgeleitete Klasse hat die Attribute x und y und verfügt über die Methoden a und b (im UML Sprachgebrauch Operationen a und b). Die Vererbung (engl. Inheritance) ist eines der grundlegenden Konzepte der… …   Deutsch Wikipedia

Share the article and excerpts

Direct link
Do a right-click on the link above
and select “Copy Link”