Zähl auf mich – Java Enums

Rate this post

Eigentlich denkt man als erfahrener Entwickler oft, dass reine CORE Java Themen eher was für Anfänger sind. Das muss aber nicht unbedingt so sein. Ein Grund ist natürlich die Betriebsblindheit, die sich bei ‚Profis‘ mit der Zeit einstellt. Dieser Betriebsblindheit kann man nur entgegenwirken, wenn man sich gelegentlich auch mit vermeintlich vertrauten Themen erneut beschäftigt. Aufzählungen in Java sind leider zu Unrecht etwas unbeachtet und finden viel zu wenig Anwendung. Ein möglicher Grund für diese stiefmütterliche Behandlung können auch die vielen im Netz verfügbaren Tutorials zu Enums sein, die lediglich eine triviale Verwendung demonstrieren. Fangen wir zuallererst also damit an, wo Enums in Java ihre Verwendung finden.

Gehen wir einmal von ein paar einfachen Szenarien aus, die uns im Entwickleralltag begegnen. Stellen wir uns vor, wir müssen eine Hashfunktion implementieren, die mehrere Hash-Algorithmen wie MD5 und SHA-1 unterstützen kann. Als Einstiegspunkt haben wir auf Benutzerebene dann oft eine Funktion, die so oder ähnlich ausschauen kann: String hash(String text, String algorithm); – eine technisch korrekte Lösung.

Ein anderes Beispiel kann das Logging sein. Um einen Logeintrag zu schreiben, könnte die Methode wie folgt aussehen: void log(String message, String logLevel); und so lassen sich die Beispiele in diesem Stil beliebig fortführen.

Das Problem dieser Variante sind freie Parameter wie logLevel und algorithm, die für den korrekten Gebrauch nur eine sehr begrenzte Anzahl von Möglichkeiten gestatten. Das Risiko, dass Entwickler im Projektstress diesen Parameter falsch befüllen, ist recht hoch. Tippfehler, Variationen in Groß- und Kleinschreibung und so weiter sind mögliche Fehlerquellen. In der Java API gibt es durchaus einige solcher verbesserungswürdigen Designentscheidungen.

Um die Verwendung der API, also der nach außen genutzten Funktionen, zu verbessern, führen wie so oft viele Wege nach Rom. Ein Weg, der durchaus öfter zu sehen ist, ist die Verwendung von Konstanten. Um die Lösung ein wenig besser zu verdeutlichen, greife ich das Beispiel der Hash-Funktionalität auf. Es sollen die beiden Hash-Algorithmen MD5 und SHA-1 in der Klasse Hash unterstützt werden.

void Hash {
	
	public final static MD5 = "1";
	public final static SHA-1 = "2";

	/**
	 * Creates a hash from text by using the chosen algorithm.
	 */
	public String calculateHash(String text, String algorithm) {
		String hash = "";

		if(algorithm.equals(MD5)) {
			hash = MD5.hash(text);
		}
		if(algorithm.equals(SHA-1)) {
			hash = SHA-1.hash(text);
		}
		return hash;
	}
}

// Call
calculateHash("myHashValue", Hash.MD5);

In der Klasse Hash werden die beiden Konstanten MD5 und SHA-1 definiert, um anzuzeigen, welche Werte für den Parameter algorithem gültige Werte darstellen. Entwickler mit etwas Erfahrung erkennen diese Konstellation recht schnell und verwenden sie auch korrekt. Auch hier ist die Lösung technisch gesehen absolut korrekt. Aber auch wenn Dinge syntaktisch richtig sind, heißt es nicht, dass sie im semantischen Kontext bereits eine optimale Lösung darstellen. Die hier vorgestellte Strategie missachtet nämlich die Entwurfskriterien für objektorientierte Programmierung. Obwohl wir bereits durch die Konstanten gültige Eingabewerte für den Parameter algorithm bereitstellen, gibt es keinen Mechanismus, der die Verwendung der Konstanten erzwingt.

Nun könnte man argumentieren, das ist doch nicht wichtig, solange die Methode mit einem gültigen Parameter aufgerufen wird. Dem ist aber nicht so. Denn jeder Aufruf der Methode calculateHash ohne die Verwendung der Konstanten führt bei Refactorings zu möglichen Problemen. Will man aus Vereinheitlichung beispielweise den Wert “2“ in sha1 ändern, wird das überall da zu Fehlern führen, wo die Konstanten nicht zum Einsatz kommen. Wie können wir es also besser machen? Die bevorzugte Lösung sollte die Verwendung von Enums sein.

Alle in Java definierten Enums sind von der Klasse java.lang.Enum [2] abgeleitet. Eine Auswahl der Methoden ist:

  • name() gibt den Namen dieser Enum-Konstante genau so zurück, wie er in ihrer Enum-Deklaration angegeben ist.
  • ordinal() gibt die Ordnungszahl dieser Enum-Konstante zurück (ihre Position in der Enum-Deklaration, wobei der ersten Konstante die Ordnungszahl Null zugewiesen wird).
  • toString() gibt den Namen dieser Enum-Konstante so zurück, wie er in der Deklaration enthalten ist.

Enums gehören in Java von Anfang an zum Sprachkern und sind recht intuitiv zu benutzen. Für den Parameter algorithm erzeugen wir mit den beiden Werten MD5 und SHA1 die Enumklasse HashAlgorithm.

public enum HashAlgorithm {
MD5, SHA1
}

Dies ist die einfachste Variante, wie man ein Enum definieren kann. Eine einfache Aufzählung der gültigen Werte als Konstanten. In dieser Notation sind Konstantenname und Konstantenwert identisch. Wichtig beim Umgang mit Enums ist sich bewusst zu sein das die Namenkonventionen für Konstanten gelten.

Die Namen von als Klassenkonstanten deklarierten Variablen und von ANSI-Konstanten sollten ausschließlich in Großbuchstaben geschrieben und die Wörter durch Unterstriche („_“) getrennt werden. (ANSI-Konstanten sollten zur Vereinfachung der Fehlersuche vermieden werden.) Bsp.: static final int GET_THE_CPU = 1;

[1] Oracle Dokumentation

Diese Eigenschaft führt uns zum ersten Problem bei der vereinfachten Definition von Enums. Wenn wir aus irgendeinem Grund anstatt SHA-1 anstatt SHA1 definieren wollen, wird das zu einem Fehler führen, da “-“ nicht der Konvention für Konstanten entspricht. Daher findet sich für die Definition von Enums meist folgendes Konstrukt:

public enum HashAlgorithm {

    MD5("MD5"),
    SHA1("SHA-1");

    private String value;

    HashAlgorithm(final String value) {
        this.value = value;
    }

    public String getValue() {
        return this.value;
    }
}


In diesem Beispiel wird die Konstante SHA1 um einen String Wert “SHA-1“ erweitert. Um an diesen Wert zu gelangen, ist es notwendig, einen Zugriff dafür zu implementieren. Dieser Zugriff erfolgt über die Variable value, welche den zugehörigen Getter getValue() besitzt. Damit die Variable value auch befüllt wird, benötigen wir noch einen zugehörigen Konstruktor. Wie die verschiedenen Zugriffe auf den Eintrag SHA1 mit den entsprechenden Ausgaben zeigt der nachfolgende Testfall.

public class HashAlgorithmTest {

    @Test
    void enumNames() {
        assertEquals("SHA1",
                HashAlgorithm.SHA1.name());
    }

    @Test
    void enumToString() {
        assertEquals("SHA1",
                HashAlgorithm.SHA1.toString());
    }
    
    @Test
    void enumValues() {
        assertEquals("SHA-1",
                HashAlgorithm.SHA1.getValue());
    }


    @Test
    void enumOrdinals() {
        assertEquals(1,
                HashAlgorithm.SHA1.ordinal());
    }
}


Zuerst greifen wir auf den Namen der Konstante zu, der für unser Beispiel SHA1 lautet. Das gleiche Ergebnis erhalten wir mit der toString() Methode. Den Inhalt von SHA1, also den Wert SHA-1 erhalten wir über den Aufruf unserer definierten Methode getValue(). Zusätzlich kann noch die Position von SHA1 in der Enum abgefragt werden. Dies ist für unser Beispiel die zweite Stelle, an der SHA1 vorkommt, und hat dadurch den Wert 1. Schauen wir uns für die Implementierung der Hash-Klasse aus Listing 1 die Änderung für Enums an.

public String calculateHash(String text, HashAlgorithm algorithm) {
    String hash = "";
    switch (algorithm) {
        case MD5:
		hash = MD5.hash(text);
            break;
        case SHA1:
		hash = SHA1.hash(text);
            break;
    }
    return hash;
}

//CaLL
calculateHash(“text2hash“, HashAlgorithm.MD5);

Wir sehen in diesem Beispiel eine deutliche Vereinfachung der Implementierung und erreichen zudem eine sichere Verwendung für den Parameter algorithm, der nun durch die Enum bestimmt wird. Sicher gibt es einige Kritiker, die möglicherweise die Menge des zu schreibenden Codes bemängeln und darauf verweisen, dass dies viel kompakter möglich wäre. Solche Argumente kommen eher von Personen, die keine Erfahrung mit großen Projekten von mehreren Millionen Zeilen Quelltext konfrontiert wurden oder nicht in einem Team mit mehreren Programmierern zusammenarbeiten. Im objektorientierten Entwurf geht es nicht darum, möglichst jedes überflüssige Zeichen zu vermeiden, sondern lesbaren, fehlertoleranten, ausdruckstarken und leicht änderbaren Code zu schreiben. Alle diese Attribute haben wir in diesem Beispiel mit Enums abgedeckt.

Laut dem Tutorial von Oracle für Java Enums [3] sind noch viel komplexere Konstrukte möglich. Enums verhalten sich wie Java Klassen und können auch mit Logik angereichert werden. Die Spezifikation sieht hier vor allem die Methoden compare() und equals() vor, was Vergleichen und Sortieren ermöglicht. Für das Beispiel der Hashalgorithmen würde diese Funktionalität keinen Mehrwert bringen. Auch sollte man grundsätzlich davon Abstand nehmen, weitere Funktionalität beziehungsweise Logik in Enums unterzubringen, da diese ähnlich wie reine Datenklassen behandelt werden sollten.

Dass Enums durchaus ein wichtiges Thema in der Java Entwicklung darstellen, zeigt auch der Umstand, dass J. Bloch in seinem Buch “Effective Java“ dazu ein ganzes Kapitel mit knapp 30 Seiten gewidmet hat.

Damit haben wir ein ausführliches Beispiel über die korrekte Verwendung von Enums durchgearbeitet und vor allem auch erfahren, wo dieses Sprachkonstrukt in Java eine sinnvolle Anwendung findet.

Ressourcen

Abonnement / Subscription

[English] This content is only available to subscribers.

[Deutsch] Diese Inhalte sind nur für Abonnenten verfügbar.


Dieser Eintrag wurde von Elmar Dott unter Artikel veröffentlicht und mit , , , verschlagwortet. Setze ein Lesezeichen für den Permalink.
Elmar Dott

Über Elmar Dott

Elmar Dott realisiert seit über 20 Jahren als freier Berater in internationalen Projekten große Web-Applikationen. Seine Schwerpunkte sind DevOps, Konfigurationsmanagement, Software-Architekturen & Release Management. Als Trainer teilt er sein Wissen in Schulungen und spricht auch regelmäßig auf Konferenzen über aktuelle Themen.

Schreibe einen Kommentar