Eine Anleitung zu Java-Enums

1. Übersicht

In diesem Artikel werden wir sehen, was Java-Enums sind, welche Probleme sie lösen und wie einige der Entwurfsmuster in der Praxis verwendet werden können.

Das Schlüsselwort enum wurde in Java 5 eingeführt. Es bezeichnet einen speziellen Klassentyp, der die Klasse java.lang.Enum immer erweitert . Die offizielle Dokumentation zu ihrer Verwendung finden Sie in der Dokumentation.

Auf diese Weise definierte Konstanten verbessern die Lesbarkeit des Codes, ermöglichen die Überprüfung der Kompilierungszeit, dokumentieren die Liste der akzeptierten Werte im Voraus und vermeiden unerwartetes Verhalten aufgrund der Übergabe ungültiger Werte.

Hier ist ein schnelles und einfaches Beispiel für eine Aufzählung, die den Status einer Bestellung für eine Pizza definiert. Der Bestellstatus kann BESTELLT , BEREIT oder LIEFERBAR sein :

public enum PizzaStatus { ORDERED, READY, DELIVERED; }

Darüber hinaus enthalten sie viele nützliche Methoden, die Sie sonst selbst schreiben müssten, wenn Sie herkömmliche öffentliche statische Endkonstanten verwenden würden.

2. Benutzerdefinierte Aufzählungsmethoden

OK, jetzt, da wir ein grundlegendes Verständnis dafür haben, was Aufzählungen sind und wie Sie sie verwenden können, lassen Sie uns unser vorheriges Beispiel auf die nächste Ebene bringen, indem wir einige zusätzliche API-Methoden für die Aufzählung definieren:

public class Pizza { private PizzaStatus status; public enum PizzaStatus { ORDERED, READY, DELIVERED; } public boolean isDeliverable() { if (getStatus() == PizzaStatus.READY) { return true; } return false; } // Methods that set and get the status variable. } 

3. Vergleichen von Aufzählungstypen mit dem Operator "=="

Da Aufzählungstypen sicherstellen, dass nur eine Instanz der Konstanten in der JVM vorhanden ist, können wir den Operator "==" sicher verwenden, um zwei Variablen zu vergleichen, wie im obigen Beispiel gezeigt. Darüber hinaus bietet der Operator "==" Sicherheit bei der Kompilierung und Laufzeit.

Schauen wir uns zunächst die Laufzeitsicherheit im folgenden Snippet an, in dem der Operator "==" zum Vergleichen von Status verwendet wird und eine NullPointerException nicht ausgelöst wird, wenn einer der Werte null ist . Umgekehrt würde eine NullPointerException ausgelöst, wenn die Methode equals verwendet würde:

if(testPz.getStatus().equals(Pizza.PizzaStatus.DELIVERED)); if(testPz.getStatus() == Pizza.PizzaStatus.DELIVERED); 

Schauen wir uns zur Sicherheit der Kompilierungszeit ein anderes Beispiel an, bei dem eine Aufzählung eines anderen Typs mit der Methode equals als wahr bestimmt wird - da die Werte der Aufzählung und der Methode getStatus zufällig gleich sind, aber logischerweise die Vergleich sollte falsch sein. Dieses Problem wird durch Verwendung des Operators "==" vermieden.

Der Compiler kennzeichnet den Vergleich als Inkompatibilitätsfehler:

if(testPz.getStatus().equals(TestColor.GREEN)); if(testPz.getStatus() == TestColor.GREEN); 

4. Verwenden von Aufzählungstypen in Switch-Anweisungen

Aufzählungstypen können auch in switch- Anweisungen verwendet werden:

public int getDeliveryTimeInDays() { switch (status) { case ORDERED: return 5; case READY: return 2; case DELIVERED: return 0; } return 0; }

5. Felder, Methoden und Konstruktoren in Aufzählungen

Sie können Konstruktoren, Methoden und Felder in Aufzählungstypen definieren, die es sehr leistungsfähig machen.

Lassen Sie uns das obige Beispiel erweitern und den Übergang von einer Stufe einer Pizza zu einer anderen implementieren und sehen, wie wir die zuvor verwendete if- Anweisung und switch- Anweisung entfernen können:

public class Pizza { private PizzaStatus status; public enum PizzaStatus { ORDERED (5){ @Override public boolean isOrdered() { return true; } }, READY (2){ @Override public boolean isReady() { return true; } }, DELIVERED (0){ @Override public boolean isDelivered() { return true; } }; private int timeToDelivery; public boolean isOrdered() {return false;} public boolean isReady() {return false;} public boolean isDelivered(){return false;} public int getTimeToDelivery() { return timeToDelivery; } PizzaStatus (int timeToDelivery) { this.timeToDelivery = timeToDelivery; } } public boolean isDeliverable() { return this.status.isReady(); } public void printTimeToDeliver() { System.out.println("Time to delivery is " + this.getStatus().getTimeToDelivery()); } // Methods that set and get the status variable. } 

Das folgende Test-Snippet zeigt, wie dies funktioniert:

@Test public void givenPizaOrder_whenReady_thenDeliverable() { Pizza testPz = new Pizza(); testPz.setStatus(Pizza.PizzaStatus.READY); assertTrue(testPz.isDeliverable()); }

6. EnumSet und EnumMap

6.1. EnumSet

Das EnumSet ist eine spezielle Set- Implementierung, die für die Verwendung mit Enum- Typen vorgesehen ist.

Es ist eine sehr effiziente und kompakte Darstellung eines bestimmten Satzes von Enum - Konstanten , wenn sie einen im Vergleich HashSet aufgrund der internen Bit Vektordarstellung , die verwendet wird. Und es bietet eine typsichere Alternative zu herkömmlichen int- basierten „Bit-Flags“, mit denen wir präzisen Code schreiben können, der besser lesbar und wartbar ist.

Das EnumSet ist eine abstrakte Klasse mit zwei Implementierungen namens RegularEnumSet und JumboEnumSet , von denen eine abhängig von der Anzahl der Konstanten in der Aufzählung zum Zeitpunkt der Instanziierung ausgewählt wird.

Deshalb ist es immer eine gute Idee , diesen Satz zu verwenden , wenn wir mit einer Sammlung von ENUM - Konstanten in den meisten Szenarien arbeiten wollen (wie subsetting, Hinzufügen, Entfernen und für Massenoperationen wie containsAll und removeAll ) und Verwendung Enum.values ( ), wenn Sie nur alle möglichen Konstanten durchlaufen möchten.

Im folgenden Codeausschnitt können Sie sehen, wie EnumSet zum Erstellen einer Teilmenge von Konstanten und deren Verwendung verwendet wird:

public class Pizza { private static EnumSet undeliveredPizzaStatuses = EnumSet.of(PizzaStatus.ORDERED, PizzaStatus.READY); private PizzaStatus status; public enum PizzaStatus { ... } public boolean isDeliverable() { return this.status.isReady(); } public void printTimeToDeliver() { System.out.println("Time to delivery is " + this.getStatus().getTimeToDelivery() + " days"); } public static List getAllUndeliveredPizzas(List input) { return input.stream().filter( (s) -> undeliveredPizzaStatuses.contains(s.getStatus())) .collect(Collectors.toList()); } public void deliver() { if (isDeliverable()) { PizzaDeliverySystemConfiguration.getInstance().getDeliveryStrategy() .deliver(this); this.setStatus(PizzaStatus.DELIVERED); } } // Methods that set and get the status variable. } 

Die Ausführung des folgenden Tests demonstrierte die Leistungsfähigkeit der EnumSet- Implementierung der Set- Schnittstelle:

@Test public void givenPizaOrders_whenRetrievingUnDeliveredPzs_thenCorrectlyRetrieved() { List pzList = new ArrayList(); Pizza pz1 = new Pizza(); pz1.setStatus(Pizza.PizzaStatus.DELIVERED); Pizza pz2 = new Pizza(); pz2.setStatus(Pizza.PizzaStatus.ORDERED); Pizza pz3 = new Pizza(); pz3.setStatus(Pizza.PizzaStatus.ORDERED); Pizza pz4 = new Pizza(); pz4.setStatus(Pizza.PizzaStatus.READY); pzList.add(pz1); pzList.add(pz2); pzList.add(pz3); pzList.add(pz4); List undeliveredPzs = Pizza.getAllUndeliveredPizzas(pzList); assertTrue(undeliveredPzs.size() == 3); }

6.2. EnumMap

EnumMap ist eine spezielle Map- Implementierung, die mit Enum-Konstanten als Schlüssel verwendet werden soll. Es ist eine effiziente und kompakte Implementierung im Vergleich zu HashMap und wird intern als Array dargestellt:

EnumMap map; 

Lassen Sie uns einen kurzen Blick auf ein reales Beispiel werfen, das zeigt, wie es in der Praxis eingesetzt werden kann:

public static EnumMap
    
      groupPizzaByStatus(List pizzaList) { EnumMap
     
       pzByStatus = new EnumMap
      
       (PizzaStatus.class); for (Pizza pz : pizzaList) { PizzaStatus status = pz.getStatus(); if (pzByStatus.containsKey(status)) { pzByStatus.get(status).add(pz); } else { List newPzList = new ArrayList(); newPzList.add(pz); pzByStatus.put(status, newPzList); } } return pzByStatus; } 
      
     
    

Die Ausführung des folgenden Tests demonstrierte die Leistungsfähigkeit der EnumMap- Implementierung der Map- Schnittstelle:

@Test public void givenPizaOrders_whenGroupByStatusCalled_thenCorrectlyGrouped() { List pzList = new ArrayList(); Pizza pz1 = new Pizza(); pz1.setStatus(Pizza.PizzaStatus.DELIVERED); Pizza pz2 = new Pizza(); pz2.setStatus(Pizza.PizzaStatus.ORDERED); Pizza pz3 = new Pizza(); pz3.setStatus(Pizza.PizzaStatus.ORDERED); Pizza pz4 = new Pizza(); pz4.setStatus(Pizza.PizzaStatus.READY); pzList.add(pz1); pzList.add(pz2); pzList.add(pz3); pzList.add(pz4); EnumMap
    
      map = Pizza.groupPizzaByStatus(pzList); assertTrue(map.get(Pizza.PizzaStatus.DELIVERED).size() == 1); assertTrue(map.get(Pizza.PizzaStatus.ORDERED).size() == 2); assertTrue(map.get(Pizza.PizzaStatus.READY).size() == 1); }
    

7. Implementieren Sie Entwurfsmuster mithilfe von Aufzählungen

7.1. Singleton-Muster

Normalerweise ist die Implementierung einer Klasse mit dem Singleton-Muster nicht trivial. Aufzählungen bieten eine einfache und schnelle Möglichkeit, Singletons zu implementieren.

In addition to that, since the enum class implements the Serializable interface under the hood, the class is guaranteed to be a singleton by the JVM, which unlike the conventional implementation where we have to ensure that no new instances are created during deserialization.

In the code snippet below, we see how we can implement singleton pattern:

public enum PizzaDeliverySystemConfiguration { INSTANCE; PizzaDeliverySystemConfiguration() { // Initialization configuration which involves // overriding defaults like delivery strategy } private PizzaDeliveryStrategy deliveryStrategy = PizzaDeliveryStrategy.NORMAL; public static PizzaDeliverySystemConfiguration getInstance() { return INSTANCE; } public PizzaDeliveryStrategy getDeliveryStrategy() { return deliveryStrategy; } }

7.2. Strategy Pattern

Conventionally the Strategy pattern is written by having an interface that is implemented by different classes.

Adding a new strategy meant adding a new implementation class. With enums, this is achieved with less effort, adding a new implementation means defining just another instance with some implementation.

The code snippet below shows how to implement the Strategy pattern:

public enum PizzaDeliveryStrategy { EXPRESS { @Override public void deliver(Pizza pz) { System.out.println("Pizza will be delivered in express mode"); } }, NORMAL { @Override public void deliver(Pizza pz) { System.out.println("Pizza will be delivered in normal mode"); } }; public abstract void deliver(Pizza pz); }

Add the following method to the Pizza class:

public void deliver() { if (isDeliverable()) { PizzaDeliverySystemConfiguration.getInstance().getDeliveryStrategy() .deliver(this); this.setStatus(PizzaStatus.DELIVERED); } }
@Test public void givenPizaOrder_whenDelivered_thenPizzaGetsDeliveredAndStatusChanges() { Pizza pz = new Pizza(); pz.setStatus(Pizza.PizzaStatus.READY); pz.deliver(); assertTrue(pz.getStatus() == Pizza.PizzaStatus.DELIVERED); }

8. Java 8 and Enums

The Pizza class can be rewritten in Java 8, and you can see how the methods getAllUndeliveredPizzas() and groupPizzaByStatus() become so concise with the use of lambdas and the Stream APIs:

public static List getAllUndeliveredPizzas(List input) { return input.stream().filter( (s) -> !deliveredPizzaStatuses.contains(s.getStatus())) .collect(Collectors.toList()); } 
public static EnumMap
    
      groupPizzaByStatus(List pzList) { EnumMap
     
       map = pzList.stream().collect( Collectors.groupingBy(Pizza::getStatus, () -> new EnumMap(PizzaStatus.class), Collectors.toList())); return map; }
     
    

9. JSON Representation of Enum

Using Jackson libraries, it is possible to have a JSON representation of enum types as if they are POJOs. The code snippet below shows the Jackson annotations that can be used for the same:

@JsonFormat(shape = JsonFormat.Shape.OBJECT) public enum PizzaStatus { ORDERED (5){ @Override public boolean isOrdered() { return true; } }, READY (2){ @Override public boolean isReady() { return true; } }, DELIVERED (0){ @Override public boolean isDelivered() { return true; } }; private int timeToDelivery; public boolean isOrdered() {return false;} public boolean isReady() {return false;} public boolean isDelivered(){return false;} @JsonProperty("timeToDelivery") public int getTimeToDelivery() { return timeToDelivery; } private PizzaStatus (int timeToDelivery) { this.timeToDelivery = timeToDelivery; } } 

We can use the Pizza and PizzaStatus as follows:

Pizza pz = new Pizza(); pz.setStatus(Pizza.PizzaStatus.READY); System.out.println(Pizza.getJsonString(pz)); 

to generate the following JSON representation of the Pizzas status:

{ "status" : { "timeToDelivery" : 2, "ready" : true, "ordered" : false, "delivered" : false }, "deliverable" : true }

For more information on JSON serializing/deserializing (including customization) of enum types refer to the Jackson – Serialize Enums as JSON Objects.

10. Conclusion

In diesem Artikel haben wir die Java-Enumeration untersucht, von den Sprachgrundlagen bis hin zu fortgeschritteneren und interessanteren Anwendungsfällen aus der Praxis.

Codefragmente aus diesem Artikel finden Sie im Github-Repository.