Enumeration (EnumSet and EnumMap, etc.)

Article directory

  • 1. Overview
  • 2. Custom enumeration method
  • 3. Use == to compare enumeration types
  • 4. Use enumerated types in switch statements
  • 5. Properties, methods and constructors of enumerated types
  • 6. EnumSet and EnumMap
    • 6.1. EnumSets
  • 6.2. EnumMap
  • 7. Implement some design patterns through enumeration
    • 7.1 Singleton pattern
    • 7.2 Strategy pattern
  • 8. Java 8 and enums
  • 9. JSON representation of Enum type
  • 10. Supplement

1. Overview

The enum keyword was introduced in java5 to indicate a special type of class that always inherits the java.lang.Enum class. For more information, you can check its official documentation.

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

Constants defined in this way make code more readable, allow compile-time checking, pre-document the list of acceptable values, and avoid unexpected behavior due to passing in invalid values.

The following example defines the state of a simple enumeration type pizza order. There are three states: ORDERED, READY, DELIVERED:

package shuang.kou.enumdemo.enumtest;

public enum PizzaStatus {<!-- -->
    ORDERED,
    READY,
    DELIVERED;
}

Simply speaking, 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 the enum:

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 the enumeration type ensures that only one instance of the constant exists in the JVM, we can safely compare two variables using the == operator, as shown in the above example; moreover, == operator provides compile-time and runtime safety.

First, let’s take a look at runtime safety in the following snippet, where the == operator is used to compare states and not throw a NullPointerException if both values are null. Instead, if the equals method is used, a NullPointerException will be thrown:

Pizza.PizzaStatus pizza = null;
System.out.println(pizza.equals(Pizza.PizzaStatus.DELIVERED));//null pointer exception
System.out.println(pizza == Pizza.PizzaStatus.DELIVERED);//normal operation

For compile-time safety, let’s look at another example, comparing two different enum types:

if (Pizza.PizzaStatus.DELIVERED.equals(TestColor.GREEN)); // compiles fine
if (Pizza.PizzaStatus.DELIVERED == TestColor.GREEN); // Compilation failed, type mismatch

4. Use enumeration type in switch statement

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 enumerated types

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

Next, let’s extend the example above to implement the transition from one stage of the pizza to another, and see how we can 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 works:

@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 specially designed 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 maintainable.

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

Enumeration constant collection operations in many scenarios (such as: subsetting, adding, deleting, containsAll and removeAll batch operations) use EnumSet Very suitable; use Enum.values() if you need to iterate over all possible constants.

public class Pizza {<!-- -->
 
    private static EnumSet<PizzaStatus> undeliveredPizzaStatus =
      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 test demonstrates 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 enumeration constants as keys. It is an efficient and compact implementation compared to the corresponding HashMap and is internally represented as an array:

EnumMap<Pizza.PizzaStatus, Pizza> map;

Let’s take a quick look at a real 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 test demonstrates 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

Typically, implementing the Singleton pattern with classes is not an easy task, and enums provide an easy way to implement singletons.

Both “Effective Java” and “Java and Patterns” recommend this method very much. 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 and provides a serialization mechanism for free, which 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 would be more concise to use enumeration to achieve single instance control, and it provides a serialization mechanism for free, and the JVM fundamentally provides guarantees 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 an enum:

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? Please see the code below:

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

The singleton PizzaDeliverySystemConfiguration is obtained by PizzaDeliverySystemConfiguration.getInstance()

7.2 Strategy pattern

Usually, 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 easily done, adding a new implementation means just defining another instance with a certain implementation.

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

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? Please 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 method lambda and the Stream API make the getAllUndeliveredPizzas() and groupPizzaByStatus() methods so neat:

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 an enum type as a POJO. The snippet below 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 status is shown in the following JSON:

{<!-- -->
  "status" : {<!-- -->
    "timeToDelivery" : 2,
    "ready" : true,
    "ordered" : false,
    "delivered" : false
  },
  "deliverable" : true
}

For more information on JSON serialization/deserialization of enum types (including customization), see Jackson – Serialize Enums to JSON Objects.

10. Supplement

As we mentioned above, we can make it more powerful by defining properties, methods and constructors in enum types.

Let me show you through a practical 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 for use"),
    FORGET_PASSWORD(100001, "forgot password use"),
    UPDATE_PHONE_NUMBER(100002, "Update phone number usage");

    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 use
PinType{<!-- -->code=100001, message='forgot password to use'}

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