Blog

Dynamische Enums in Java

Andrew Phillips

Aktualisiert Oktober 23, 2025
8 Minuten

Mit der Einführung von Enums in Java 5 wurde endlich eine leistungsfähige (und leicht magische) Möglichkeit geschaffen, feste Mengen zu modellieren. Man kann jedoch auch zu viel des Guten haben, und in manchen Situationen ist eine Enum ein wenig zu starr, um praktisch zu sein. Hier möchte ich ein Muster diskutieren, das ich - in Ermangelung eines besseren Namens - die dynamische Enum nenne, die dieses Problem angeht. "Dynamische Enums"? Ist das nicht ein bisschen wie 'typsichere Skriptsprache' oder 'vollständig spezifizierte Anforderungen'?" Nun, lassen Sie mich versuchen, den Zweck einer dynamischen Enum zu umreißen:Nehmen wir an, dass die Menge der Länder der Welt (alle 203, zum Zeitpunkt des Schreibens) Teil Ihres Domänenmodells ist. Natürlich ist die Anzahl der Länder begrenzt, daher ist String keine gute Wahl.Wie wäre es mit einer Enum? Nun, die Liste der Länder ist auch nicht so starr - jedes Mal, wenn Sie eine Nachricht über die Invasion eines Landes oder die Ausrufung der Unabhängigkeit sehen, würden Sie denken: "Ich muss daran denken, das Programm morgen neu zu kompilieren und eine Notfallversion zu beantragen." Eine vernünftige Lösung besteht darin, eine Liste von Ländern zu führen (z.B. in einer Datenbanktabelle oder einer Ressourcendatei) und diese beim Start der Anwendung in den Speicher zu laden. Das Hinzufügen eines Landes erfordert dann lediglich einen Neustart der Anwendung. Aber wie können Sie überprüfen, ob eine bestimmte String- oder - je nach Modellierungswahl - Country-Instanz tatsächlich eines der Länder repräsentiert? Nun, eine Art von

CountryRepository.get(countryIsoCode) != null

check könnte verwendet werden, aber das ist eine völlig anwendungsspezifische Logik. Ein anderes Szenario: Nehmen wir an, Sie bauen eine HR-Anwendung für den Geheimdienst Ihrer Majestät. Eines der Schlüsselelemente des Domänenmodells wird Ihr AgentRecord sein, der Dinge wie Codenummer, Name, Lizenz zum Töten, Anzahl der Tötungen, Gesamtwert des beschädigten Eigentums usw. enthalten könnte.Das sind ziemlich schwergewichtige Objekte, die man mit sich herumschleppen muss - Sie möchten wahrscheinlich nicht, dass Moneypenny aus einer Dropdown-Liste von AgentRecords auswählen muss. Idealerweise hätten wir gerne eine Art "leichtgewichtige Darstellung", die 1:1 mit den AgentRecords korrespondiert.Es gibt eine solche "natürliche" Darstellung: die Codenummer. Aber wenn wir das in unserem Code verwenden, werden wir wahrscheinlich mit Dingen enden wie

BureaucracyService.recordDamageCaused(int agentCodeNumber, int damageInMillionGBP)

was sicherlich weniger selbstdokumentierend ist als

BureaucracyService.recordDamageCaused(Agent agent, int damageInMillionGBP)

Hier ist Agent ein "enum-ähnliches" Objekt, das wie folgt aussehen könnte

public class Agent {
  public final int codeNumber;
  public Agent(int codeNumber) {
  this.codeNumber = codeNumber;
  }
}

oder Sie könnten einen Getter hinzufügen, wenn Ihnen die Idee eines öffentlichen endgültigen Feldes nicht gefällt. "Aber warten Sie einen Moment! Was soll Sie daran hindern, einen Agenten mit einer nicht existierenden Codenummer zu erstellen? Schlimmer noch, was ist, wenn Ihre Geschäftslogik irgendwo new Country("Sowjetunion") verwendet? Wenn Sie Countries.SOVIET_UNION verwenden und dann die Enum-Konstante entfernen, würden Sie einen Kompilierungsfehler erhalten. Aber jetzt haben Sie einfach eine Zeitbombe in Ihrem Code!" Sehr wahr. Das scheint einfach der Preis zu sein, den Sie für Ihr Mittagessen zahlen müssen. Mögliche Ansätze zur Abschwächung dieses Risikos bestehen darin, die Verwendung bestimmter Enum-Konstanten auf ein Minimum zu beschränken (was nicht immer machbar ist) oder zu entscheiden, dass Elemente nur zu Ihrer dynamischen Enum hinzugefügt, nicht aber entfernt werden dürfen. In jedem Fall scheinen dynamische Enums für Systeme, in denen Enum-Konstanten nicht "dynamisch", d.h. auf der Grundlage von Benutzereingaben, ausgewählt werden, weniger relevant zu sein. Vergleichen Sie

Klasse TrafficLight {
  private static enum Status { ROT, GRÜN }
  privaten Staat Staat;
  void changeState(boolean goGreen) {
  state = (goGreen ? State.GREEN : State.RED);
  }
}

ein Beispiel für eine "statische Auswahl", mit

Klasse TrafficLight {
  ...
  void changeState(String targetState) {
  state = State.valueOf(targetState);
  }
}

was verdeutlicht, was ich mit "dynamischer" Auswahl meine. In diesem letzteren Fall besteht immer die Möglichkeit, dass der Wert ungültig ist, wenn targetState nicht vertrauenswürdig ist (z.B. aus einer HTTP-Anfrage stammt, die von einer Benutzeroberfläche übermittelt wurde). Die Verwendung einer "echten" Enum ändert daran nichts! Wenn State nun ein dynamisches und kein echtes Enum wäre (was es mir z.B. erlauben würde, AMBER hinzuzufügen, ohne neu kompilieren zu müssen), wäre es dann nicht praktisch, wenn ich valueOf, values und all die anderen Methoden, die wir von Enums gewohnt sind, aufrufen könnte und sie sich auf die gleiche Weise verhalten würden? Das scheint mir auf jeden Fall besser zu sein, als den Code für den Aufruf von stateRepository.find(targetState) oder einer anderen, nicht standardisierten Methode umschreiben zu müssen. An dieser Stelle kommen DynamicEnum und DynamicEnumerable ins Spiel. Ein schwergewichtiges Objekt wie AgentRecord würde DynamicEnumerableAgent implementieren, um anzuzeigen, dass die Menge der Agentendatensätze im Wesentlichen fest ist und durch eine Menge leichtgewichtiger Agent-Objekte dargestellt werden kann. Da Java-Enums jede Enum-Konstante mit einem String-Wert verknüpfen, muss eine schwergewichtige DynamicEnumerable auch einen String-Namen definieren, der mit ihrer leichtgewichtigen Darstellung verknüpft ist.

public interface DynamicEnumerable {
  String name();
  E enumValue();
}
public AgentRecord implements DynamicEnumerable {
  int codeNumber;
  String-Name;
  boolean licenceToKill;
  int numKills;
  long totalDamagedPropertyValue; // in GBP * 100
  public String name() {
  return "00" + Integer.toString(codeNumber);
  }
  public Agent enumValue() {
  return new Agent(codeNumber);
  }
}

Die DynamicEnum selbst ist im Wesentlichen eine Fabrik für die dynamischen Enum-Konstanten, die eine bestimmte DynamicEnumerable darstellen. Sie bietet alle Funktionen, die über die statischen Methoden einer regulären Enum verfügbar sind, wie z.B. Werte.

public interface DynamicEnum<e, D erweitert DynamicEnumerable>  erweitert Comparator {
  boolean exists(E enumValue);
  E valueOf(String name);
  Liste Werte();
  int ordinal(E enumValue);
  Bereich festlegen(E von, E bis);
  D backingValueOf(E enumValue);
}

Darüber hinaus bietet die backingValueOf-Methode eine Möglichkeit, auf das schwergewichtige "Backing"-Objekt zuzugreifen, das einer dynamischen Enum-Konstante entspricht. Dies könnte z.B. erforderlich sein, um einen Bildschirm "Agentendetails" zu füllen.Da die praktischen Aspekte des Abrufs und die Regeln für die Anordnung und Aktualisierung der unterstützenden Objekte vollständig implementierungsspezifisch sind, handelt es sich bei DynamicEnum um eine Schnittstelle und nicht um eine übergeordnete Klasse. Eine Folge davon ist, dass im Gegensatz zu einer regulären Enum-Klasse die Werte usw. sind keine statischen Methoden der leichtgewichtigen Klasse. Im Prinzip könnte die leichtgewichtige Klasse DynamicEnum selbst implementieren, aber mit all den zusätzlichen (Instanz!) Methoden und dem Code zu deren Implementierung wäre sie kaum noch leichtgewichtig. Stattdessen wird die erwartete Verwendung wahrscheinlich ein einfaches, normalerweise unveränderliches Objekt wie Agent als leichtgewichtiges Objekt, und ein Agenten Repository oder Fabrik, die DynamicEnum<Agent, AgentRecord> implementiert und für das Laden von AgentRecords und die Pflege der Korrespondenz zwischen Agents und ihren unterstützenden AgentRecords verantwortlich ist . Das vielleicht einfachste, aber keineswegs ungewöhnliche Szenario ist eine dynamische Enum, deren Mitglieder in einer statischen Ressource oder Datenbanktabelle definiert sind. Diese wird beim Start der Anwendung geladen, in der Annahme, dass sie sich nicht ändert, während die Anwendung läuft. Eine Änderung der Enum erfordert daher einen Neustart der Anwendung, aber das ist oft immer noch viel bequemer als eine Neukompilierung und ein erneutes Deployment. Hier ist eine einfache DynamicEnum-Implementierung, die diesen Fall behandelt. Sie verwendet ein Repository, um die Backing-Objekte aus einer bestimmten Ressource zu laden. Die Reihenfolge, in der das Repository die Elemente zurückgibt, bestimmt die Reihenfolge der Enum.

public interface DynamicEnumerableRepository {
  Liste loadAll();
}
public class StaticResourceBackedDynamicEnum<e, D erweitert DynamicEnumerable>
  implementiert DynamicEnum {
  private final List orderedDynamicEnumValues;
  private final Map dynamicEnumValues;
  private final Map dynamicEnumValueNames;
  private class DynamicEnumValueDescriptor {
  private int ordinal;
  private D backingObject;
  private DynamicEnumValueDescriptor(int ordinal, D backingObject) {
  this.ordinal = ordinal;
  this.backingObject = backingObject;
  }
  }
  public StaticResourceBackedDynamicEnum(
  DynamicEnumerableRepository dynamicEnumerableRepository) {
  List dynamicEnumerables = dynamicEnumerableRepository.loadAll();
  int numDynamicEnumerables = dynamicEnumerables.size();
  orderedDynamicEnumValues = new ArrayList(numDynamicEnumerables);
  dynamicEnumValues = new HashMap(numDynamicEnumerables);
  dynamicEnumValueNames = new HashMap(numDynamicEnumerables);
  for (int i = 0; i  <  numDynamicEnumerbles; i++) {
  D dynamicEnumerable = dynamicEnumerables.get(i);
  E dynamicEnumValue = dynamicEnumerable.enumValue();
  // ...
  orderedDynamicEnumValues.add(dynamicEnumValue);
  dynamicEnumValues.put(dynamicEnumValue,
  new DynamicEnumValueDescriptor(i, dynamicEnumerable));
  dynamicEnumValueNames.put(dynamicEnumerable.name(), dynamicEnumValue);
  }
  }
  public boolean exists(E enumValue) {
  return dynamicEnumValues.containsKey(enumValue);
  }
  public E valueOf(String name) {
  // ...
  return dynamicEnumValueNames.get(name);
  }
  // ...
  public Liste Werte() {
  return new ArrayList(orderedDynamicEnumValues);
  }
  public int ordinal(E enumValue) {
  // ...
  return dynamicEnumValues.get(enumValue).ordinal;
  }
  public int compare(E enumValue1, E enumValue2) {
  // ...
  return (ordinal(enumValue1) - ordinal(enumValue2));
  }
  public Set Bereich(E von, E bis) {
  // ...
  // subList behandelt den Index "to" als *exclusive*, aber *inclusive* ist hier erforderlich
  return new HashSet(orderedDynamicEnumValues.subList(
  orderedDynamicEnumValues.indexOf(from), orderedDynamicEnumValues.indexOf(to) + 1));
  }
  public D backingValueOf(E enumValue) {
  // ...
  return dynamicEnumValues.get(enumValue).backingObject;
  }
}

Der größte Teil der Argumentprüfung wurde in dem Codeausschnitt weggelassen. Die Implementierung ist auch nicht besonders effizient - es gibt jede Menge Duplikation in den drei wichtigsten internen Datenstrukturen orderedDynamicEnumValues, dynamicEnumValues und dynamicEnumValueNames, die zweifellos optimiert werden könnten. Der Code erwartet auch, dass das zugrunde liegende Repository im Konstruktor verwendbar ist. Wenn das Repository eine Transaktion benötigt, um auf eine Datenbank zuzugreifen, ist dies möglicherweise nicht der Fall, z.B. wenn die deklarative Transaktionsverwaltung von Spring verwendet wird. Hier könnte ein Initialization on Demand Holder oder ein anderes geeignetes Singleton-Initialisierungsmuster verwendet werden. In jedem Fall ist die Erstellung eines dynamischen AgentsEnum so einfach wie die Erstellung eines dynamischen AgentsEnumValues, vorausgesetzt, es gibt ein AgentRecordRepository, das alle AgentRecords aus Moneypennys altem Aktenschrank lesen kann.

public class Agents extends StaticResourceBackedDynamicEnum {
  public Agents(DynamicEnumerableRepository agentRecordRepository) {
  super(agentRecordRepository);
  }
}

Dies ermöglicht Ihnen, Code zu schreiben wie:

Agents agents = new Agents(agentRecordRepository);
assert agents.valueOf("007").equals(new Agent(7)) // unter der Annahme, dass Agent "gleich" für die Codenummer definiert
assert agents.values().contains(new Agent(8)); // 008 ('Bill') erscheint in Goldfinger
assert (agents.exists(new Agent(-73)) == false);
assert (agents.range(new Agent(1), new Agent(7)).size() == 5); // 001, 004, 005, 006 und 007
assert agents.backingValueOf(new Agent(7)).hasLicenseToKill();

Den Quellcode und ein Maven-Projekt finden Sie hier.

Verfasst von

Andrew Phillips

Contact

Let’s discuss how we can support your journey.