An enum is a programming construct that is available in most programming languages. In this blog, we'll see what makes enums useful, how to use an enum in your code, and when not to use an enum, all in the context of the Java programming language. So, let's begin.
Enumerations, or enums for short, are a custom data type that can take on a value from among a few options that the programmer explicitly lists down. Enums are useful in scenarios where we want to represent entities with discrete states, day of the week, the state of matter (solid, liquid, or gas), etc.
Suppose that we are creating an application to manage the course creation process at Educative. Let's further suppose that the course creation pipeline consists of the following stages:
Ideation: A course idea is proposed. It may be approved or rejected.
Creation: Content is written.
Proofreading: Content is proofread.
Launch: The course is launched.
Here's a Java program that represents this flow:
class Course {// Represents the current stage of the course creationint courseStage;// The proposed course nameString courseName;// Constructor for the coursepublic Course(String name) {courseName = name;courseStage = 0;}// Approve a coursevoid approve() {if (courseStage != 0) {System.out.println("Course is already approved!");return;}courseStage = 1;}// Send a course for proofreadingvoid sendForProofreading() {if (courseStage != 1) {System.out.println("Only a course in creation stage can be sent for proofreading!");return;}courseStage = 2;}// Launch a coursevoid launch() {if (courseStage != 2) {System.out.println("Only a proofread course not already launched, can be launched!");return;}courseStage = 3;}// Returns a string representation of the coursepublic String toString() {StringBuilder courseRep = new StringBuilder();courseRep.append("The course " + courseName + " is in ");if (courseStage == 0) {courseRep.append("Ideation");}else if (courseStage == 1) {courseRep.append("Creation");}else if (courseStage == 2) {courseRep.append("Proofreading");}else {courseRep.append("Launch");}courseRep.append(" stage.");return courseRep.toString();}}class Application {public static void main( String args[] ) {Course c1 = new Course("Docker for Developers");System.out.println(c1);c1.approve();System.out.println(c1);c1.approve();c1.sendForProofreading();System.out.println(c1);c1.launch();System.out.println(c1);}}
We have defined a class named Course that represents a course in the pipeline. This class has an int attribute named courseStage, which represents the current stage of the course. We are using the following encoding for the course stage:
Integer value | Corresponding pipeline stage |
0 | Ideation |
1 | Creation |
2 | Proofreading |
3 | Launch |
We have defined methods in the Course class to move the course from one pipeline stage to the next. We've also implemented some rudimentary sanity checking in these methods. For example, as we can see in lines 13 – 16 of the code above, we don't approve a course unless it is in the ideation stage. In main(), we create a Course object named c1 and take it through the pipeline (lines 58 – 66).
There may be several problems with this solution depending on the perspective that you look at it from. From this blog's perspective, there are two main problems:
1- The code assigned to each stage is arbitrary and like a magic number.
2- Changing the code to adapt to changes in the course creation process is prone to error.
Imagine what happens if we decide to insert a new stage somewhere in the pipeline. Pause for a moment and think about this.
For the moment, ignore the open-closed principle and assume that we are willing to modify the source code.
Let's think about where in the pipeline we decide to insert a new stage.
Suppose that we sometimes run marketing campaigns after a course is launched. In this case, we could add a stage at the end of the pipeline and update it to the following:
Does this create any problems in our code? We still need to add a new promotion() method to the Course class. The courseStage attribute for a course in the promotion stage would be set to 4, and we'll need to add a new condition to the toString() method. That's it. No other method implementations get affected.
Suppose that we decide to insert a new stage, say "author search", to the pipeline.
In this case, we'd need to add a method authorSearch() to the Course class. What about the courseStage encoding? Do we assign -1 to represent this new stage since 0 is already taken for the Ideation stage? That doesn't feel right.
If we re-assign 0 to Author Search, and increment the code for all existing stages, then we'd have to update code in all the methods. That's not considered a good practice. It's always possible to forget to make changes at one or two places, thereby introducing bugs.
By now, the suspense is broken. This is kind of similar to case 2. So, no point in discussing this in detail. Summary: there's a problem.
Most programming languages provide enumerations as a solution to this kind of problem. Enumerations, or enums, for short, are a custom data type that can take on a value from among a few options that the programmer explicitly lists down. Here's our earlier program re-written with enums.
enum CourseStage {IDEATION,CREATION,PROOFREADING,LAUNCH}class Course {// Represents the current stage of the course creationCourseStage courseStage;// The proposed course nameString courseName;// Constructor for the coursepublic Course(String name) {courseName = name;courseStage = CourseStage.IDEATION;}// Approve a coursevoid approve() {if (courseStage != CourseStage.IDEATION) {System.out.println("Course is already approved!");return;}courseStage = CourseStage.CREATION;}// Send a course for proofreadingvoid sendForProofreading() {if (courseStage != CourseStage.CREATION) {System.out.println("Only a course in creation stage can be sent for proofreading!");return;}courseStage = CourseStage.PROOFREADING;}// Launch a coursevoid launch() {if (courseStage != CourseStage.PROOFREADING) {System.out.println("Only a proofread course not already launched, can be launched!");return;}courseStage = CourseStage.LAUNCH;}// Returns a string representation of the coursepublic String toString() {StringBuilder courseRep = new StringBuilder();courseRep.append("The course " + courseName + " is in ");if (courseStage == CourseStage.IDEATION) {courseRep.append("Ideation");}else if (courseStage == CourseStage.CREATION) {courseRep.append("Creation");}else if (courseStage == CourseStage.PROOFREADING) {courseRep.append("Proofreading");}else {courseRep.append("Launch");}courseRep.append(" stage.");return courseRep.toString();}}class Application {public static void main( String args[] ) {Course c1 = new Course("Docker for Developers");System.out.println(c1);c1.approve();System.out.println(c1);c1.approve();c1.sendForProofreading();System.out.println(c1);c1.launch();System.out.println(c1);}}
We declare an enumeration on lines 1 – 6 using the enum keyword, which is followed by a name for the enum. Within the curly braces, we list down constants that represent all the possible values for this enumeration. It is a convention to use all caps for the enum constants.
An enum declaration is like a class declaration. Just as an object of a (non-static) class needs to be defined somewhere in the program, an enum variable must also be declared. We do this in line 9 in the code above. From this point onwards, a variable of type CourseStage can take on any one of the values defined on lines 2 – 5. On line 15, we initialize this variable to CourseStage.IDEATION. Notice that we use the enum name followed by a dot operator followed by the constant value. Similarly, we've used the enum values throughout the rest of the program.
This gets rid of the magic numbers from the program. Updating the code to adapt to changes in the course creation process is still prone to error. However, we no longer have the magic numbers splattered all over the code. The constants are human-readable and they convey the meaning to the reader.
Let's see what happens if we try to display an enum value:
enum CourseStage {IDEATION,CREATION,PROOFREADING,LAUNCH}class Application {public static void main( String args[] ) {System.out.println(CourseStage.IDEATION);System.out.println(CourseStage.CREATION);System.out.println(CourseStage.PROOFREADING);System.out.println(CourseStage.LAUNCH);}}
The above program displays text like "IDEATION," "CREATION," etc on the console. If you are coming from a C++ background, you might have expected to see integers 0, 1, 2, and 3 on the console.
Let's see if we can force the Java compiler to use the enum constants as integers when used in arithmetic.
enum CourseStage {IDEATION,CREATION,PROOFREADING,LAUNCH}class Application {public static void main( String args[] ) {System.out.println(CourseStage.IDEATION + 1);}}
Nope! It turns out that this throws a compilation error. But, don't worry! The base class for all Java enums is java.lang.Enum. This base class provides a few useful methods, one of which is ordinal(), which represents the ordinal corresponding to a particular enum constant. So, we could fix the compilation error in the above program as follows:
enum CourseStage {IDEATION,CREATION,PROOFREADING,LAUNCH}class Application {public static void main( String args[] ) {System.out.println(CourseStage.IDEATION.ordinal() + 1);}}
If you’re teaching someone what is enum in Java, show the “daily driver” APIs they’ll use constantly:
values() returns an array of all constants (iteration, UI lists).
valueOf(String) parses by exact constant name (throws if unknown).
name() is the declared name; toString() defaults to name() but can be overridden for display.
compareTo() orders by declaration order; avoid relying on that order for business logic.
Using switch keeps intent obvious and avoids if/else ladders:
switch (stage) {case IDEATION -> approveIdea();case CREATION -> writeContent();case PROOFREADING -> review();case LAUNCH -> publish();}
Prefer switching early at the boundary and delegating to methods so the rest of your code stays polymorphic and clean.
A powerful way to explain what enum is in Java is to show that enums can implement interfaces and attach behavior per constant:
interface NextAction { void perform(); }enum CourseStage implements NextAction {IDEATION { public void perform() { approveIdea(); } },CREATION { public void perform() { writeContent(); } },PROOFREADING { public void perform() { review(); } },LAUNCH { public void perform() { publish(); } };// shared helpers or state can sit here (final fields, methods)static void approveIdea() { /* ... */ }static void writeContent() { /* ... */ }static void review() { /* ... */ }static void publish() { /* ... */ }}
Each constant overrides perform()—no switch required where the behavior is consumed:
void advance(CourseStage stage) { stage.perform(); }
This keeps responsibilities localized to the enum and scales better than scattered switch cases.
Several practitioners discourage the use of enums and call it a code smell. A primary reason for this is that using enums often results in switch-case or if-else ladders splattered all over the program. This violates the single-responsibility principle. If we have to make any changes to an enum declaration, we have to update the code at several places. Missing the changes at any place in the program may introduce bugs.
Instead of enums, practitioners recommend using classes with inheritance. In the above example, we could create a hierarchy with a CourseState class at the base, and derived classes Ideation, Creation, ProofReading, Launch etc. above it.
Enums in Java are very similar to classes because in addition to the constants representing the discrete values for the type, we can declare other member variables and methods too.
Let's take an example in which we have implemented a distributed leader election protocol. In this protocol, when a node comes online, it sends a HELLO packet to discover the network. In response, it receives one or more HELLO packets as acknowledgment. Then, it sends a JOIN packet to join the group of nodes already there. The nodes elect a leader by sending ELECTLEADER packets. A node may leave any time by sending a LEAVE message. Each type of packet has a predefined size.
In the following program, we define an enum to represent these different types of packets. We have represented the packet types using names like HELLO and JOIN. But what is that parenthesis syntax on line 2, for instance? It looks like a constructor, right? In fact, it is. You'll find that thix constructor is defined on lines 7 -- 9. It takes an argument sz and assigns its value to the enum instance member variable size (defined on line 6). So, line 2 effectively means that if we declare a variable of type PacketType.HELLO, it will have an instance member variable size equal to 10.
We might want to know how long (in milliseconds) it will take to transmit a packet of a certain type on a network interface with a given line rate in kilobits per second. To calculate this, we've defined an instance member method getTransmissionDelay() in lines 10 -- 11.
In the main() method, as an example, we've created a HELLO packet and displayed its transmission delay on a 100 kbps network interface.
enum PacketType {HELLO (10),JOIN (100),ELECTLEADER (500),LEAVE (10);private final int size;PacketType(int sz) {size = sz;}public double getTransmissionDelay(int lineRate) {return (double)size / lineRate;}}class Application {public static void main( String args[] ) {PacketType p1 = PacketType.HELLO;System.out.println(p1.getTransmissionDelay(100));}}
A key differentiator between classes and enums in Java is that the latter are final and hence, you cannot create type hierarchies based on your enums.
A rounded understanding of what enum is in Java includes a few rules of thumb:
Constructors are implicitly private: you can add fields and constructors, but you can’t call them directly or subclass an enum. Constant instances are created only by the declaration.
Serialization is special: enums serialize by name, not by field values; don’t add mutable state you expect to round-trip.
Never persist ordinal(): declaration order can change. Persist name() (stable) or an explicit code:
enum PacketType {HELLO("H"), JOIN("J"), LEAVE("L");private final String code;PacketType(String code) { this.code = code; }public String code() { return code; }public static Optional<PacketType> fromCode(String c) {for (var t : values()) if (t.code.equalsIgnoreCase(c)) return Optional.of(t);return Optional.empty();}}
Parse safely: wrap valueOf or provide a fromString that returns Optional/default to avoid exceptions from user input.
JSON mapping: most libraries default to using name(). If you’ve defined custom codes, configure your serializer or add a getter to expose it.
Thread-safety: enum instances are singletons and inherently thread-safe; prefer them for stateless strategies and registries.
These practices reduce brittleness and make enums reliable as public, persisted, and interoperable types.
Enums are a convenient way to represent data that can take on one of a few discrete values. In Java, enums are like classes. In fact, every enum in Java is derived from java.lang.Enum. We may define instance member variables and methods in an enum, which would allow us to keep all implementation focused in one place. Enums in Java are final and you cannot create an enum derived from a user-defined enum. When representing a complex scenario, it might be worthwhile creating a class hierarchy rather than using enums.
We hope that this blog piqued your curiosity. To learn more, you might find the following courses useful:
The All-in-One Guide to Java Programming is a great course if you want to refresh your Java knowledge.
Collections in Java is a great course if you want to learn about the key collection types in Java.
Build Your Robot World in Java is a fun short course in which you will build a robot world in Java, step by step.