Generics sind seit der Java Version 5 Bestandteil der Sprache. Sie wurden von Entwicklern seit langem gefordert. Hintergrund ist auch die bisherige Typlosigkeit der Java Collection API. In Collections können vor Java 5 alle Datentypen, welche von Object abgeleitet wurden, abgelegt werden. Dieses Verhalten wird immer wieder gerne als Fehlerquelle herangezogen, um die Typsicherheit von Collections zu verlangen. Mit der Version 5 wurde dieser Forderung im letzten Jahr Rechnung getragen. Dies vermutlich auch vor dem Hintergrund, dass die .NET Plattform von Microsoft in der Version 2 ebenfalls einen Genericsmechanismus aufweisen wird.
Als ich noch mit C++ gearbeitet habe, habe ich auch gedacht, dass Generics, dort Templates genannt und mit etwas anderer Semantik versehen, ein sinnvolles Sprachkonstrukt sind. In C++ sind sie das auch, da es dort keine allgemeine Superklasse wie java.lang.Object gibt. Die C++ STL ist damit sicherlich einfacher zu bedienen. In Java hingegen sind Generics zumindest problematisch. Ich selbst hatte in den letzten 10 Jahren meines Arbeitens mit Java keinerlei Probleme mit der Collection API. Zumindest hatte ich keine der angeblich so häufig auftretenden casting Probleme. Warum nicht? Und warum sind Generics in diesem Zusammenhang nicht sinnvoll? Dies versuche ich im folgenden, anhand eines kleinen Beispiels, darzulegen.
Customer und Customers
Das Beispiel baut sich um eine Klasse Customer auf. Das Beispiel ist zwar etwas überstrapaziert, eignet sich aber hervorragend für die Beschreibung der Probleme. Ziel ist es eine Menge an Kunden zu verwalten. Ein erfahrener Entwickler wird sicherlich nicht brutal hingehen und alle Kunden in einer Collection ablegen. Er wird eine eigene Sammerklasse anbieten, in deren Implementierung er unterschiedliche Abfragen implementieren kann. Dies wird er schon machen, weil er weiss, dass diese Erweiterungsanforderungen immer kommen.
Die Klasse für eine Menge von Kunden nenne ich Customers. Um ihr neue Kunden hinzuzufügen bekommt sie eine add-Methode.
public class Customers {
private final List customers = new ArrayList();
public void addCustomer(final Customer c) {
customer.add(c);
}
}
Um eine Abfrage nach allen Benutzern durchzuführen werden unerfahrene Entwickler der Klasse eine Methode hinzufügen, die eine List zurück liefert, über deren Inhalt wiederum iteriert werden kann. So entsteht schnell arbeitender Code.
public class Customers {
private final List customers = new ArrayList();
public void addCustomer(final Customer c) {
customer.add(c);
}
public List<Customer> getCustomers() {
return customers;
}
}
Allerdings geht bei diesem Vorgehen die Kapselung verloren. Ein Benutzer der API weiss, dass die Kunden intern in einer Liste gespeichert werden. Ein Umstieg auf Sets ist nicht mehr möglich. Diesem Umstand könnte noch begegnet werden, indem an Stelle einer Liste ein Datentyp Collection zurück gegeben wird. Die Tatsache, dass die Kapselung aufgebrochen wird ändert sich dadurch nicht. Jetzt lassen sich nicht einfach neue, effektive Suchanfragen in der Klasse Customers implementieren. Um dies zu demonstrieren wird das Beispiel erweitert.
Die Applikation bekommt die ID aus einer anderen Quelle als unserem Customer. Dies ist durchaus möglich und nicht ungewöhnlich. Um in der Menge der Kunden jetzt denjenigen mit der ID heraus zu finden, muss die gesamte Kundenliste durchlaufen werden.
final String id = "123";
final List customerList = customers.getCustomer();
Customer customer = null;
// Java 5 syntax
for (Customer c : customerList) {
final String cid = c.getId();
if (id.equals(cid)) {
customer = c;
break;
}
}
// Do something additional
Sicherlich ist es nicht sinnvoll jedesmal einen grossen Teil der Liste zu durcklaufen, nur um den richtigen Kunden zu finden. Der Code ist ineffektiv. Sinnvoller ist es die ID eines Kunden als Schlüssel (Key) in einer Map abzulegen. Hierfür wird die add-Methode erweitert:
public class Customers {
private final List customers = new ArrayList();
private final Map customersById = new HashMap();
public void addCustomer(final Customer c) {
customer.add(c);
customersbyId.put(c.getId(), c);
}
// ...
}
Mit der Einführung eines neuen Containers in der Customers-Klasse muss es auf diese auch abfragende Zugriffe geben. Dem vorhergenden Code entsprechend, würde dies über einen Methode geschehen, welche die Map mit den ID zu Kunden Beziehung nach aussen führt:
public class Customers {
private final Map customersById = new HashMap();
// ...
public Map getCustomersById() {
return customersById;
}
}
In der zurückgegebenen Map sind jetzt allerdings keinerlei sematische Informationen mehr vorhanden. Es ist nicht bekannt, was es für einen Zusammenhang zwischen dem Schlüsseltypen String und dem Werttypen Customer gibt. Neben diesem gravierenden Problem wird allerdings auch das Arbeiten von Operationen auf den Mengen problematisch. Hierfür können nicht mehr einfach alle, der von den Containerklassen gelieferten, Methoden genutzt werden. Vielmehr muss der Benutzer jetzt, als Beispiel, zum löschen sowohl in der List und in der Map den jeweiligen Kunden löschen. Entsprechendes gilt für das hinzufügen. Um dies zu wissen, muss er aber wiederum wissen wie der, eigentlich gekapselte gehörte, innere Aufbau der Klasse aussieht. Hierfür muss er nicht unbedingt den Quellcode kennen, es reichen Hinweise in der Dokumentation. In Alternative kann der Entwickler der Klasse Customers Löschmethoden in der Klasse selbst anbieten und nur unveränderliche Container zurück liefern oder die Löschmethoden in den jeweiligen Containermethoden überschreiben, indem er z.B. diese in anonymen Klassen bei der Auslieferung umschliesst. Allerdings wird hierdurch der Code entweder unnötig aufgebläht oder die Semantik des nicht möglichen löschens oder hinzufügens im Container verwirrt den Benutzer. Der Sinn, durch Generics weniger Code zu produzieren wird ad absurdum geführt. Die Möglichkeit Fehler zu machen steigt enorm an.
Sinnvoll ist es die Methoden jeweils in der Klasse Customers selbst zu implementieren. Dadurch wird die Klasse leichter wartbar und für den Benutzer einfacher zu bedienen. Bei Änderungen, z.B. dem hinzufügen einer neuen Abfragemethode, muss nicht auch der benutzende Code angepasst werden. In diesem Fall reichen untypisierte Container innerhalb der kapselden Klasse vollkommen aus.
Fazit
Ich habe bisher keine unproblematische Verwendung der in Java 5 eingeführten Generics gefunden. Das hier beschriebene Beispiel zeigt auf einfache Art und Weise, wie man sich dem falschen Einsatz eines Sprachfeaturs, dessen Nutzen ins Gegenteil verwandeln kann. Aus diesem Grund sollte in den Codeingstandards eines Unternehmen auf den Einsatz von Generics besonders eingegangen werden. Bis zum Beweis des sinnvollen Gegenteils würde ich Generics sogar ganz verbieten.
Problematisch ist es aber doch, dass ich sogut wie keinerlei Vorteile durch die Generics habe, wenn sowieso alle Operationen, ausser dem traversieren durch eine Datenmenge, verboten sind. Es muss dann ein Mehraufwand für die Dokumentation betrieben werden, da ein Containerklasse ihrer eigentlichen Aufgaben, nämlich dem verwalten von Daten, beraubt wurde. Die, durch die Interfacesignatur, propagierten Möglichkeiten wie hinzufügen oder löschen sind nicht mehr gegeben und müssen doch wieder in der Customers-Klasse selbst abgebildet werden. Das heisst es sind Methoden auf zwei unterschiedlichen Klassen, die im Grunde die gleiche Aufgabe haben, verteilt. Die Problematik der nicht mehr sichtbaren Semantik bei der Map wird auch nicht gelöst, was wiederum die Fehlermöglichkeiten erhöht.
Mir erscheinen die Argumente noch nicht als ein Vorteil, um wirklich überzeugt zu sein, dass Generics unbedingt nötig sind. Vielmehr sehe ich eine stärker Komplexität in die Sprache einziehen, die sich nicht nur auf die Syntax und Semantik von Generics in Java beziehen. Sie gilt auch für den realen Einsatz, bei dem ein mehr an Aufwand betrieben werden muss, um sich gegen die aufkommenden Probleme zu wappnen. Wenn dieser Aufwand aber betrieben werden muss, der durch Generics eigentlich vermindert werden sollte, dann sehe ich deren Einsatz als nicht gerechtfertiogt an.

Ich kann in diesem Artikel keine Gegenargument gegen Generics finden --- eigentlich ist das Thema der korrekte Umgang mit Collections (generische oder nicht generische) bei der Entwicklung eingern Klassen, die eine "Sammlung" von Daten repräsentieren.
Grundsätzlich sollte eine eigene Klasse wie Customers eine Konvertierung in Java-Collections bereitstellen. Dies ermöglicht es erst solche Daten mit Hilfe von Standard-API-Methoden zu bearbeiten. (Sollte die Klasse Customers eigene Convenience-Methoden besitzen, so sind diese natürlich vorzuziehen.)
Es gibt zwei Fälle:
1. Fall: In diesem Fall sollte die Klasse eine Collection zurückliefern, die von AbstractCollection ableitet und nicht "mutable" ist. Dazu muss nur eine size-Methode sowie ein Iterator (mit hasNext und next, aber ohne remove) implementiert werden. In den meisten Fällen ist dies ein vertrettbarer Aufwand.
2. Fall: Wenn eine Klasse zur Implementierung eine Collection verwendet und diese nach aussen gibt, dann sollte dies in den meisten Fällen nur eine "unmutable colloction" sein. Ansonsten wird die Kapselung der Klasse wirklich durchbrochen.
Zur Erzeugung von "unmutable collections" gibt es entsprechende Wrapper, bereitgestellt durch die Klasse Collections:
public class Customers { private final List<Customer> customers = new ArrayList(); private final Map<String, Customer< customersById = new HashMap(); ... /** * Returns an unmodifiable collection of * the stored customers. * @return an unmodifiable collection */ public Collection<Customer> getCustomers() { return CollectionsCollections.unmodifiableCollection(customers); } /** Returns an unmodifiable map to retrieve * a customer from its ID. */ public Map<String, Customer> getCustomersById() { return Collections.unmodifiableMap(customersById); } }Grundsätzlich sollte lieber ein allgemeinerer Typ wie Collection zurückgegeben werden als ein konkreterer Typ wie List, um eben so wenig Details wie möglich über die Implementation bekanntzugeben.Falls Bedarf besteht die "exportierten" Collections eben doch mutable sein zu lassen, dann wird es ggf. komplizierter und man ist gezwungen die entsprechenden Methoden (u.a. add und remove) entsprechend anzupassen. Bei dem Export einer mutable Collection empfiehlt es sich auch diese "type safe" zu machen, d.h. gegen das Einfügen von Daten unerwarteten Typs sicher zu machen. (Dies geht mit Hilfe von Collections.checkedList usw.)
Fazit: Der Umgang mit Collections Bedarf einiger Sorgfalt --- sonst Durchbricht man schnell die Kapselung. Diese war schon so ohne Generics und ändert sich mit der Einführung von Generics kein Stück. Entwickeln ist halt nicht einfach...