Java Enums 101

 ยท 16 min
AI-generated by DALL-E

Introduced in 2004 alongside Java 5, Enums are a distinct type identified by their own keyword. They encapsulate a predefined, fixed set of named constants. This simple characteristic already provides ample value in our day-to-day code. However, their application extends beyond merely consolidating constants within a single type.


What Are Java Enums

Let’s start with the most basic question to understand the topic at hand better: what are Java Enums anyway?

Or even better, what are other ways to group constants, and why are Enums a better solution.

Grouping Constants

Grouping constant values under a single type for easier access is simple. Just declare a final class, add some static final fields, and a final constructor to prevent instantiation:

java
public final class Direction {
  public static final int NORTH = 0;
  public static final int EAST  = 1;
  public static final int SOUTH = 2;
  public static final int WEST  = 3;

  private Direction() { }
}

Seems easy enough. Now, we can use the constants as fields of the Direction type for things like decision-making:

java
var direction = detectDirection();

if (direction == Direction.SOUTH) {
  System.out.println("It's getting warmer!");
}

The code is more straightforward and self-documenting compared to using so-called magic numbers, yet the benefits it offers are minimal. A closer examination of just these few lines reveals several issues with this method.

Constants need values
For a field to be considered constant, it must be declared final, and a final field must have a value. Therefore, the constant signifies more than a name-based state.

Values need types
Given that a value exists, it also needs a type. In this instance, the choice of int was motivated by its simpler comparison mechanics compared to, for instance, a String that could represent a direction’s name.

Lack of association with the grouping type
While the constants are consolidated under the type Direction, the constants themselves are oblivious to that relationship. This ignorance extends to the compiler, which remains unaware of any implicit or conceptual links. As a result, these fields can be compared to any int, whether they are supposed to be a direction or not.

Non-fixed set of constants
In addition to having no relationship between the grouping type and its fields (besides access), the final class approach lacks accountability. For example, if we use the constants for the cases of a switch statement, neither the compiler nor the IDE can assist in deducting the “correct” values, as there are potentially 4.3 billion of them.

No (simple) utility methods
While it is possible to introduce utility methods for a Direction by adding static methods to the type, these methods are plagued by the same lack of relationship as the constants.

These numerous issues highlight a fundamental problem: the absence of a tangible link between the constants and their collective type. This is precisely the gap that Enums are designed to fill.

Enums to the Rescue

Being treated as a special type by the JVM, Enums and their constants don’t have to adhere to the same mechanics as an equivalent grouping Class.

The enum keyword is used to declare a type with comma-separated named but value-less constants:

java
public enum Direction { 
  NORTH,
  EAST,
  SOUTH,
  WEST
}

At first glance, the Direction Enum is a type- and value-less equivalent to the previous Class-based approach. But what we can’t see on the surface is the underlying Bytecode:

java
// Command: javap Direction.class
// Line breaks added for readability
// OUTPUT:
Compiled from "Direction.java"
public final class Direction extends java.lang.Enum<Direction> {

  public static final Direction NORTH;
  public static final Direction EAST;
  public static final Direction SOUTH;
  public static final Direction WEST;

  public static Direction[] values();
  public static Direction valueOf(java.lang.String);

  static {};
}

The most apparent distinction in using Enums over a Class-based approach is that the constants are of type Direction themselves. This already fixes three of the problems from the previous section: “constants need values”, “values need types”, “Lack of association with the grouping type”.

Like Records, Enums are final and always extend a supertype, in this case, the self-referential generic java.lang.Enum<T>. This gives us a lot of additional functionality, like implementing Comparable and Serializable, or auxiliary methods like String name() or int ordinal().

Also, there are two methods directly available on the type:

  • public static Direction[] values()
  • public static Direction valueOf(java.lang.String)

These two functions underscore the connection between the constants and the Enum itself by giving us access to the constants, either as a complete list via values() or by converting the name of a constant into its corresponding Enum value with valueOf(String). The automagically available methods are possible thanks to the compiler knowing all potential values and giving us easy access to the information at runtime.

This knowledge-advantage addresses another issue with the Class-based approach: “non-fixed set of constants”. Now, the IDE can also help you with more sensible auto-completion or handling all cases of a switch statement.

So, only one issue remains: the issue of utility methods. As Enums are types, and the constants are values of that type, utility methods can either work on a constant level or as static methods:

java
public enum Direction { 
  NORTH,
  EAST,
  SOUTH,
  WEST;

  public String abbreviate() {
    return switch (this) {
      case NORTH -> "N";
      case EAST  -> "E";
      case SOUTH -> "S";
      case WEST  -> "W";
    };
  }
}

System.out.println("Direction: " +  Direction.SOUTH.abbreviate());

Having access to the constant in the form of this in the method is a convenient way to encapsulate Enum-related logic within it.

However, in the case of abbreviating the current Direction, it seems excessive to use a switch on each call. Perhaps associating an additional value with an Enum constant could simplify it?

Adding Values to Constants

The constant values specified in an Enum represent instances of that type. Similar to other types, they are essentially instantiated through an implicit argument-less constructor.

Nevertheless, it’s possible to enhance an Enum with additional fields, methods, and tailor-made constructors as necessary:

java
public enum Direction { 
  NORTH("N"),
  EAST("E"),
  SOUTH("S"),
  WEST("W");

  public final String abbreviation;

  Direction(String abbreviation) {
    this.abbreviation = abbreviation;
  }
}

The design of fields and constructors is somewhat flexible, besides that the constructor must be either private or package-private scoped, and they’re exclusively invoked during constant declaration and can’t be called directly.

While it’s not mandatory for a field to be final, allowing changes to it will definitely become a problem at some point. As there’s only one instance of an Enum constant, it affects everyone if changed. That’s why you should always declare any Enum field final, so the compiler ensures it has an actual value in the constructor.


More Enum Use Cases

The ability to associate a constant name with additional values in a type-safe manner enables Enums to offer functionality beyond merely categorizing constants within a type.

Functional Design Patterns

The factory pattern is a pattern for creating instances of a type without exposing the actual implementation details of how to construct such an object or even what the actual type is if hidden behind an interface.

Let’s say we have a shared interface for different shapes with an explicit ShapeType for the supported types:

java
public interface Shape {
  int corners();
  Color color();
  ShapeType type();
}

public enum ShapeType {
  CIRCLE,
  TRIANGLE,
  SQUARE,
  PENTAGON;
}

A Shape factory accepts the type and color and creates a concrete object:

The ShapeType restricts what shapes we support:

java
public class ShapeFactory {

  public static Shape newShape(ShapeType type,
                               Color color) {
    Objects.requireNonNull(type);
    Objects.requireNonNull(color);

    return switch (type) {
      case CIRCLE   -> new Circle(color);
      case TRIANGLE -> new Triangle(color);
      case SQUARE   -> new Square(color);
      case PENTAGON -> new Pentagon(color);
      default -> throw new IllegalArgumentException("Unknown type: " + type);
    };
  }
}

There are four different code parts so far:

  • A shared interface
  • A shape-identifying Enum
  • Concrete implementations of the different shapes (not shown)
  • A factory creating the actual shapes

These parts depend on each other, which is expected as they build an overall unit. However, this interdependence makes the code more fragile if changed. If a new ShapeType is introduced, the factory has to account for it, or the default case will throw an Exception.

As the factory is a simple abstraction over the shapes, why not remove the dependency on a factory and move the factory directly into the Enum?

The concrete implementations only require a Color to create a Shape, so they can simply be represented by a Function<Color, Shape>, which can be added as a field to the Enum, thanks to method references:

java
public enum ShapeType {
  CIRCLE(Circle::new),
  TRIANGLE(Triangle::new),
  SQUARE(Square::new),
  PENTAGON(Pentagon::new);

  public final Function<Color, Shape> factory;

  ShapeType(Function<Color, Shape> factory) {
    this.factory = factory;
  }
}

Each Enum constant has its personal factory directly attached to it. Now, all we need is a factory method calling them:

java
public enum ShapeType {
  // ...

  public Shape newInstance(Color color) {
    Objects.requireNonNull(color);
    return this.factory.apply(color);
  }
}

var redCircle = ShapeType.CIRCLE.newInstance(Color.RED);

Exposing the factory as a public field might seem unnecessary as a dedicated method for Shape creation is available. But this way, we can use factory as a functional jump-off point, like creating more specialized factories:

java
Function<Shape, Shape> cornerPrinter =
  shape -> {
    System.out.println("Shape created with " + shape.corners() + " corners");
    return shape;
  };

var debugFactory = ShapeType.CIRCLE.factory.andThen(cornerPrint);
var redCircle = debugFactory.apply(Color.RED);

If we don’t want to expose the field directly, this could be further simplified by implementing the factory method type as a functional interface directly, so each constant pulls double duty:

java
public enum ShapeType implements Function<Color, Shape> {
  // ...

  private final Function<Color, Shape> factory;

  public Shape apply(Color color) {
    Objects.requireNonNull(color);
    return this.factory.apply(color);
  }
}

var debugFactory = ShapeType.CIRCLE.andThen(cornerPrint);
var redCircle = debugFactory.apply(Color.RED);

Moving the factory directly into the Enum streamlines the decision-making process. The ShapeType constants are now bound to their counterpart, and the compiler enforces adding a factory for each new type of Shape. In the case of such a simple abstraction, it reduces the necessary boilerplate and allows the compiler to ensure correctness if extended further.

The factory pattern is only one possibility. I could’ve shown how to implement an Operation Enum for a calculator with BiOperator values doing the actual work. The point is that Enums can provide a flexible but still structured approach to code organization by bringing the decision-making process closer to the action.

Which way to go depends on your personal preference. Implementing the functional interface might remove further friction, but it also reduces the visibility of the available functionality. Personally, I like my Java code to be more explicit and verbose so that “future me” still understands what I was actually trying to achieve in the first place.

Enums as Constant Data Structures

Let’s imagine an Enum called Feature that represents the available features of a multi-tenant web application and their default state, etc.:

java
public enum Feature {
  DATABASE(true),
  CDN(false),
  CACHE(true),

  REST(true),
  GRAPHQL(false),

  PAYPAL(true),
  STRIPE(false),
  CREDIT_CARD(false),

  GOOGLE_LOGIN(true),
  APPLE_LOGIN(false),
  GITHUB_LOGIN(true);

  public final boolean defaultState;

  Feature(boolean defaultState) {
    this.defaultState = defaultState;
  }
}

When building such an application, we want to check for each tenant if a particular feature is enabled or disabled. But checking if one of a particular group of features is active becomes tedious if we need to check multiple values. And if a new one gets added, we need to set it for all tenants, or we might miss updating a check, etc. So, instead of manually checking related features individually, let’s bake the relationships directly into the Enum!

First, what are the possible groups we want?

Looking at the existing values, the following groupings reveal themselves: API, PAYMENT, and SSO.

The concept is that, on a login page, a single check for SSO suffices to decide whether to render any single-sign-on component. Then, the component is responsible for further checks of more delicate features to do its work.

In code, that means each Feature needs to know who it depends on:

java
public enum Feature {
  // ...
  
  API(true),
  REST(true, Feature.API),
  GRAPHQL(false, Feature.API),

  PAYMENT(true),
  PAYPAL(true, Feature.PAYMENT),
  STRIPE(false, Feature.PAYMENT),
  CREDIT_CARD(false, Feature.PAYMENT),

  SSO(true),
  GOOGLE_LOGIN(true, Feature.SSO),
  APPLE_LOGIN(false, Feature.SSO),
  GITHUB_LOGIN(true, Feature.SSO);

  public final boolean defaultState;
  public final Feature dependsOn;

  Feature(boolean defaultState, Feature dependsOn) {
    this.defaultState = defaultState;
    this.dependsOn = dependsOn;
  }

  Feature(boolean defaultState) {
    this(defaultState, null);
  }
}

To know if a Feature is active, a tenant’s features must be checked, so let’s add a simple method directly into the Enum:

java
public enum Feature {
  // ...
  ;

  public boolean isActive(Collection<Feature> features) {
    // CHECK POSSIBLE DEPENDENCY FIRST
    if (this.dependsOn != null && !this.dependsOn.isActive(feature)) {
      return false;
    }

    // EITHER ACTIVE OR DEFAULT VALUE
    return features.contains(this) || this.defaultValue;
  }
}

This example is quite simplistic to fit in the scope of the article. However, there are libraries out there that built a complex tree of interconnected Enums to represent complex data structures.

One of these libraries I worked with in the past is (the now archived) “user-agent-utils” that represented the whole user agent string with Enums, including:

  • Browser (including parent and children)
  • Browser type
  • Rendering engine
  • Manufacturer
  • Operating system

With the constant nature of Enums, in combination with the ability to include logic in the form of methods (static and otherwise), the library created a constant, strong-typed, and compile-time-checked data structure to represent, parse and validate user agents without any additional database.

However, I recommend not overdoing it. For a user-agent parser, building such a complex data structure might be justified, given the inherent complexity of the underlying data. But it also means that a new user agent requires additional code, not a new entry in a database or text file somewhere.

In most cases, simplicity should be king or queen. Trying to be too clever is often a good recipe for creating a disaster in the waiting.


Technical Details and Pitfalls

Having explored the fundamentals of using Enums and some more advanced use cases, it’s time to look at some technical details and potential caveats.

Comparing Enum Constants

Every Java developer knows the rule that non-primitives should never be compared using ==, and equals needs to be used instead.

But what about Enums?
They are non-primitive types. Does the same rule apply here, too?

Well, you can use either, but one comparison method makes more sense than the other.

Let’s look at ==. The semantics are straightforward: the operator compares the memory addresses of the operands. Since Enum constants are singletons, the comparison succeeds. And the best part, the compiler checks for us if we compare compatible types, so there’s another safety net.

On the other hand, examining Java’s boolean equals(Object) method reveals a different picture. It’s indifferent to the specific types involved, leading to two main issues.

Firstly, being a method call, it isn’t inherently safe from null references:

java
Shape maybeShape = null;

// ...

var isTriangle = maybeShape.equals(Shape.TRIANGLE);
// throws NullPointerException

And secondly, the compiler can’t check types, as the following code compiles fine:

java
var isFour = Shape.CIRCLE.equals(4);

However, most IDEs show a warning if you try to compare unrelated types. Nevertheless, in my opinion, compile-time-safety is always preferable to IDE rules or static code analysis

One aspect that might affect our decision on which to use is a specialized equals implementation for Enums. Let’s take a look at the source code of Enum#equals:

java
public final boolean equals(Object other) {
  return this==other;
}

Well, looks like using == seems the better choice. It’s compile-time safe and handles null gracefully. And it’s less characters to type.

Unstable Hash Code

Hash codes serve a crucial function and are part of the consistency contract for equals and hashCode. For example, an Integer uses its primitive value as its hash code, and String and many other types follow predictable algorithms. Even container formats, like Optional or many collection types, are stable depending on their content.

Enums, however, might present an unexpected twist…

Of course, the hash code of any Enum constant is stable, it couldn’t be otherwise. But they are only stable for the current JVM instance!

In many scenarios, this “limitation” poses no issue. But as soon as you intend to use it for an identifier or checksum that needs to persist beyond the current JVM, you’ve got a problem, as I did personally a while back.

In a REST API, we needed a stable checksum-like value to simplify comparing different versions. Most entities already had a version field, but some more complex or composed entities did not.

So, we calculated an overall checksum with the help of Objects#hash(Object...). If we make sure that each type has a correct and stable hashCode implementation, it should work fine.

Spoiler alert: it didn’t, thanks to Enum fields.

Checksum values varied across application servers, and application restart resulted in different checksums even on the same server.This unstable versioning led to an excessive amount of data being transmitted needlessly, and frequent unwarranted updates on the client side of things.

To rectify this, we developed our own hash(Object...) helper method that detects an Enum and creates a stable hash code based on the name of an Enum’s constant:

java
public static final int hash(Object... objs) {
  if (objs == null) {
    return 0;
  }

  int result = 1;

  for (var obj : objs) {
    if (obj == null) {
      result = 31 * result;
      continue;
    }

    if (obj instanceof Enum<?> e) {
      result = 31 * result + e.name().hashCode();
    } else {
      result = 31 * result + obj.hashCode();
    }
  }
  return result;
}

The instanceof check and using name() instead of the constant itself is the only change from Arrays.hash(Object[]), which Objects.hash(Object...) delegates to.

EnumSet and EnumMap

The JDK gives us two specialized types for the Set<E> and Map<K, V> interfaces that are optimized for Enums:

  • EnumSet<E>
  • EnumMap<E, V>

Both of them use the ordinal value of the constants to provide better performance than their non-Enum counterparts.

EnumSet<E> has multiple factory method to simplify creation:

java
// 5 different variants
EnumSet.of(...)

// Creates an empty EnumSet
EnumSet.noneOf(Class<E>)

// Create an EnumSet with all possible values
EnumSet.allOf(Class<E>)

// Creates an EnumSet with all constant in the range (inclusive)
EnumSet.range(E, E)

Internally, the EnumSet either uses the package-private JumboEnumSet for Enums with more than 64 constants or a RegularEnumSet otherwise.

EnumMap<E, V> doesn’t have any factory methods.

Both types do not accept null and will throw a NullPointerException, unlike their Hash... counterpart.


Conclusion

Hopefully, the article conferred the idea that Enums are more sophisticated than they first appear. Their special treatment in the JDK is well-deserved, as they provide ample benefits over alternative approaches and give us better performance (e.g., EnumSet and EnumMap) than many of their equivalent constructs.

However, their “specialness” also comes with intricate technical details that must be considered, like the JVM-stable hash codes. But moving behavior into an Enum often helps steer clear from too much conditional logic elsewhere.

Still, it’s also important not to overutilize or even “abuse” Enums. While the examples in this article were supposed to demonstrate how we can streamline code and reduce redundancy, there’s a fine line to walk. Nothing is gained from saving a few initial keystrokes if the trade-off is creating a more rigid overall system.

As with all the available tools in Java and the JDK, Enums are just one of them. But knowing your tools better is an important first step to choosing the right ones.


A Functional Approach to Java Cover Image
Interested in using functional concepts and techniques in your Java code?
Check out my book!
Available in English, Polish, and soon, Chinese.