DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Immutable Objects Using Record in Java
  • Squid Game: The Clean Code Trials — A Java Developer's Survival Story
  • Writing DTOs With Java8, Lombok, and Java14+
  • Formatting Strings in Java: String.format() Method

Trending

  • Throughput vs Goodput: The Performance Metric You Are Probably Ignoring in LLM Testing
  • Architecting Petabyte-Scale Hyperspectral Pipelines on AWS
  • Stop Writing Dialect-Specific SQL: A Unified Query Builder for Node.js
  • S3 Vectors: How to Build a RAG Without a Vector Database
  1. DZone
  2. Coding
  3. Java
  4. Stranger Things in Java: Enum Types

Stranger Things in Java: Enum Types

Java enum types are more than named constants. This article explains how they work and why they matter in real Java applications.

By 
Claudio De Sio Cesari user avatar
Claudio De Sio Cesari
·
Mar. 16, 26 · Analysis
Likes (1)
Comment
Save
Tweet
Share
4.3K Views

Join the DZone community and get the full member experience.

Join For Free

This article is part of the series “Stranger things in Java,” dedicated to language deep dives that will help us master even the strangest scenarios that can arise when we program. All articles are inspired by content from the book “Java for Aliens” (in English), the book “Il nuovo Java”, and the book “Programmazione Java.”

This article is a short tutorial on enumeration types, also called enumerations or enums. They are one of the fundamental constructs of the Java language, alongside classes, interfaces, annotations, and records. They are particularly useful to represent sets of known and unchangeable values, such as the days of the week or the cardinal directions.

What Is an Enum?

An enum is declared with the enum keyword and typically contains a list of values, called the elements (or values, or also constants) of the enumeration.

Let’s consider, for example:

Java
 
public enum CardinalDirection {    
    NORTH, SOUTH, WEST, EAST; 
}


Here, we defined an enum named CardinalDirection, with four elements: NORTH, SOUTH, WEST and EAST. The elements defined in the enumeration are the only possible instances of type CardinalDirection, and it is not possible to instantiate other objects of the same type. Therefore, if we tried to instantiate an object from the CardinalDirection enumeration, we would get a compilation error:

Java
 
var d = new CardinalDirection(); // ERROR: you cannot create new instances


Elements of an Enumeration

Using an enumeration, therefore, mainly means using its elements. For example, the following method returns true if the direction parameter matches the NORTH element of CardinalDirection:

Java
 
static boolean isNorth(CardinalDirection direction) {    
    return direction == CardinalDirection.NORTH; 
}


In the following example, instead, we assign references to the elements of CardinalDirection:

Java
 
CardinalDirection d1 = CardinalDirection.SOUTH; 
System.out.println(d1 == CardinalDirection.SOUTH); // true 
var d2 = CardinalDirection.EAST;
System.out.println(d2 == CardinalDirection.WEST); // false


Each element is implicitly declared public, static and final. In fact, from these examples we can observe that:

  • To use the elements of an enumeration, you must always refer to them via the name of the enumeration (for example CardinalDirection.SOUTH).
  • We can compare elements directly with the == operator because they are implicitly final and unique.
  • The names of enumeration elements follow the naming conventions for constants.

During compilation, the CardinalDirection enumeration is transformed into a class similar to the following:

Java
 
public class CardinalDirection {    
    public static final CardinalDirection NORTH = new CardinalDirection();    
    public static final CardinalDirection SOUTH = new CardinalDirection();    
    public static final CardinalDirection WEST  = new CardinalDirection();    
    public static final CardinalDirection EAST  = new CardinalDirection(); 
}


While the compiler ensures that no other elements can be instantiated besides those declared in the enumeration.

* Backward compatibility is a fundamental feature of Java that ensures code written for earlier versions of the platform continues to work on more recent versions of the JVM, without requiring changes. Backward compatibility is one of the main reasons why Java is widely used in enterprise environments and long-lived systems. 

Why Use Enumerations?

One of the main advantages of enumerations is the ability to represent a limited set of values in a safe way. Without an enum, there is a risk of using strings or “magic” numbers, which can introduce errors that are difficult to detect.

Let us consider the following example:

Java
 
public class Compass {
    public void move(String direction) {
        String message;
        if (direction.equals("NORTH")) {
            message = "You move north";
        } else if (direction.equals("SOUTH")) {
            message = "You move south";
        } else if (direction.equals("WEST")) {
            message = "You move west";
        } else if (direction.equals("EAST")) {
            message = "You move east";
        } else {
            message = "Invalid direction: " + direction;
        }
        System.out.println(message);
    }
}


With this approach, it is possible to pass any string, even an invalid one. For example, the value of the direction parameter could be "north" or "North", but it should be "NORTH" in order for the method to work correctly. The compiler cannot help us prevent such errors.

In the following code, we use the CardinalDirection enumeration to completely eliminate arbitrary values and delegate to the compiler the validation of the allowed values:

Java
 
public class Compass {
    public void move(CardinalDirection direction) {
        String message = switch (direction) {
            case NORTH -> "You move north";
            case SOUTH -> "You move south";
            case WEST  -> "You move west";
            case EAST  -> "You move east";
        };
        System.out.println(message);
    }
}


In this way:

  • The direction parameter can only take the values defined in the enumeration.
  • It is not possible to specify an invalid value: such an error would be detected at compile time.
  • The switch expression must be exhaustive**, therefore the compiler requires that all alternatives are handled in order to compile without errors.

Enumerations make code safer and more readable, because they avoid the use of “magic” values or arbitrary strings to represent concepts that have a limited number of alternatives.

 ** To learn about the concept of exhaustiveness related to switch expressions, introduced in Java 14, you can read the article entitled “The new switch.”

Enumerations and Inheritance

We have seen that the compiler transforms the CardinalDirection enum into a class whose elements are implicitly declared public, static, and final. However, we have not yet said that such a class:

  • Is itself declared final. This implies that enumerations cannot be extended.
  • Extends the generic class java.lang.Enum. Consequently, it cannot extend other classes (but it can still implement interfaces).

In practice, the declaration of the CardinalDirection class will be similar to the following:

Java
 
public final class CardinalDirection extends Enum<CardinalDirection> {
    // rest of the code omitted 
}


Therefore, we cannot create hierarchies of enumerations in the same way we do with classes.

Moreover, all enumerations inherit:

  • The methods declared in the Enum class.
  • The methods and properties of the Serializable and Comparable interfaces, which are implemented by Enum.
  • The methods from the Object class.

However, in the last paragraph of this tutorial, we will see how it is possible, in some sense, to extend an enumeration. 

Methods Inherited From the Enum Class

By extending Enum, enumerations inherit several methods:

  • name: returns the name of the element as a string (it cannot be overridden because it is declared final).
  • toString: returns the same value as name, but it can be overridden.
  • ordinal: returns the position of the element in the enumeration starting from index 0 (it is declared final).
  • valueOf: a static method that takes a String as input and returns the enumeration element corresponding to the name.
  • values: a static method not actually present in java.lang.Enum, but generated by the compiler for each enumeration. It returns an array containing all enumeration elements in the order in which they are declared.

For example, the name method is defined to return the element name, so:

Java
 
System.out.println(CardinalDirection.SOUTH.name()); // prints SOUTH


will print the string "SOUTH". The equivalent toString method also returns the enum name, so the instruction:

Java
 
System.out.println(CardinalDirection.SOUTH); // prints SOUTH


produces exactly the same result (since println calls toString on the input object). The difference is that the name method cannot be overridden because it is declared final, while the equivalent toString method can always be overridden.

Enum also declares a method complementary to toString: the static method valueOf. It takes a String as input and returns the corresponding enumeration value. For example:

Java
 
CardinalDirection direction = CardinalDirection.valueOf("NORTH");
System.out.println(direction == CardinalDirection.NORTH); // prints true


The special static values method returns an array containing all enumeration elements in the order in which they were declared. You can use this method to iterate over the values of an enumeration you do not know. For example, using an enhanced for loop, we can print the contents of the CardinalDirection enumeration, also introducing the ordinal method:

Java
 
for (CardinalDirection cd : CardinalDirection.values()) {
    System.out.println(cd + "\t is at position " + cd.ordinal());
}


Note that the ordinal method (also declared final) returns the position of an element within the array returned by values. The output of the previous example is therefore:

Java
 
NORTH    is at position 0
SOUTH    is at position 1 
WEST     is at position 2 
EAST     is at position 3


For completeness, the Enum class actually declares two other, less interesting methods:

  • getDeclaringClass: returns the Class object associated with the enum type to which the value on which the method is invoked belongs.
  • describeConstable: a method introduced in Java 12 to support advanced constant descriptions for low-level APIs. This is a specialized API that is not used in traditional application development.

Methods and Properties Inherited From the Serializable and Comparable Interfaces

The Enum class implements the Serializable and Comparable interfaces, and consequently, enumeration objects have the properties of being serializable and comparable.

While the marker interface Serializable does not contain methods, the functional interface Comparable makes the natural ordering of the elements of an enumeration coincide with the order in which the elements are defined within the enumeration itself. This means that the only abstract method compareTo of the Comparable interface determines the ordering of two enumeration objects based on the position of the objects within the enumeration.

Note that the compareTo method is declared final in the Enum class, and therefore it cannot be overridden in our enumerations.

Methods Inherited From the Object Class

Enumerations inherit all 11 methods of the Object class.

In particular, as already mentioned, we can override the toString method.

The other methods we usually override, such as equals, hashCode, and clone, are instead declared final and therefore cannot be overridden.

In fact, to compare two enumeration instances, it is sufficient to use the == operator, since enumeration values are constants, and therefore, there is no need to redefine the equals and hashCode methods.

Moreover, enumerations cannot be cloned, since their elements must be the only possible instances. In this case as well, the java.lang.Enum class declares the clone method inherited from Object as final. Being also declared protected, it is not even visible outside the java.lang package of Enum.

Customizing an Enumeration

Since it is transformed into a class, an enumeration can declare everything that can be declared in a class (methods, variables, nested types, initializers, and so on), with some constraints on constructors. In fact, constructors are implicitly considered private and can only be used by the enumeration elements through a special syntax. Compilation will instead fail if we try to create new instances using the new operator.

For example, the following code redefines the CardinalDirection enumeration:

Java
 
public enum CardinalDirection {
    NORTH("north"),   // invokes constructor 2
    SOUTH("south"),   // invokes constructor 2
    WEST("west"),     // invokes constructor 2
    EAST;             // invokes constructor 1
                      // equivalent to EAST()
    // instance variable
    private String description;
     
    // constructor number 1
    private CardinalDirection() {
        this("east"); // calls constructor 2
    }

    // constructor number 2 (implicitly private)
    CardinalDirection(String direction) {
        setDescription("direction " + direction);
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    @Override
    public String toString() {
        return "We are pointing " + description;
    }
}


We can observe that:

  • We declared two constructors in the enumeration: one explicitly private and the other implicitly private. It is not possible to declare constructors with public or protected visibility. Apart from this, the same rules that apply to class constructors also apply here. As with classes, if we do not declare any constructors, the compiler will add a no-argument constructor for us (the default constructor). Also, as with classes, the default constructor will not be generated if we explicitly declare at least one constructor, as in the previous example.
  • The enumeration elements invoke the declared constructors using a special syntax. In the declaration of the NORTH, SOUTH, and WEST elements, we added a pair of parentheses and passed a string parameter. This ensures that constructor number 2, which takes a String parameter, is invoked when these instances are created. The EAST element, instead, does not use parentheses and therefore invokes the no-argument constructor. Note that we could also have added an empty pair of parentheses to obtain the same result.
  • The declaration of the enumeration elements must always precede all other declarations. If we placed any declaration before the element list, compilation would fail. Note that the semicolon after the element list is optional if no other members are declared.

Static Nested Enumerations and Static Imports

It is not uncommon to create nested enumerations, which, unlike nested classes, are always static. For example, suppose we want to create an enumeration that defines the possible account types (for example "standard" and "premium") for customers of an online shop. Since the account type is strictly related to the concept of an account, it makes sense to declare the Type enumeration nested within the Account class:

Java
 
package com.online.shop.data; 
public class Account {
    public enum Type {STANDARD, PREMIUM} // static nested enum

    // other code omitted...

    public static void main(String[] args) {
        System.out.println(Type.PREMIUM); // access to the static enumeration
    }
}


If instead we want to print an enumeration element from outside the Account class, we can use the following syntax:

Java
 
System.out.println(Account.Type.PREMIUM);


Of course, we can also use static import when appropriate, for example:

Java
 
import static com.online.shop.data.Account.Type; 
// ... 
System.out.println(Type.PREMIUM);


Or even:

Java
 
import static com.online.shop.data.Account.Type.PREMIUM;
// ... 
System.out.println(PREMIUM);


Enumerations and static import were both introduced in Java 5. static import, in fact, allows us to reduce verbosity when using enumerations.

Extending an Enumeration

We know that we cannot extend an enumeration; however, it is possible to use the anonymous class syntax for each element, redefining the methods declared in the enumeration. We can define methods in the enumeration and override them in its elements. Let us rewrite the CardinalDirection enumeration once again:

Java
 
public enum CardinalDirection {
    NORTH {
        @Override
        public void test() {
            System.out.println("method of NORTH");
        }
    },
    SOUTH, WEST, EAST;

    public void test() {
        System.out.println("method of the enum");
    } 
}


Here, we defined a method called test that prints the string "method of the enum". The NORTH element, however, using a syntax similar to that of anonymous classes, also declares the same method, overriding it. In fact, the compiler will turn the NORTH element into an instance of an anonymous class that extends CardinalDirection. Therefore, the statement:

Java
 
CardinalDirection.NORTH.test();


Will print:

Java
 
method of NORTH


While the statement:

Java
 
CardinalDirection.SOUTH.test();


Will print:

Java
 
method of the enum


Because SOUTH does not override test. The same output will be produced when invoking the test method on the EAST and WEST elements as well.

Enumerations and Polymorphism

After examining the relationship between enumerations and inheritance, we can now use enumerations by exploiting polymorphism in a more advanced way. For example, let us consider the following Operation interface:

Java
 
public interface Operation {
    boolean execute(int a, int b); 
}


We can implement this interface within an enumeration Comparison, customizing the implementation of the execute method for each element:

Java
 
public enum Comparison implements Operation {
    GREATER {
        public boolean execute(int a, int b) {
            return a > b;
        }
    },
    LESS {
        public boolean execute(int a, int b) {
            return a < b;
        }
    },
    EQUAL {
        public boolean execute(int a, int b) {
            return a == b;
        }
    }; 
}


With this structure, we can write code such as the following:

Java
 
boolean result = Comparison.GREATER.execute(10, 5); 
System.out.println("10 greater than 5 = " + result); 
result = Comparison.LESS.execute(10, 5); 
System.out.println("10 less than 5 = " + result); 
result = Comparison.EQUAL.execute(10, 5); 
System.out.println("10 equal to 5 = " + result);


Which will produce the following output:

Java
 
10 greater than 5 = true 
10 less than 5 = false 
10 equal to 5 = false


Implementing an interface in an enumeration allows you to associate a behavior with each enum value and exploit polymorphism, making the code more extensible, readable, and robust.

Conclusion

Enumerations are particularly useful when:

  • The domain of values is closed and known in advance, such as cardinal directions, object states, days of the week, priority levels, and so on.
  • You want to make code safer by eliminating arbitrary strings or “magic” numbers.
  • Each element must be able to have specific properties or methods, while maintaining clarity and readability.

They are less suitable when:

  • The elements can vary dynamically over time, for example, if they come from a database or external configurations.
  • You want to model an extensible hierarchy of types, for which classes and interfaces remain more flexible solutions.

In this article, we have seen that enumerations are not simply lists of constants, but real classes with predefined instances, methods inherited from Enum, the ability to implement interfaces, and even the possibility to redefine behavior for individual values through anonymous classes.

These aspects make enum a surprisingly powerful and, in some cases, unexpected tool: a perfect example of stranger things in Java.

Author’s Note

This article is based on some paragraphs from chapters 4 and 7 of my book “Programmazione Java” and from my English book “Java for Aliens.”

Element Java (programming language) Strings

Published at DZone with permission of Claudio De Sio Cesari. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Immutable Objects Using Record in Java
  • Squid Game: The Clean Code Trials — A Java Developer's Survival Story
  • Writing DTOs With Java8, Lombok, and Java14+
  • Formatting Strings in Java: String.format() Method

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook