Using enums in Java is really not that simple

1. Overview

In this article, we will see what Java enums are, what problems they solve and how to implement some design patterns using Java enums in practice.

The enum keyword was introduced in java5, indicating a special type of class, which always inherits the java.lang.Enum class. For more information, you can view its own official documentation.

Enumerations are often compared with constants, probably because we actually use a lot of enumerations to replace constants. So what are the advantages of this approach?

Constants defined in this way make code more readable, allow compile-time checks, pre-record a list of acceptable values, and avoid unexpected behavior caused by passing in invalid values.

The following example defines a simple enumeration type pizza order status, there are three states ORDERED, READY, DELIVERED:

package shuang.kou.enumdemo.enumtest;

public enum PizzaStatus {
    ORDERED,
    READY, 
    DELIVERED; 
}

In short, we avoid defining constants through the above code, and we put all the constants related to the status of the pizza order into an enumeration type.

System.out.println(PizzaStatus.ORDERED.name());//ORDERED
System.out.println(PizzaStatus.ORDERED);//ORDERED
System.out.println(PizzaStatus.ORDERED.name().getClass());//class java.lang.String
System.out.println(PizzaStatus.ORDERED.getClass());//class shuang.kou.enumdemo.enumtest.PizzaStatus

2. Custom enumeration method

Now that we have a basic understanding of what enums are and how to use them, let's take the previous example to the next level by defining some additional API methods on enums:

public class Pizza {
    private PizzaStatus status;
    public enum PizzaStatus {
        ORDERED,
        READY,
        DELIVERED;
    }

    public boolean isDeliverable() {
        return getStatus() == PizzaStatus.READY;
    }

    // Methods that set and get the status variable.
}

3. Use == to compare enumeration types

Since enum types ensure that only one instance of a constant exists in the JVM, we can safely use the == operator to compare two variables, as in the example above; furthermore, the == operator provides both compile-time and runtime safety.

First, let's take a look at runtime safety in the following code snippet, where the == operator is used to compare states and neither throws a NullPointerException if both values ​​are null. Conversely, if the equals method is used, a NullPointerException will be thrown:

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

For compile-time safety, let's look at another example, two different enumeration types are compared, and the result of the comparison using the equal method is determined to be true, because the enumeration value of the getStatus method is consistent with the enumeration value of another type, but logically it should be is false. This problem can be avoided by using the == operator. Because the compiler will say a type incompatibility error:

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

4. Use enum types in switch statements

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

5. Properties, methods and constructors of enumeration types

There are my (JavaGuide) additions at the end of the article.

You can make it even more powerful by defining properties, methods and constructors on enum types.

Next, let's extend the above example to implement the transition from one stage of pizza to another and see how to get rid of the if and switch statements we used earlier:

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.
}

The following code shows how it work s:

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

6.EnumSet and EnumMap

6.1. EnumSet

EnumSet is a Set type designed specifically for enumeration types.

Compared to HashSet, it is a very efficient and compact representation of a specific set of Enum constants due to the use of an internal bit vector representation.

It provides a type-safe alternative to traditional int-based "bit flags", allowing us to write concise code that is more readable and easier to maintain.

EnumSet is an abstract class that has two implementations: RegularEnumSet, JumboEnumSet, which one you choose depends on the number of constants in the enumeration at instantiation.

EnumSet is very suitable for enumerating constant set operations in many scenarios (such as subset, add, delete, containsAll and removeAll batch operations); if you need to iterate over all possible constants use Enum.values().

public class Pizza {

    private static EnumSet<PizzaStatus> 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<Pizza> getAllUndeliveredPizzas(List<Pizza> 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.
}

The following tests demonstrate the power of EnumSet in certain scenarios:

@Test
public void givenPizaOrders_whenRetrievingUnDeliveredPzs_thenCorrectlyRetrieved() {
    List<Pizza> 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<Pizza> undeliveredPzs = Pizza.getAllUndeliveredPizzas(pzList); 
    assertTrue(undeliveredPzs.size() == 3); 
}

6.2. EnumMap

EnumMap is a specialized map implementation for using enum constants as keys. Compared to the corresponding HashMap, it is an efficient and compact implementation and is represented internally as an array:

EnumMap<Pizza.PizzaStatus, Pizza> map;

Let's take a quick look at a real-world example that demonstrates how to use it in practice:

Iterator<Pizza> iterator = pizzaList.iterator();
while (iterator.hasNext()) {
    Pizza pz = iterator.next();
    PizzaStatus status = pz.getStatus();
    if (pzByStatus.containsKey(status)) {
      pzByStatus.get(status).add(pz);
    } else {
      List<Pizza> newPzList = new ArrayList<>();
      newPzList.add(pz);
      pzByStatus.put(status, newPzList);
    }
}

The following tests demonstrate the power of EnumMap in certain scenarios:

@Test
public void givenPizaOrders_whenGroupByStatusCalled_thenCorrectlyGrouped() {
    List<Pizza> 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<Pizza.PizzaStatus,List<Pizza>> 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. Implement some design patterns through enumeration

7.1 Singleton Pattern

In general, implementing the Singleton pattern using classes is not trivial, and enums provide an easy way to implement singletons.

"Effective Java" and "Java and Patterns" both highly recommend this approach. What are the benefits of implementing enumeration in this way?

<Effective Java>

This method is similar in function to the public domain method, but it is more concise, provides a serialization mechanism for free, and absolutely prevents multiple instantiations, even in the face of complex serialization or reflection attacks. Although this approach has not been widely adopted, single-element enumeration types have become the best way to implement Singleton. ——"Effective Java Chinese Edition Second Edition"

Java and Patterns

In "Java and Patterns", the author wrote that it is more concise to use enumeration to implement single-instance control, and provides a serialization mechanism for free, and is fundamentally guaranteed by the JVM to absolutely prevent multiple instantiations. A more concise, efficient, and safe way to implement singletons.

The following code snippet shows how to implement the singleton pattern using enums:

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;
    }
}

How to use it? See the code below:

PizzaDeliveryStrategy deliveryStrategy = PizzaDeliverySystemConfiguration.getInstance().getDeliveryStrategy();

The singleton PizzaDeliverySystemConfiguration is obtained through PizzaDeliverySystemConfiguration.getInstance()

7.2 Strategy Mode

Typically, the strategy pattern is implemented by different classes implementing the same interface.

This also means that adding new strategies means adding new implementation classes. With enums, this can be done easily, adding a new implementation means just defining another instance with an implementation.

The following code snippet shows how to implement the strategy pattern using enumerations:

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 Pizza:

public void deliver() {
    if (isDeliverable()) {
        PizzaDeliverySystemConfiguration.getInstance().getDeliveryStrategy()
          .deliver(this);
        this.setStatus(PizzaStatus.DELIVERED);
    }
}

How to use it? See the code below:

@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 lambda and Stream API make the getAllUndeliveredPizzas() and groupPizzaByStatus() methods so concise:

getAllUndeliveredPizzas():

public static List<Pizza> getAllUndeliveredPizzas(List<Pizza> input) {
    return input.stream().filter(
      (s) -> !deliveredPizzaStatuses.contains(s.getStatus()))
        .collect(Collectors.toList());
}

groupPizzaByStatus() :

public static EnumMap<PizzaStatus, List<Pizza>> 
  groupPizzaByStatus(List<Pizza> pzList) {
    EnumMap<PizzaStatus, List<Pizza>> map = pzList.stream().collect(
      Collectors.groupingBy(Pizza::getStatus,
      () -> new EnumMap<>(PizzaStatus.class), Collectors.toList()));
    return map;
}

9. JSON representation of Enum type

Using the Jackson library, it is possible to represent JSON of enumeration types as POJO s. The following code snippet shows Jackson annotations that can be used for the same purpose:

@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 Pizza and PizzaStatus as follows:

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

The generated Pizza state is shown in the following JSON:

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

For more information on JSON serialization/deserialization (including customization) of enum types, see Jackson - Serialize enums to JSON objects.

10. Summary

In this article, we discussed the Java enumeration type, from basic knowledge to advanced applications and practical application scenarios, so that we can feel the power of enumeration.

11. Supplement

We mentioned above that we can make it more powerful by defining properties, methods and constructors on enum types.

Let me show through an actual example, when we call the SMS verification code, there may be several different uses, we define it as follows:

public enum PinType {

    REGISTER(100000, "Register to use"),
    FORGET_PASSWORD(100001, "Forgot password to use"),
    UPDATE_PHONE_NUMBER(100002, "Update mobile number using");

    private final int code;
    private final String message;

    PinType(int code, String message) {
        this.code = code;
        this.message = message;
    }

    public int getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }

    @Override
    public String toString() {
        return "PinType{" +
                "code=" + code +
                ", message='" + message + '\'' +
                '}';
    }
}

actual use:

System.out.println(PinType.FORGET_PASSWORD.getCode());
System.out.println(PinType.FORGET_PASSWORD.getMessage());
System.out.println(PinType.FORGET_PASSWORD.toString());

Output:

100001
 Forgot password to use
PinType{code=100001, message='Forgot password to use'}

In this way, it will be very flexible and convenient to use in practice!

Author: Snailclimb
Link: Using enums in Java is really not that simple
Source: github

Tags: Java Back-end enum

Posted by Celauran on Mon, 09 May 2022 23:00:40 +0300