|
|||||||||||||||||||
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | CONTACT | | | | |||||||||||||||||||
|
Aufzählungstypen und ihre Fallstricke
|
||||||||||||||||||
Wir haben in der letzten Ausgabe unserer Kolumne die Aufzählungstypen
vorgestellt, die als neues Sprachmittel in Java 5.0 zur Sprache hinzugekommen
sind. Dieses Mal wollen wir einige Erfahrungen aus der praktischen
Verwendung von Aufzählungstypen hernehmen, um einige Fallstricke zu
erläutern.
Unliebsame ÜberraschungenDie Aufzählungstypen in Java sind nicht nur simple Aufzählungen von symbolischen Konstanten, sondern die einzelnen Enum-Objekte können Felder und Methoden haben. Nicht selten definiert ein Enum-Typ auch statische Felder - und hier können sich Probleme ergeben.Das folgende Beispiel stammt aus einer konkreten Anwendung, bei der wir einen Aufzählungstyp mit 2 möglichen Werten benötigen, der die verschiedenen Arten der Kommunikation in der Anwendung abbilden soll: entweder unverschlüsselt oder über SSL. Bei SSL (= Secure Socket Layer, offiziell TLS = Transport Layer Security) werden die übertragen Daten verschlüsselt, damit sie nicht abgehört werden können. (Mehr zu SSL und TLS ist unter / SSL / zu finden.) Unser Aufzählungstyp sieht im ersten Ansatz zunächst so aus: enum ConnectionType {Des weiteren sollen müssen abhängig vom ConnectionType unterschiedliche Verbindungen hergestellt werden. Diese Verbindungen sind bereits durch zwei Connection-Typen repräsentiert: eine SimpleConnection und eine SslConnection, die beide ein gemeinsames Connection-Interface implementieren. Das Wissen darüber, welche Art von Connection zu welchem ConnectionType erzeugt werden muß, wollen wir in eine Factory-Methode legen. Die Factory-Methode ist so gedacht, daß sie einen der beiden Werte vom Enum-Typ ConnectionType zusammen mit allen weiteren benötigten Daten als Parameter übergeben bekommt und dann entweder eine SimpleConnection oder eine SslConnection erzeugt. Diese Factory-Methode wollen wir als statische Methode im Enum-Typ ConnectionType implementieren, weil sie logisch eng mit dem Verbindungstyp zusammenhängt, den der ConnectionType repräsentiert. Unser Enum-Typ sieht dann so aus: enum ConnectionType {Damit die Factory-Methode weiß, welche Art von Connection zu welchem ConntectionType gehört, muß diese Zuordnungsinformation im Aufzählungstyp abgelegt werden. Wir haben zu diesem Zweck dem Enum-Typ ein statisches Feld vom Type Map gegeben. In der Map wird dem ConnectionType direkt der Konstruktor der zugehörige Connection-Klasse zugeordnet. Die zugrunde liegende Idee ist: die Factory-Methode sucht in der Map zum jeweiligen ConntectionType den Konstruktor der zugehörigen Connection-Klasse und erzeugt per Reflection ein entsprechendes Connection-Objekt. Hier ist der ConnectionType mit der statischen Map: enum ConnectionType {Der Typ der Map ist Map<ConnectionType, Constructor<? extends Connection>>, weil der Schüssel einer der Enum-Werte aus unserem Enum--Type ConnectionType ist und der zugeordnete Werts ein Konstruktor für eine Connection. Nun ist noch offen, wann und wie diese Map mit Inhalt gefüllt wird. Man könnte alle Key-Value-Paare in einem static-Initializer auf einmal eintragen. Das hätte aber den Nachteil, daß dieser static-Initializer immer dann angepaßt werden müßte, wenn ein neuer Subtyp von ConnectionType dazukommt. Wir wollen eine wartungsfreundlichere und flexiblere Lösung. Unsere Überlegung ist: jeder Enum-Wert fügt seine eigenes Key-Value-Paar in die statische Map ein, nämlich sich selbst zusammen mit dem zugehörigen Konstruktor. Die einzelnen Enum-Werte registrieren sich sozusagen zusammen mit dem Konstruktor der zugehörigen Connection-Klasse in der Map. Diese Registrierung wird am sinnvollsten während der Konstruktion der jeweiligen Enum-Werte gemacht, damit sie nicht vergessen wird. Unser Enum-Typ benötigt also einen Konstruktor. Diesem Konstruktor wird die Typrepräsentation, d.h. das Class-Objekt, der zum ConnectionType gehörenden Connection-Klasse angegeben. Der Konstruktor des übergebenen Typs wird dann in der Map abgelegt. Unser Enum-Typ sieht dann wie folgt aus: enum ConnectionType {An diesem Punkt sieht unsere Lösung eigentlich recht vielversprechend aus. Aber leider mag der Compiler sie ganz und gar nicht. Er bemängelt, dass der Zugriff auf die statische Map im Konstruktor nicht erlaubt ist. Connection.java:31: illegal reference to static field from initializerUm diese Fehlermeldung zu verstehen, muß man sich daran erinnern, was der Compiler aus der Definition eines Enum-Typs macht. Wir hatten das im unserem letzten Artikel ausführlich diskutiert /ENUM1/. Er generiert aus dem Enum-Typ eine Klasse und die einzelnen Enum-Werte sind statische Objekte, die in einem static-Initializer konstruiert werden. Das bedeutet, daß der Konstruktur unseres Enum-Typs in einem static-Initializer aufgerufen wird. Dieser Konstruktor greift seinerseits auf die statische Map zu, um dort Einträge zu machen. In dieser Situation ist ungeklärt, wie die Initialisierungsreihenfolge ist. Wird erst die statische Map konstruiert oder erst die Enum-Objekte? Das hängt davon ab, was der Compiler generiert, und wir haben keinerlei Einfluß auf die Initialisierungsreihenfolge. In unserem konkreten Beispiel hätte der Compiler das Folgende generieren können: class ConnectionType extends Enum<ConnectionType> {Wenn der Compiler es so macht wie oben gezeigt, dann besteht hier kein Problem. Die Map wird konstruiert, ehe im Static-Initializer-Block die Konstruktoren der Enum-Werte gerufen werden, in denen auf die Map zugegriffen wird. Es ist aber in der Sprachspezifikation nicht festgelegt, was der Compiler genau zu generieren hat und wie die genaue Initialisierungsreihenfolge von Enum-Werten und statischen Feldern des Enum-Typs auszusehen hat. Jeder Compiler kann die Initialisierungen anders generieren und deshalb gibt es keine Garantie, daß die Initialisierungsreihenfolge so ist, wie wir sie in diesem Beispiel brauchen. Zwar könnte ein intelligenter Compiler prinzipiell den Sourcecode auf Initialisierungsabhängigkeiten hin analysieren und die jeweils korrekte Initialisierungsreihenfolge zu generieren versuchen. In unserem einfachen Beispiel wäre das sicher möglich gewesen. Aber ganz im Allgemeinen ist eine solche Analyse zu komplex, als dass man sie von einem Compiler in allen Fällen erwarten könnte. Die uns bekannten Compiler (z.B. der Compiler in der Java Standard Edition von Sun oder der Compiler im Eclipse IDE) machen keine aufwendige Analyse, sondern bemühen sich, eventuelle Probleme durch entsprechende Fehlermeldungen zu verhindern. Deshalb führen Enum-Typen mit statischen Feldern gelegentlich zu prophylaktischen und unter Umständen überraschenden Fehlermeldungen, wie der gezeigten. Was macht man nun in einer solchen Situation? Wir müssen irgendwie erreichen, daß die Map bereits konstruiert ist, ehe auf sie zugegriffen wird. Das Problem läßt sich mit Hilfe einer zusätzlichen Klasse lösen. Die fragliche Map haben wir als privates statisches Feld in eine separate Hilfsklasse eingepackt. Der Zugriff auf die Map erfolgt daher stets über die umgebende Hilfsklasse. Der Compiler muß, ehe er auf die Map zugreift, die Hilfsklasse laden und wird dabei die Klasseninitialisierung machen, d.h. alle statischen Felder initialisieren und alle static-Initializer der Hilfsklasse ausführen. Diese Technik garantiert, daß die Hilfsklasse - und damit auch ihr statisches Feld - vor dem ersten Zugriff initialisiert wird. Das ist unabhängig von den Initialisierungen, die der Compiler für die synthetische Enum-Klasse und ihre statischen Teile generiert, und funktioniert immer. Hier unsere Lösung: enum ConnectionType { Mehr ÜberraschungenNun könnte man aufgrund der vorsorglichen Compiler-Meldung im obigen Beispiel auf den Gedanken kommen, daß die Benutzung von Enum-Typen relativ sicher ist, weil der Compiler generell versucht, alle Fallstricke durch entsprechende Fehlermeldungen schon im Vorfeld auszuschalten. Leider ist das nicht so.In dem oben gezeigten Beispiel hatten wir eine Map verwendet, deren Key-Typ ein Enum-Typ ist, nämlich unser ConnectionType. Für solche Maps gibt es eine effizientere Implementierung, die sogenannte EnumMap. In dieser EnumMap wird ausgenutzt, daß es nur eine beschränkte Anzahl von Werten des Key-Typs gibt. Der Key-Typ kann deshalb effizient auf Bits abgebildet werden und die EnumMap ist in solch einer Situation performanter als die normale HashMap oder TreeMap.
public static enum ConnectionType {
private static class StaticMapHelper {
private static final Map<ConnectionType, Constructor<? extends Connection>>
public static Connection
createConnection(ConnectionType ct,
private ConnectionType(Class<
? extends Connection> c) {
}
Exception in thread "main" java.lang.ExceptionInInitializerError
Auslöser dieses Initialisierungsfehlers ist eine NullPointerException von der new-Expression, in der die EnumMap erzeugt wird. Aus ungeklärten Gründen wirft der Konstruktor der EnumMap eine unerwartete NullPointerException. Das Initialisierungsproblem wird verursacht durch die Implementierung der EnumMap. In der EnumMap werden nämlich die Keys in einem Cache gehalten. Hier die relevanten Details aus der Implementierung der EnumMap im JDK von Sun Version 1.5.0_05: class EnumMap<K extends Enum<K>,V> {Dieser Key-Cache wird im Konstruktor der EnumMap angelegt und dabei wird auf die Enum-Werte zugegriffen - offenbar in der Annahme, daß der Enum-Type zu diesem Zeitpunkt bereits geladen und vollständig initialisiert ist und daß die Enum-Werte bereits existieren. Das ist in unserem Beispiel aber nicht der Fall. Wir haben ja extra dafür gesorgt, daß die Map konstruiert wird, ehe die Enum-Werte konstruiert werden. Hier beißt sich nun die Katze in den Schwanz. Eine statische EnumMap in einem Enum-Typ, der im Konstruktor auf die statische EnumMap zugreifen will, ist im JDK 5.0 von Sun nicht möglich. Wir haben deshalb auf die Verwendung einer EnumMap verzichten müssen und statt dessen eine HashMap verwendet. ZusammenfassungIn diesem Beitrag haben wir uns einige typische Fallstricke beim Umgang mit Aufzählungstypen angesehen. Die typischen Fallstricke sind Initialisierungsfehler, die dann entstehen, wenn der Enum-Typ statische Felder hat.Das dargestellte Beispiel hat gezeigt, wie verzwickt der Umgang mit Enum-Typen sein kann. Als Implementierer eines Enum-Typs, der statische Felder hat, muß man sehr genau auf die Reihenfolge der statischen Initialisierungen achten und mit unliebsamen Überraschungen rechnen. Unserer Erfahrung nach sind statische Felder in Enum-Typen aber keineswegs selten. Da praktisch alles in einem Enum-Typ statisch ist, ist die Verwendung von weiteren statischen Elementen sogar relativ naheliegend und natürlich. In unserem Beispiel haben wir das Problem mit der statischen Map durch eine Hilfsklasse lösen können. Gewiß wäre es auch möglich gewesen, auf die statische Map ganz zu verzichten, so wie wir auch auf die Verwendung der EnumMap verzichtet haben. Ganz generell bleibt aber die Erkenntnis, daß die Verwendung von statischen Feldern in Aufzählungstypen naheliegend und gleichzeitig fehleranfällig ist. Das ist aber auch der einzige Bereich, in denen Aufzählungstypen Kopfschmerzen bereiten - ganz im Gegensatz zu den Generics, denen wir in den nächsten Beiträgen dieser Kolumne widmen werden. Literaturverweise und weitere Informationsquellen
Die gesamte Serie über Enum-Typen:
LeserzuschriftEin aufmerksamer Leser hat uns zu diesem Beitrag folgendes geschrieben:
Ja, der Code ist korrekt und Herr Sahnwaldt hat völlig Recht. Wenn man auf die statische Map komplett verzichten kann, dann ist es deutlich eleganter. Unser Beispiel stammt aus einem in der Praxis aufgetretenem Fall, den wir zum Zwecke der Erläuterung in unserem Artikel drastisch vereinfacht haben. Im Originalkontext enthielt die Map noch eine ganze Menge anderer Informationen, so dass man um die statische Map leider nicht ohne Weiteres herum kam.
Es bleibt also festzuhalten: wenn man statische Felder in Enum-Typen
vermeiden kann, dann ist es sicher eine gute Idee, es zu tun; wenn man
sie nicht vermeiden kann oder will, dann kann man den etwaigen Problemen
mit den oben geschilderten Techniken begegnen.
|
|||||||||||||||||||
© Copyright 1995-2012 by Angelika Langer. All Rights Reserved. URL: < http://www.AngelikaLanger.com/Articles/EffectiveJava/29.EnumPitfall/29.EnumPitfall.html> last update: 4 Nov 2012 |