DEV Community

RyTheTurtle
RyTheTurtle

Posted on

Advanced Enums in Java

Enums are a powerful mechanism used to specify that a type is limited to one of a set of constants. In addition to simple labeling of constants, the Java implementation of Enums can be used for simplifying code and improving readability. This article will explore some advanced uses of Java Enums, including advanced type safety, defining configuration, singletons, and defining simple state machines as a directed, acyclic graph (DAG).

Type Safety With Constants

The first and often only use of Enums taught to Java developers is to use it to label constant values. Not only is this a useful tool for readability by avoiding "magic constants", it is also a powerful tool for defensive programming by adding type constraints to method parameters.

Consider the following function that adds an application-specific error type to the general log message for when an exception occurs.

public void logError(String errorType, Exception e){ 
  log.error(String.format("Application error: %s",errorType), e);
}
// ... 
public void createAccount(){ 
  try{ 
    // ...
  } catch (Exception e){
    logError("Account Creation", e);
  }
} 
Enter fullscreen mode Exit fullscreen mode

This code has a few issues that make it difficult to maintain

  1. Repetition if there are multiple places that log errors during account creation, we have to hope the developer knows that the correct label for the error logs is "Account Creation" and not "creating account" or "setup" or something else. This repetition is also susceptible to misspellings or inconsistent capitalization that causes issues with searching debug logs.
  2. Type Safety Yes, the parameter has to be a string, but it's not likely that any string is a valid input to this method. Instead, there needs to be some mechanism to limit what string is acceptable as input.

We can leverage Enums to address these problems

// AppErrorType.java
public enum AppErrorType { 
  ACCOUNT_CREATION("Account Creation"),
  PROFILE_SETUP("Profile Setup"),
  //...
  EMAIL_UPDATE("Email Update");

  final String label; 
  ErrorType(final String l) { 
    this.label = l; 
  } 
}

// some other class...
public void logError(AppErrorType errorType, Exception e){ 
  final String msg = String.format("Application error: %s",
                                    errorType.label);
  log.error(msg, e);
}
// ... 
public void createAccount(){ 
  try{ 
    // ...
  } catch (Exception e){
    logError(AppErrorType.ACCOUNT_CREATION, e);
  }
} 
Enter fullscreen mode Exit fullscreen mode

Using an Enum for error types, this update

  • eliminates the chances of misspellings in the error labels
  • limits the scope of valid string instead of allowing any arbitrary string input to the logError method
  • eliminates the need for extra logic and tests on the method to ignore invalid strings
  • provides a single place for developers to look up what error labels are valid.

This technique can be implemented for any data type, and is a valuable tool for defensive programming where a method needs to only accept a particular set of valid values as an input.

Defining Configuration

Additionally, since Enums can have any number of fields, they are useful for configuring and grouping related values.

enum Subscription { 
  BASIC(9.99, 
        1,
        List.of(Feature.ADS,
                Feature.BASIC_CHANNELS)),
  PREMIUM(21.00,
          4,
          List.of(Feature.AD_FREE,
                  Feature.BASIC_CHANNELS,
                  Feature.SPORTS_PACKAGE,
                  Feature.PREMIUM_MOVIES));
  private double cost;
  private int maxDevices;
  private List<Feature> includedFeatures;

  Subscription(Double c, int d, List<Feature> f){
    this.cost = c;
    this.maxDevices = d;
    this.includedFeatures = f;
  } 
}
Enter fullscreen mode Exit fullscreen mode

Implementing Singletons

A singleton is a class that is intended to only ever have a single instance created. Seemingly simple on the surface, implementing a thread-safe, serializable singleton in Java has several approaches that are tricky and have performance implications in multithreaded environments.

// singleton with double-checked locking
public class MySingleton {
  private static volatile MySingleton instance;
  public static MySingleton getInstance() {
    if (instance == null) {
      synchronized (MySingleton .class) {
        if (instance == null) {
          instance = new MySingleton();
        }
      }
    }
    return instance;
  }
  void doSomething(){
    //...
  }
//...
} 
Enter fullscreen mode Exit fullscreen mode

Instead, we can leverage the fact that the Java Virtual Machine(JVM) guarantees Enums to only ever have one instance created.

public enum MySingleton { 
  INSTANCE;

  void doSomething(){
    //...
  }
  //...
}
Enter fullscreen mode Exit fullscreen mode

This is a simple way to leverage the JVM to ensure our singletons are thread-safe and serializable without having to repeat the same boilerplate construction and serialization logic for every singleton we create.

State Machines

Many applications in software can be modeled as workflows. A workflow is a directed graph of states that can be implemented as a Finite State Machine(FSM). The FSM specifies all valid transitions from any given state. An input, called a context, enters an initial state and can traverse the graph after some processing until it reaches a terminal state (a state which has no valid transitions to other states).

Since Enums are classes in Java, we can use Enums to model the finite state machine with the following steps:

  1. define an interface for the state machine states that allows a state process a context and indicate whether the current state is terminal
interface FSMState {
  abstract FSMState process(Context ctx);
  abstract boolean isTerminal();
}
Enter fullscreen mode Exit fullscreen mode
  1. Implement the states using an interface. the process method on each state performs whatever processing is required on the context at that state and then returns the next state to transition to in the graph.
enum State implements FSMState{ 
  FIRST { 
    public State process(Context ctx){
      return SECOND;
    }

    public boolean isTerminal(){
      return false;
    }
  },
  SECOND { 
    public State process(Context ctx){
      return LAST;
    } 

    public boolean isTerminal(){
      return false;
    }
  },
  LAST {
    public State process(Context ctx){
      return this;
    }

    public boolean isTerminal(){
      return true;
    }
  };
}
Enter fullscreen mode Exit fullscreen mode
  1. Define the context object that will be the input throughout the state machine. A context object must contain it's own current state, and implement a process function that advances the context by using the state. This will be driven by an external machine later.
class Context {  
  private FSMState currentState;

  public Context(){ 
    this.currentState = State.FIRST;
  }

  public FSMState getCurrentState(){
    return this.currentState;
  }

  public void process(){ 
    if(!this.currentState.isTerminal()){
      this.currentState = this.currentState.process(this);
    }
  }

  public boolean isFinished(){ 
    return this.currentState.isTerminal();
  }
} 
Enter fullscreen mode Exit fullscreen mode
  1. Define a machine that initializes and drives the context through it's different states. The machine logic is really dependent on the application, but an example of how to drive a context through states looks something like
class Machine { 
  public static void main(String[] args) {
    Context context = new Context();
    System.out.println("current state: "+context.getCurrentState());
    while(!context.isFinished()){ 
      context.process();
      System.out.println("current state: "+context.getCurrentState());
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Given our examples, running Machine.main() results in the following output

current state: FIRST
current state: SECOND
current state: LAST
Enter fullscreen mode Exit fullscreen mode

A Powerful Addition to the Toolbox

Enums are a powerful data type, especially in Java. This article has demonstrated how Enums can be used to improve readability of code by labeling constants, restricting method inputs, and reduce boilerplate when defining Singletons and state machines. Enums are a valuable addition to any developer's mental toolbox.

Top comments (0)