Java Enums 101
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.
Table of Contents
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:
Seems easy enough.
Now, we can use the constants as fields of the Direction
type for things like decision-making:
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:
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:
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:
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:
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:
A Shape
factory accepts the type and color and creates a concrete object:
The ShapeType
restricts what shapes we support:
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:
Each Enum constant has its personal factory directly attached to it. Now, all we need is a factory method calling them:
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:
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:
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.:
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:
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:
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:
And secondly, the compiler can’t check types, as the following code compiles fine:
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
:
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:
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:
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.