Hidden Beauties of Java Enums

Let’s take a look at the power and beauty of what Java Enums can provide…

I also created a video describing the same topics based on this article, so you can follow along combining both the video and this post.

What is an Enum?

Enums are the preferred way to define fixed values you want to use in your code. They are a special type of Java class and contain a group of unchangeable variables.

For example:

enum Level {
  INFO,
  WARNING,
  ERROR
}

Projects Using Enums

A few of my “pet projects” make extensive use of Java enums, and while working on these, I learned that it’s not clear to everyone that these are really powerful and can contain much more than just a list of fixed values.

Let’s take a look at a few of these use cases.

Raspberry Pi Boards in an Enum Database

For the Pi4J project, a library was needed that contains some information about the various Raspberry Pi boards, and which protocols they support on their GPIO (General Purpose Input/Output) pins. As this information should be available inside the code, on boards running without an internet connection, an API was not the right solution.

Therefore the decision was made to generate a Java library that contains all this information by using a list of enums: github.com/Pi4J/pi4j-board-info.

On top of this library, a website and API are created at api.pi4j.com. As a result, the database can now be used in offline and online modes.

Parsing of LottieFiles

In another project Lottie4J I’m trying to parse animations in JSON format into Java objects to create a JavaFX player. To make the LottieFiles-format easier to understand within this Java project, enums are used for the various definitions as you can see in the GitHub project.

Video Categories for 4drums.media

This is a project inspired by my 12y son, who wants to collect and share drum videos, and hopes he can build a community around this. It’s fun to be able to use my “professional knowledge” to build such a project quickly, thanks to Spring and Vaadin, but it’s a work in progress as my son created a TODO list with ideas with 100 remaining bullet points…

Enums are used in this project to define the available video and tutorial categories which include a label, icon, text and background color.

public enum VideoCategory {
    SPECTACULAR("la-dragon", "category.video.spectacular", "#CD0000", "#FFFFFF"),
    TUTORIAL("la-drum", "category.video.tutorial", " #0000EE", "#FFFFFF"),
    I_MADE_THIS("la-portrait", "category.video.selfmade", " #A2CD5A", "#000000"),
    ...;

    private final String icon;
    private final String translationId;
    private final String colorBg;
    private final String colorText;

    VideoCategory(String icon, String translationId, String colorBg, String colorText) {
        this.icon = icon;
        this.translationId = translationId;
        this.colorBg = colorBg;
        this.colorText = colorText;
    }

    public String getIcon() {
        return icon;
    }
    ...
}

Examples of Enum Usage

Let’s look at some examples, step-by-step. The full code is available on GitHub as EnumExtended.java and can be executed with JBang! with jbang EnumExtended.java.

Basic Functionality

A basic example of the use of an Enum is the following:

enum Level {
  INFO,
  WARNING,
  ERROR
}

System.out.println("Error level: " + Level.WARNING);

Output:

Error level: WARNING

Enums were introduced to replace the use of int constants as seen in other languages, which would look like this in Java:

class MyProgram {
   public final static int LEVEL_INFO = 1;
   public final static int LEVEL_WARNING = 2;
   public final static int LEVEL_ERROR = 3;
   ...
}

But an enum can do a lot more! Let’s take a look at some examples…

Using Enums in Switch

By using enums, switch - case code can be written in a very clean way.

public int getLevelValue(Level level) {
    switch (level) {
        case INFO: 
            return 1;
        case WARNING: 
            return 2;
        case ERROR: 
            return 3;
    }
    return 0;
}

But as we will see further, this code can still be improved a lot, by extending the Level enum…

Use Enum Instead of Boolean

Enums are also a good use case to replace boolean checks. Let’s take an example with customers that can become “inactive” in a system. Typically you would do this with a boolean.

class Customer {
    String name;
    boolean active;
    ...
}

But a bit later, your use-case changes and customers could also be suspended because they stopped paying the bills. Would you then add and extra boolean suspended? This could lead to a lot of changes in your program which could be avoided by using an enum for the customer state, like this:

enum CustomerState {
    ACTIVE,
    INACTIVE,
    SUSPENDED
}

class Customer {
    String name;
    CustomerState state;
    ...
}

Extra Data in an Enum Value

For me, the true power of an enum, is the fact that it can actually hold a lot of information and provide methods to use these values.

In the following example, the Level enum we used before, is extended with a severity value, label description and a color code.

enum Level {
    INFO(1, "Informative message", 0x00aa00),
    WARNING(2, "Warning message", 0xFFA500),
    ERROR(3, "Error message", 0xA30000);

    private final int severity;
    private final String label;
    private final int color;

    private Level(int severity, String label, int color) {
        this.severity = severity;
        this.label = label;
        this.color = color;
    }

    public int getSeverity() {
        return severity;
    }

    public String getLabel() {
        return label;
    }

    public int getColor() {
        return color;
    }
}

We can now get the data of all the Level values with the following code:

for (Level level : Level.values()) {
    System.out.println("Level " + level.name()
            + "\n\tSeverity: " + level.getSeverity()
            + "\n\tLabel: " + level.getLabel()
            + "\n\tColor: " + level.getColor());
}

Which will produce this output:

Level INFO
	Severity: 1
	Label: Informative message
	Color: 43520
Level WARNING
	Severity: 2
	Label: Warning message
	Color: 16753920
Level ERROR
	Severity: 3
	Label: Error message
	Color: 10682368

So this means level.getSeverity() fully replaces the getLevelValue(Level level) method with switch-case we’ve seen before.

JSON parsing with Enums

In many cases where you want to generate JSON output or parse JSON to Java objects, enums can be involved.

JSON Example Data

Let’s take for example this JSON file that contains a few log messages. The level is stored with the severity value from the Level enum.

[
    { 
        "level": 1,
        "timestamp": 1675867184342,
        "message": "Program started"
    },
    {
        "level": 2,
        "timestamp": 1675867185921,
        "message": "File X not found"
    },
    {
        "level": 3,
        "timestamp": 1675867186357,
        "message": "Error at line Y"
    }
]

Record to Load the JSON data

We want to read these messages into Java objects, by using the Jackson library and a record:

record LogMessage(Level level, Long timestamp, String message) {
    @JsonIgnore
    public ZonedDateTime getZonedDateTime() {
        return ZonedDateTime.ofInstant(Instant.ofEpochMilli(timestamp()), ZoneId.of("UTC"));
    }
}

The additional method getZonedDateTime will convert the Long timestamp value to be used in our code, but because of the @JsonIgnore attribute, this value will not be converted back to JSON as you will see later.

Reading the JSON to Java objects

By using the Jackson library, we can easily convert this JSON data to an array of LogMessage, if we add a few Jackson attributes to the enum, and by using the ObjectMapper:

enum Level {
    ...;
    @JsonValue
    private final int severity;
    ...
    @JsonCreator
    public static Level fromValue(Integer severity) 
        throws IllegalArgumentException {
        return Arrays.stream(Level.values()).sequential()
                .filter(v -> v.getSeverity() == severity)
                .findFirst()
                .orElseThrow(() -> 
                    new IllegalArgumentException("The given severity does not exist:" + severity));
    }
}

ObjectMapper objectMapper = new ObjectMapper();
LogMessage[] logMessages = objectMapper
    .readValue(json, LogMessage[].class);

for (LogMessage logMessage : logMessages) {
    System.out.println("Log message at " 
            + logMessage.getZonedDateTime()
            + "\n\tSeverity: " 
            + logMessage.level().getLabel()
            + "\n\tMessage: " 
            + logMessage.message());
}

This will generate the following output. As you can see, the numeric level value from the JSON has been converted to a Level-enum-value:

Log message at 2023-02-08T14:39:44.342Z[UTC]
        Severity: Informative message
        Message: Program started
Log message at 2023-02-08T14:39:45.921Z[UTC]
        Severity: Warning message
        Message: File X not found
Log message at 2023-02-08T14:39:46.357Z[UTC]
        Severity: Error message
        Message: Error at line Y

Converting Java Object to JSON data

Because in the previous changes, we already added the @JsonValue attribute to the severity variable of the enum, the ObjectMapper will use the correct value to convert the level back to JSON. Let’s use the PrettyPrinter to generate formatted JSON from our logMessages array:

System.out.println(objectMapper
    .writerWithDefaultPrettyPrinter()
    .writeValueAsString(logMessages));

This will create the following output, which contains the same content as our source JSON data.

[ {
  "level" : 1,
  "timestamp" : 1675867184342,
  "message" : "Program started"
}, {
  "level" : 2,
  "timestamp" : 1675867185921,
  "message" : "File X not found"
}, {
  "level" : 3,
  "timestamp" : 1675867186357,
  "message" : "Error at line Y"
} ]

Converting ZonedDateTime to JSON

As mentioned before, because of the @JsonIgnore attribute on the getZonedDateTime method, this value is not included in the generated JSON. And this even needs an extra remark… java.time.ZonedDateTime is not supported by default by Jackson. The extra module com.fasterxml.jackson.datatype:jackson-datatype-jsr310 is needed.

So a few extra changes are needed to be able to export this value, by removing @JsonIgnore, adding @JsonFormat and registering the JavaTimeModule in the objectMapper:

record LogMessage(Level level, Long timestamp, String message) {
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy hh:mm:ss")
    public ZonedDateTime getZonedDateTime() {
        return ZonedDateTime.ofInstant(Instant.ofEpochMilli(timestamp()), ZoneId.of("UTC"));
    }
}

objectMapper.registerModule(new JavaTimeModule());
System.out.println(objectMapper
    .writerWithDefaultPrettyPrinter()
    .writeValueAsString(logMessages));

This generates JSON with the ZonedDateTime in the defined format:

[ {
  "level" : 1,
  "timestamp" : 1675867184342,
  "message" : "Program started",
  "zonedDateTime" : "08-02-2023 02:39:44"
}, {
  "level" : 2,
  "timestamp" : 1675867185921,
  "message" : "File X not found",
  "zonedDateTime" : "08-02-2023 02:39:45"
}, {
  "level" : 3,
  "timestamp" : 1675867186357,
  "message" : "Error at line Y",
  "zonedDateTime" : "08-02-2023 02:39:46"
} ]

Conclusion

Java enums can contain much more than just a list of definitions, but also data and extended functionality!

Please experiment with the given example code file and discover how you can use these as a base to improve your code…