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 
Aufzählungstypen und ihre Fallstricke

Aufzählungstypen und ihre Fallstricke
Aufzählungstypen und ihre Fallstricke

JavaSPEKTRUM, Januar 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 ).

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 Überraschungen

Die 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 {
  NOENCODE, SSL
}
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 {
  NOENCODE, SSL;

  public static Connection createConnection(
                ConnectionType ct,
                SelectionKey key,
                int idx) {
    …
  }
}

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 {
  NOENCODE(SimpleConnection.class),
  SSL(SslConnection.class);

  public static Connection createConnection(
                ConnectionType ct,
                SelectionKey key,
                int idx) {
    Object[] params = { key, idx };
    return conCtors.get(ct.name()).newInstance(params);
  }

  private static final
    Map<ConnectionType,
        Constructor<? extends Connection>> conCtors
    = new HashMap<ConnectionType,
                  Constructor<? extends Connection>>();
  …
}

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 {
  NOENCODE(SimpleConnection.class),
  SSL(SslConnection.class);

  public static Connection createConnection(
                ConnectionType ct,
                SelectionKey key,
                int idx) {
    Object[] params = { key, idx };
    return conCtors.get(ct.name()).newInstance(params);
  }

  private static final
    Map<ConnectionType,
        Constructor<? extends Connection>> conCtors
    = new HashMap<ConnectionType,
                  Constructor<? extends Connection>>();

  private ConnectionType(Class<? extends Connection> c) {
    Class[] params = { java.nio.channels.SelectionKey.class,
                       java.lang.Integer.class };
    conCtors.put(this, c.getConstructor(params));
  }
}

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 initializer
                conCtors.put(this, c.getConstructor(params));
                ^
1 error
Um 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> {
  public static final ConnectionType NOENCODE;
  public static final ConnectionType SSL;
  private static final Map conCtors = new HashMap();
  private static final ConnectionType ENUM$VALUES[];
  static {
    NOENCODE = new ConnectionType("NOENCODE",0,SimpleConnection);
    SSL = new ConnectionType("SSL",1,SslConnection);
    ENUM$VALUES = (new ConnectionType[] {NOENCODE, SSL});
  }
  private ConnectionType(String s, int i, Class c) {
    super(s, i);
    Class params[] = {java/nio/channels/SelectionKey,
                      java/lang/Integer};
    conCtors.put(this, c.getConstructor(params));
  }
  ...
}
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 {
  NOENCODE(SimpleConnection.class), SSL(SslConnection.class);

  private static class StaticMapHelper {
    private static final
      Map<ConnectionType,
          Constructor<? extends Connection>> conCtors
      = new HashMap<ConnectionType,
                    Constructor<? extends Connection>>();
  }
  ...
  private ConnectionType(Class<? extends Connection> c) {
    Class[] params = { java.nio.channels.SelectionKey.class,
                       java.lang.Integer.class };
    StaticMapHelper.conCtors.put(this, c.getConstructor(params));
  }
}

Mehr Überraschungen

Nun 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 {
        NOENCODE(SimpleConnection.class), SSL(SslConnection.class);

        private static class StaticMapHelper {

            private static final Map<ConnectionType, Constructor<? extends Connection>>
              conCtors = new EnumMap<ConnectionType,
                                     Constructor<? extends Connection>>(ConnectionType.class);
        }

        public static Connection createConnection(ConnectionType ct,
                SelectionKey key, int idx) {
            try {
                Object[] params = { key, idx };
                return StaticMapHelper.conCtors.get(ct).newInstance(params);
            } catch (Exception e) {
                System.err.println("ERROR " + e
                        + " cannot create connection for: " + ct.name());
                return null;
            }
        }

        private ConnectionType(Class< ? extends Connection> c) {
            try {
                Class[] params = { java.nio.channels.SelectionKey.class,
                        java.lang.Integer.class };
                StaticMapHelper.conCtors.put(this, c.getConstructor(params));
            } catch (Exception e) {
                System.err.println("ERROR " + e
                        + "cannot create connection constructor for: "
                        + this.name());
            }

        }
    }
}

Exception in thread "main" java.lang.ExceptionInInitializerError
 at Connection$ConnectionType.<init>(Connection.java:37)
 at Connection$ConnectionType.<clinit>(Connection.java:12)
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
 at java.lang.reflect.Method.invoke(Unknown Source)
 at java.lang.Class.getEnumConstants(Unknown Source)
 at java.lang.Class.enumConstantDirectory(Unknown Source)
 at java.lang.Enum.valueOf(Unknown Source)
 at Test.parseServerconfig(Test.java:9)
 at Test.main(Test.java:25)
Caused by: java.lang.NullPointerException
 at java.util.EnumMap.<init>(Unknown Source)
 at Connection$ConnectionType$StaticMapHelper.<clinit>(Connection.java:17)
 ... 11 more

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>  {
  public EnumMap(Class<K> keyType) {
   this.keyType = keyType;
   keyUniverse = keyType.getEnumConstants();
   vals = new Object[keyUniverse.length];
  }
  ...
}
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.

Zusammenfassung

In 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

/JLS3/ The Java Language Specification, Third Edition
James Gosling, Bill Joy, Guy Steele, Gilad Bracha,
Addison Wesley, June 2005
URL: http://java.sun.com/docs/books/jls/
/SSL/ Wikipedia-Eintrag zu SSL/TLS
URL: http://de.wikipedia.org/wiki/Transport_Layer_Security

Die gesamte Serie über Enum-Typen:

/ENUM1/  Aufzählungstypen - Das Sprachmittel
Klaus Kreft & Angelika Langer
Java Spektrum, November 2006
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/28.Enums/28.Enums.html
/ENUM2/  Aufzählungstypen und ihre Fallstricke
Klaus Kreft & Angelika Langer
Java Spektrum, Januar 2007
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/29.EnumPitfall/29.EnumPitfall.htm

Leserzuschrift

Ein aufmerksamer Leser hat uns zu diesem Beitrag folgendes geschrieben:
 
Leserbrief
 
Hallo,

durch Ihren Newsletter angeregt habe ich gerade den Artikel "Aufzählungstypen und ihre Fallstricke" gelesen.

http://www.angelikalanger.com/Articles/EffectiveJava/29.EnumPitfall/29.EnumPitfall.html

Für das darin angeführte Problem, je nach enum Konstante verschiedene Connection Implementierungen zu erzeugen, gibt es auch eine andere, meiner Meinung nach elegantere, Lösung als die statische Factorymethode, nämlich die Factorymethode als Instanzmethode zu implementieren:

    enum ConnectionType
    {
      NOENCODE
      {
        public Connection createConnection( SelectionKey key, int idx)
        {
          return new SimpleConnection(key, idx);
        }
      },

      SSL
      {
        public Connection createConnection( SelectionKey key, int idx)
        {
          return new SslConnection(key, idx);
        }
      };

      public abstract Connection createConnection( SelectionKey key, int idx);

    }

Ich habe den Code nicht kompiliert, ich denke er müsste aber korrekt sein...

Schönen Gruß,
Christopher Sahnwaldt

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.
 
 

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-2012 by Angelika Langer.  All Rights Reserved.    URL: < http://www.AngelikaLanger.com/Articles/EffectiveJava/29.EnumPitfall/29.EnumPitfall.html  last update: 4 Nov 2012