Hin und Her mit Enums

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.