1. Overview

The Java enum type provides a language-supported way to create and use constant values. By defining a finite set of values, the enum is more type safe than constant literal variables like String or int.

However, enum values are required to be valid identifiers, and we’re encouraged to use SCREAMING_SNAKE_CASE by convention.

Given those limitations, the enum value alone is not suitable for human-readable strings or non-string values.

In this tutorial, we’ll use the enum features as a Java class to attach the values we want.

2. Using Java Enum as a Class

We often create an enum as a simple list of values. For example, here are the first two rows of the periodic table as a simple enum:

public enum Element {
    H, HE, LI, BE, B, C, N, O, F, NE
}

Using the syntax above, we’ve created ten static, final instances of the enum named Element. While this is very efficient, we have only captured the element symbols. And while the uppercase form is appropriate for Java constants, it’s not how we normally write the symbols.

Furthermore, we’re also missing other properties of the periodic table elements, like the name and atomic weight.

Although the enum type has special behavior in Java, we can add constructors, fields and methods as we do with other classes. Because of this, we can enhance our enum to include the values we need.

3. Adding a Constructor and a Final Field

Let’s start by adding the element names.

We’ll set the names into a final variable using a constructor:

public enum Element {
    H("Hydrogen"),
    HE("Helium"),
    // ...
    NE("Neon");

    public final String label;

    private Element(String label) {
        this.label = label;
    }
}

First of all, we notice the special syntax in the declaration list. This is how a constructor is invoked for enum types. Although it’s illegal to use the new operator for an enum, we can pass constructor arguments in the declaration list.

We then declare an instance variable label. There are a few things to note about that.

First, we chose the label identifier instead of the name. Although the member field name is available to use, let’s choose label to avoid confusion with the predefined Enum.name() method.

Second, our label field is final. While fields of an enum do not have to be final, in most cases we don’t want our labels to change. In the spirit of enum values being constant, this makes sense.

Finally, the label field is public, so we can access the label directly:

System.out.println(BE.label);

On the other hand, the field can be private, accessed with a getLabel() method. For the purpose of brevity, this article will continue to use the public field style.

4. Locating Java Enum Values

Java provides a valueOf(String) method for all enum types.

Thus, we can always get an enum value based on the declared name:

assertSame(Element.LI, Element.valueOf("LI"));

However, we may want to look up an enum value by our label field as well.

To do that, we can add a static method:

public static Element valueOfLabel(String label) {
    for (Element e : values()) {
        if (e.label.equals(label)) {
            return e;
        }
    }
    return null;
}

The static valueOfLabel() method iterates the Element values until it finds a match. It returns null if no match is found. Conversely, an exception could be thrown instead of returning null.

Let’s see a quick example using our valueOfLabel() method:

assertSame(Element.LI, Element.valueOfLabel("Lithium"));

5. Caching the Lookup Values

We can avoid iterating the enum values by using a Map to cache the labels.

To do this, we define a static final Map and populate it when the class loads:

public enum Element {

    // ... enum values

    private static final Map<String, Element> BY_LABEL = new HashMap<>();
    
    static {
        for (Element e: values()) {
            BY_LABEL.put(e.label, e);
        }
    }

   // ... fields, constructor, methods

    public static Element valueOfLabel(String label) {
        return BY_LABEL.get(label);
    }
}

As a result of being cached, the enum values are iterated only once, and the valueOfLabel() method is simplified.

As an alternative, we can lazily construct the cache when it is first accessed in the valueOfLabel() method. In that case, map access must be synchronized to prevent concurrency problems.

6. Attaching Multiple Values

The Enum constructor can accept multiple values.

To illustrate, let’s add the atomic number as an int and the atomic weight as a float:

public enum Element {
    H("Hydrogen", 1, 1.008f),
    HE("Helium", 2, 4.0026f),
    // ...
    NE("Neon", 10, 20.180f);

    private static final Map<String, Element> BY_LABEL = new HashMap<>();
    private static final Map<Integer, Element> BY_ATOMIC_NUMBER = new HashMap<>();
    private static final Map<Float, Element> BY_ATOMIC_WEIGHT = new HashMap<>();
    
    static {
        for (Element e : values()) {
            BY_LABEL.put(e.label, e);
            BY_ATOMIC_NUMBER.put(e.atomicNumber, e);
            BY_ATOMIC_WEIGHT.put(e.atomicWeight, e);
        }
    }

    public final String label;
    public final int atomicNumber;
    public final float atomicWeight;

    private Element(String label, int atomicNumber, float atomicWeight) {
        this.label = label;
        this.atomicNumber = atomicNumber;
        this.atomicWeight = atomicWeight;
    }

    public static Element valueOfLabel(String label) {
        return BY_LABEL.get(label);
    }

    public static Element valueOfAtomicNumber(int number) {
        return BY_ATOMIC_NUMBER.get(number);
    }

    public static Element valueOfAtomicWeight(float weight) {
        return BY_ATOMIC_WEIGHT.get(weight);
    }
}

Similarly, we can add any values we want to the enum, such as the proper case symbols, “He”, “Li” and “Be”, for example.

Moreover, we can add computed values to our enum by adding methods to perform operations.

7. Controlling the Interface

As a result of adding fields and methods to our enum, we’ve changed its public interface. Therefore our code, which uses the core Enum name() and valueOf() methods, will be unaware of our new fields.

The static valueOf() method is already defined for us by the Java language, so we can’t provide our own valueOf() implementation.

Similarly, because the Enum.name() method is final, we can’t override it either.

As a result, there’s no practical way to utilize our extra fields using the standard Enum API. Instead, let’s look at some different ways to expose our fields.

7.1. Overriding toString()

Overriding toString() may be an alternative to overriding name():

@Override 
public String toString() { 
    return this.label; 
}

By default, Enum.toString() returns the same value as Enum.name().

7.2. Implementing an Interface

The enum type in Java can implement interfaces. While this approach is not as generic as the Enum API, interfaces do help us generalize.

Let’s consider this interface:

public interface Labeled {
    String label();
}

For consistency with the Enum.name() method, our label() method does not have a get prefix.

And because the valueOfLabel() method is static, we do not include it in our interface.

Finally, we can implement the interface in our enum:

public enum Element implements Labeled {

    // ...

    @Override
    public String label() {
        return label;
    }

    // ...
}

One benefit of this approach is that the Labeled interface can be applied to any class, not just enum types. Instead of relying on the generic Enum API, we now have a more context-specific API.

8. Conclusion

In this article, we’ve explored many features of the Java Enum implementation. By adding constructors, fields and methods, we see that the enum can do a lot more than literal constants.

As always, the full source code for this article can be found over on GitHub.