Immer wieder kommt es vor, dass bei der Integration verschiedener Systeme von einer Enum Darstellung in eine andere konvertiert werden muss. Wer dem Fluch ausgesetzt ist, automatisch generierten Source-Code verwenden zu müssen, kennt dieses Ärgernis zur Genüge. Der Dummheit der Source-Code Generatoren gedankt, muss der Entwickler mit einer Vielzahl strukturell ähnlicher Klassen jonglieren.
In der fiktiven Problemstellung dieses Beitrags handelt es sich um die beiden Enum Klassen Richtung
und Himmelsrichtung
, deren Werte konvertiert werden müssen.
public enum Richtung { VOR, RECHTS, ZURUECK, LINKS } public enum Himmelsrichtung { NORD, OST, SUED, WEST }
Häufig hat der Entwickler eine der beiden Enum Klassen unter seiner Kontroller, dann kann er die Konvertierung durch eine explizite Verknüpfungen der Enum Werte erreichen.
public enum Himmelsrichtung { NORD(Richtung.VOR), OST(Richtung.RECHTS), SUED(Richtung.ZURUECK), WEST(Richtung.LINKS); private Richtung richtung; Himmelsrichtung(Richtung richtung) { this.richtung = richtung; } public getRichtung() { return richtung; } }
Diese Verknüpfung ist recht elegant und knapp formuliert. Obwohl ich die Möglichkeit der Enums sehr schätze, den Konstanten ein wenig Verhalten zu ermöglichen, hat diese Lösung einen leichten Beigeschmack. Wir bekommen eine feste Kopplung der Klasse Himmelsrichtung
zur Klasse Richtung
. Da beide Klassen in unterschiedlichen Modulen beheimatet sind erzeugen wir eine unnötige Abhängigkeit zwischen den Modulen.
Eine ganz andere Möglichkeit ist die Verwendung einer Konverter-Methode. die für eine Instanz der Klasse Himmelsrichtung
eine Instanz der Klasse Richtung
liefert. Wer es übersichtlich halten möchte, der verwendet eine switch
-Anweisung.
private Richtung map(HimmelsRichtung input) { if (input == null) { return null; } switch (Objects.requireNonNull(input, () -> "null not allowed")) { case NORD: return Richtung.VOR; ... case WEST: return Richtung.LINKS; default: return null; } }
Wenn der Entwickler lieber mit Arrays hantiert und die Konstanten die gleiche Reihenfolge besitzen, dann kann der Ordinalwert genutzt werden.
private Richtung map(HimmelsRichtung input) { if (input == null) { return null; } int index = input.ordinal(); Richtung[] values = Richtung.values() return index < values.length ? values[index] : null; }
Beide Varianten berechnen prozedural den Ausgabewert für einen speziellen Eingabewert. Für den Eingabewert null
gibt es beide Male eine separate Behandlung.
Alternativ dazu kann man eine bekannte Datenstruktur nutzen. In einer Map
werden die Konstanten von HimmelsRichtung
als Schlüssel und die Konstanten von Richtung
als Werte verwendet.
Map<HimmelsRichtung, Richtung> mapping = EnumUtil.mapping(HimmelsRichtung.class, Richtung.class); Richtung richtung = mapping.get(HimmelsRichtung.SUED);
Die Methode EnumUtil.mapping
erhält die beiden Enum Klassen als Parameter und erzeugt eine Map
, die Konstanten mit der gleichen Ordinalzahl paart.
public static <E extends Enum<E>, F extends Enum<F>> Map<F, E> mapping(Class<F> keyType, Class<E> valueType) { return getFeMap(keyType, keyType.getEnumConstants(), valueType.getEnumConstants()); } private static <E extends Enum<E>, F extends Enum<F>> Map<F, E> getFeMap(Class<F> keyType, F[] keys, E[] values) { if (values.length != keys.length) { throw new IllegalArgumentException("enum values does not match"); } Map<F, E> enumMap = new EnumMap<>(keyType); for (int i = 0; i < keys.length; i++) { enumMap.put(keys[i], values[i]); } return enumMap; }
Damit das nicht nur für HimmelsRichtung
und Richtung
funktioniert ist es eine generische Methode. Haben die Klassen eine unterschiedliche Kardinalität, dann wird ein Fehler geworfen.
Das Mapping ist sehr effizient, weil keine HashMap
verwendet wird, sondern die, speziell für Enum Klassen optimierte, EnumMap
. Was macht die EnumMap
so effizient?
Eine Enum Klasse enthält einen festen, unveränderlichen Satz von Konstanten und diese besitzen einen festen Ordinalwert. Daher reicht es völlig aus, ein Array für die Werte der Map
bereit zu halten. Der Zugriff auf einen Wert erfolgt über den Ordinalwert der Konstanten als Index im Array.
Warum verweist die Methode EnumUtil.mapping
auf die private Methode EnumUtil.getFeMap
und implementiert nicht direkt das Mapping? Es gibt einen weiteren trivialen Fall, der mit der EnumUtil.getFeMap
Methode implementiert werden kann. Bislang wurde nur der Fall betrachtet, dass die Zuordnung zwischen Konstanten mit gleichem Ordinalwert erfolg. Es kann aber auch vorkommen, dass eine andere Zuordnung notwendig ist, oder die Ziel-Enum eine geringere Kardinalität hat. Wie etwa im folgenden Beispiel.
Map<HimmelsRichtung, Achse> mapping = EnumUtil.mapping(HimmelsRichtung.class, Achse.X, Achse.Y, Achse.X, Achse.Y); Achse achse = mapping.get(HimmelsRichtung.SUED);
Die Werte von HimmelsRichtung
können nur auf die beiden Konstanten X
und Y
in der Enum Achse
übersetzt werden. Dazu wird eine alternative mapping
-Methode verwendet. Bei ihr werden die Werte der Map als Vararg Argumente übergeben. Die Reihenfolge der Werte entspricht dabei der Reihenfolge der Konstanten in der Enum HimmelsRichtung
. Da hier in Java intern ein Array verwendet wird, kann diese EnumUtil.mapping
Implementierung direkt auf die EnumUtil.getFeMap
Methode zurückgreifen.
public static <E extends Enum<E>, F extends Enum<F>> Map<F, E> mapping(Class<F> keyType, E... values) { return getFeMap(keyType, keyType.getEnumConstants(), values); }
Werden noch speziellere Mappings benötigt, dann sollte die EnumMap
direkt mit den Schlüssel-Wert-Paaren befüllt werden.