Java Tutorial
🔍

Java Enums

Java Enums

An enum is a class that represents a fixed set of constants - declared once, known completely at compile time, and impossible to extend with a value that was not part of the original set. Declaring enum DeliveryZone with constants LOCAL, REGIONAL, NATIONAL, and INTERNATIONAL gives you four objects, each its own singleton instance of DeliveryZone, and the compiler will not let a DeliveryZone variable hold anything else - not null accidentally typed as a string, not an out-of-range integer, nothing but one of those four. Before enums existed (Java 5 introduced them), this kind of fixed set was almost always represented with int or String constants, and almost every codebase that did this eventually shipped a bug where one of those constants was compared to the wrong group of constants - because to the compiler, they were all just integers or strings.

What Is an Enum?

An enum is a special kind of class declaration. Every enum implicitly extends java.lang.Enum<E>, and each constant listed in its body becomes a public static final instance of that enum type - created exactly once, when the enum class is first loaded, and never again.

enum DeliveryZone {
    LOCAL, REGIONAL, NATIONAL, INTERNATIONAL
}

  is roughly equivalent to the compiler generating:

final class DeliveryZone extends java.lang.Enum<DeliveryZone> {
    public static final DeliveryZone LOCAL         = new DeliveryZone("LOCAL", 0);
    public static final DeliveryZone REGIONAL      = new DeliveryZone("REGIONAL", 1);
    public static final DeliveryZone NATIONAL      = new DeliveryZone("NATIONAL", 2);
    public static final DeliveryZone INTERNATIONAL = new DeliveryZone("INTERNATIONAL", 3);

    private DeliveryZone(String name, int ordinal) { super(name, ordinal); }
}

That generated picture explains most of what makes enums behave the way they do: there are exactly four DeliveryZone objects in the entire JVM, ever; each one knows its own name and its position (ordinal) in the declaration; and the constructor is private, so nothing outside this generated code can ever create a fifth one.

Basic Overview - The Shapes an Enum Can Take

SHAPE 1 - SIMPLE CONSTANTS ONLY
  enum DeliveryZone { LOCAL, REGIONAL, NATIONAL, INTERNATIONAL }

  Fresher view  : a fixed list of named values - like a small,
                  closed set of valid options
  Deeper view   : each name is a singleton object, comparable with
                  == , usable in switch, iterable via values()

SHAPE 2 - CONSTANTS WITH FIELDS, A CONSTRUCTOR, AND METHODS
  enum TrainClass {
      SLEEPER(1.0, 72), AC_3_TIER(2.5, 64), ...
      TrainClass(double fareMultiplier, int berths) { ... }
      double calculateFare(double base) { return base * fareMultiplier; }
  }

  Fresher view  : each constant carries its own data, set up once
                  when the constant is declared
  Deeper view   : the constructor runs ONCE per constant, at class
                  loading time, in declaration order - it is never
                  called again and is implicitly private

SHAPE 3 - CONSTANT-SPECIFIC METHOD BODIES
  enum PaymentMethod implements FeeCalculator {
      UPI { public double calculateFee(double amt) { return 0.0; } },
      CARD { public double calculateFee(double amt) { return amt * 0.02; } };
  }

  Fresher view  : each constant can have its OWN version of a method,
                  instead of one shared implementation with branching
  Deeper view   : each constant with its own body is, under the hood,
                  an anonymous subclass of the enum type - PaymentMethod
                  itself can remain effectively abstract for that method

SHAPE 4 - ENUM IMPLEMENTING AN INTERFACE
  enum PaymentMethod implements FeeCalculator { ... }

  Fresher view  : the enum can be used anywhere a FeeCalculator is
                  expected, just like any other class implementing it
  Deeper view   : an enum can implement any number of interfaces - the
                  single-inheritance slot used by "extends Enum" does
                  not count against interface implementation

A fresher mainly needs shape 1 to start - most enums encountered while learning are exactly this. Shape 2 is where enums start replacing real configuration data that used to live in separate maps or constant classes. Shapes 3 and 4, used together as shown above, are where enums stop being "named constants" and start being small, purpose-built classes - and where most of the genuinely interesting interview questions live.

Why Enums Matter

The problem enums solve is best seen by looking at how the same idea was represented before Java 5. A set of related constants was typically a handful of public static final int fields, grouped in a class or interface purely for organization.

BEFORE - int constants (pre-Java 5 style):

  public class TrainClassConstants {
      public static final int SLEEPER   = 0;
      public static final int AC_3_TIER = 1;
      public static final int AC_2_TIER = 2;
      public static final int AC_1_TIER = 3;
  }

  void bookTicket(int trainClass) {
      if (trainClass == TrainClassConstants.SLEEPER) { ... }
  }

  THE PROBLEM:
    bookTicket(99)                              compiles - 99 is just an int
    bookTicket(TrainClassConstants.SLEEPER + 5) compiles - arithmetic on a "constant"
    bookTicket(SomeOtherConstants.PENDING)      compiles - both are just ints,
                                                  the compiler cannot tell these
                                                  constants apart by MEANING

AFTER - enum:

  enum TrainClass { SLEEPER, AC_3_TIER, AC_2_TIER, AC_1_TIER }

  void bookTicket(TrainClass trainClass) { ... }

  bookTicket(99)                  does NOT compile - 99 is not a TrainClass
  bookTicket(SomeOtherEnum.X)     does NOT compile - wrong type entirely
  TrainClass.values().length      always exactly 4, known at compile time

The compiler now enforces, at every call site, that only one of the four intended values can ever be passed - not because of a comment or a naming convention, but because TrainClass is its own type, unrelated to int or to any other enum. This single change - making the set of valid values part of the type system instead of a convention - is the entire reason enums exist, and it is why "why not just use constants" is one of the most telling interview follow-ups on this topic: the answer is always some version of "because the compiler stops catching mistakes the moment the values are interchangeable primitives."

How Enums Work

Declaring Enums and Using Built-in Methods

Every enum, with zero extra code, comes with values() (an array of every constant, in declaration order), name() (the constant's declared identifier as a String), ordinal() (its zero-based position in that declaration), valueOf(String) (the reverse of name()), and compareTo() (ordering by ordinal()). Enums also work directly in switch - including the newer arrow-style switch expressions.

// File: EnumBasicsDemo.java public class EnumBasicsDemo { enum DeliveryZone { LOCAL, REGIONAL, NATIONAL, INTERNATIONAL } static String estimateDays(DeliveryZone zone) { return switch (zone) { case LOCAL -> "1 day"; case REGIONAL -> "2-3 days"; case NATIONAL -> "4-6 days"; case INTERNATIONAL -> "10-15 days"; }; } public static void main(String[] args) { System.out.println("=== All constants via values() ==="); for (DeliveryZone zone : DeliveryZone.values()) { System.out.printf(" %-13s ordinal=%d delivery: %s%n", zone.name(), zone.ordinal(), estimateDays(zone)); } System.out.println(); System.out.println("=== valueOf() - converting a String to an enum constant ==="); DeliveryZone zone = DeliveryZone.valueOf("REGIONAL"); System.out.println("Parsed: " + zone); System.out.println(); System.out.println("=== compareTo() - ordering follows declaration order ==="); System.out.println("LOCAL.compareTo(NATIONAL) = " + DeliveryZone.LOCAL.compareTo(DeliveryZone.NATIONAL)); System.out.println("NATIONAL.compareTo(LOCAL) = " + DeliveryZone.NATIONAL.compareTo(DeliveryZone.LOCAL)); System.out.println(); System.out.println("=== valueOf() with an unknown name throws IllegalArgumentException ==="); try { DeliveryZone.valueOf("GLOBAL"); } catch (IllegalArgumentException e) { System.out.println("Caught: " + e.getMessage()); } } }
Output:
=== All constants via values() ===
  LOCAL         ordinal=0  delivery: 1 day
  REGIONAL      ordinal=1  delivery: 2-3 days
  NATIONAL      ordinal=2  delivery: 4-6 days
  INTERNATIONAL ordinal=3  delivery: 10-15 days

=== valueOf() - converting a String to an enum constant ===
Parsed: REGIONAL

=== compareTo() - ordering follows declaration order ===
LOCAL.compareTo(NATIONAL) = -2
NATIONAL.compareTo(LOCAL) = 2

=== valueOf() with an unknown name throws IllegalArgumentException ===
Caught: No enum constant EnumBasicsDemo.DeliveryZone.GLOBAL

Enums With Fields, Constructors, and Methods

An enum constructor runs once per constant, in declaration order, when the enum class is first loaded - never on demand, never again. The constructor is implicitly private, and the values passed to it are written directly after each constant's name.

// File: EnumFieldsDemo.java public class EnumFieldsDemo { enum TrainClass { SLEEPER(1.0, 72), AC_3_TIER(2.5, 64), AC_2_TIER(3.8, 48), AC_1_TIER(6.0, 24); private final double fareMultiplier; private final int berthsPerCoach; // Runs ONCE per constant, at class-loading time, in the order // the constants are declared above. Implicitly private - this // cannot be called from anywhere else. TrainClass(double fareMultiplier, int berthsPerCoach) { this.fareMultiplier = fareMultiplier; this.berthsPerCoach = berthsPerCoach; } double calculateFare(double baseFare) { return baseFare * fareMultiplier; } int getBerthsPerCoach() { return berthsPerCoach; } } public static void main(String[] args) { double baseFare = 450.0; System.out.println("=== Fare for each class on a base fare of Rs.450 ==="); for (TrainClass trainClass : TrainClass.values()) { System.out.printf(" %-10s Rs.%-8.2f %d berths/coach%n", trainClass, trainClass.calculateFare(baseFare), trainClass.getBerthsPerCoach()); } } }
Output:
=== Fare for each class on a base fare of Rs.450 ===
  SLEEPER    Rs.450.00    72 berths/coach
  AC_3_TIER  Rs.1125.00   64 berths/coach
  AC_2_TIER  Rs.1710.00   48 berths/coach
  AC_1_TIER  Rs.2700.00   24 berths/coach

Constant-Specific Method Bodies and Implementing Interfaces

When different constants need genuinely different behavior - not just different data - an enum can implement an interface and let each constant supply its own body for that interface's method. Every constant becomes, in effect, its own small anonymous subclass of the enum, overriding just that one method.

// File: EnumInterfaceDemo.java public class EnumInterfaceDemo { interface FeeCalculator { double calculateFee(double amount); } enum PaymentMethod implements FeeCalculator { UPI { @Override public double calculateFee(double amount) { return 0.0; // UPI transactions are fee-free } }, CARD { @Override public double calculateFee(double amount) { return amount * 0.02; // 2% processing fee } }, NET_BANKING { @Override public double calculateFee(double amount) { return 10.0; // flat fee regardless of amount } }, WALLET { @Override public double calculateFee(double amount) { return amount * 0.01; // 1% processing fee } }; // Every constant above supplies its OWN body for calculateFee - // PaymentMethod never needs a single shared implementation, and // no branching on "which constant is this" appears anywhere } public static void main(String[] args) { double amount = 2000.0; System.out.println("=== Processing fee for Rs.2000 by payment method ==="); for (PaymentMethod method : PaymentMethod.values()) { System.out.printf(" %-12s fee = Rs.%.2f%n", method, method.calculateFee(amount)); } } }
Output:
=== Processing fee for Rs.2000 by payment method ===
  UPI          fee = Rs.0.00
  CARD         fee = Rs.40.00
  NET_BANKING  fee = Rs.10.00
  WALLET       fee = Rs.20.00

Internal Working - Compiled Representation

Knowing what the compiler generates explains several rules that otherwise look arbitrary - why constructors must be private, why values() returns a fresh array each time, and why switch on an enum is fast.

WHAT THE COMPILER GENERATES FOR enum TrainClass { SLEEPER(1.0, 72), ... }:

  - TrainClass extends java.lang.Enum<TrainClass>
    (this uses Java's single class-inheritance slot - TrainClass
     cannot extend anything else, though it can still implement
     any number of interfaces)

  - Each constant becomes:
      public static final TrainClass SLEEPER =
          new TrainClass("SLEEPER", 0, 1.0, 72);
    ("SLEEPER" and 0 - the name and ordinal - are supplied by the
     compiler automatically; 1.0 and 72 are YOUR constructor arguments)

  - A private, synthetic array holding all constants in declaration order

  - public static TrainClass[] values() { return $VALUES.clone(); }
    (clone() - every call to values() returns a NEW array, so
     modifying the returned array cannot affect the real constants)

  - public static TrainClass valueOf(String name) { ... }
    (looks up a constant by its declared name; throws
     IllegalArgumentException, naming the enum's canonical class
     name and the unmatched value, if nothing matches)

WHY THE CONSTRUCTOR IS ALWAYS PRIVATE:
  enum TrainClass {
      ...
      TrainClass(double fareMultiplier, int berths) { ... } // implicitly private
  }
  Writing public or protected here is a COMPILE ERROR. If outside
  code could call new TrainClass(...), the "fixed set of constants"
  guarantee - the entire reason to reach for an enum - would no
  longer hold.

HOW switch ON AN ENUM COMPILES:
  Across javac versions, switching on an enum has been implemented
  using a small synthetic lookup table that maps each constant's
  ordinal() to the matching case - so the actual branching happens
  on a small integer, not on object identity or string comparison.
  This is part of why switch on enums is consistently fast.

EnumMap AND EnumSet:
  java.util.EnumMap<TrainClass, String> and java.util.EnumSet<TrainClass>
  work only with enum types. Internally, EnumMap stores values in an
  array indexed by ordinal(), and EnumSet stores membership as a bit
  vector. Both iterate in declaration order and are more compact and
  faster than HashMap or HashSet for the same enum keys - one of the
  few places where reaching for a specialized collection over the
  general-purpose one is close to a free win.

Basic Example

This small program declares a plain enum for the days of the week and uses it in a switch expression and a loop - shape 1 from the overview above.

public class Main { enum Day { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY } static boolean isWeekend(Day day) { return day == Day.SATURDAY || day == Day.SUNDAY; } public static void main(String[] args) { Day today = Day.SATURDAY; System.out.println("Today is: " + today); System.out.println("Is weekend? " + isWeekend(today)); System.out.println("All days:"); for (Day day : Day.values()) { System.out.println(" " + day.ordinal() + " -> " + day); } } }

Output

Today is: SATURDAY
Is weekend? true
All days:
  0 -> MONDAY
  1 -> TUESDAY
  2 -> WEDNESDAY
  3 -> THURSDAY
  4 -> FRIDAY
  5 -> SATURDAY
  6 -> SUNDAY

Day is a fixed set of seven constants - a Day variable can never hold anything else, unlike an int or String that could accidentally hold an out-of-range value. isWeekend() compares day against the enum constants using ==, which works because every constant is a singleton. Day.values() returns every constant in declaration order, and ordinal() gives each one's zero-based position in that order. Developers reach for an enum exactly like this whenever a value is "one of a known, fixed set" - order status, day of week, direction, and similar closed lists.

Enum vs Constants vs a Typesafe Class Hierarchy

ApproachType SafetyFixed Set EnforcedPer-Constant BehaviorWorks in switchBoilerplate
int / String constantsNone - any int/String compilesNo - any value of that primitive type compilesNo - requires external if/switch logicYes, but on the raw valueMinimal
Typesafe enum pattern (pre-Java 5: one class, private constructor, public static final instances)FullYes, by constructionYes - one subclass per constantNo - predates enum-aware switchSignificant - a full class plus one instance per constant
enumFullYes, enforced by the language itselfYes - constant-specific method bodiesYes, with dedicated case syntaxMinimal - the language handles what the typesafe pattern did manually

The middle row is worth knowing by name even if you never write it: the "typesafe enum pattern" was the standard workaround before Java 5, and it is exactly what enum automates. Anyone who has seen a private-constructor class with a handful of public static final instances of itself has seen what enum replaced.

Best Practices

Reach for an enum the moment a value is "one of a known, fixed set" - not just when it is purely decorative. Order statuses, payment methods, delivery zones, subscription tiers, configuration modes: if the full list of valid values is known at compile time and changing it means a code change anyway, an enum communicates and enforces that far better than int or String constants ever can.

Keep enum constants immutable. Fields on an enum constant should be final, set once in the constructor, with no setters. Every constant is a singleton shared across the entire JVM - a mutable field on TrainClass.SLEEPER would be one shared, mutable piece of state that every caller everywhere sees and can corrupt, which is precisely the kind of bug enums are supposed to make impossible.

Use constant-specific method bodies (or an implemented interface) instead of a switch scattered across the codebase, when behavior genuinely differs per constant. RefundPolicy.calculateRefund() above needed four different formulas - writing that as a switch statement on policy with one case per constant somewhere else would work, but every time a fifth policy is added, that switch (and every other switch like it elsewhere in the codebase) needs updating. Putting the behavior on the constant means there is exactly one place to add the fifth policy's logic.

Reach for EnumMap and EnumSet when the key or element type is an enum. They are not just a minor optimization over HashMap and HashSet - they iterate in declaration order (which is often the order you want to display things in anyway) and communicate, to anyone reading the code, that the key space is closed and known.

Never persist or transmit ordinal() as the representation of an enum constant. ordinal() is the constant's position in the source file - insert a new constant in the middle, or reorder two constants, and every previously stored ordinal() value now means something different. If an external representation (a database column, a JSON field, an API contract) is needed, give the enum a dedicated field for it - a code or similar - set explicitly in the constructor, independent of declaration order.

Common Mistakes

Mistake 1 - Storing ordinal() as a Persisted Value

enum TicketStatus { BOOKED, WAITLISTED, CONFIRMED, CANCELLED, COMPLETED } // WRONG - storing ordinal() in a database column // At the time this was written: BOOKED=0, WAITLISTED=1, CONFIRMED=2, ... int statusCode = TicketStatus.CONFIRMED.ordinal(); // stores 2 // Months later, someone inserts a new status at the TOP of the enum: // enum TicketStatus { ON_HOLD, BOOKED, WAITLISTED, CONFIRMED, CANCELLED, COMPLETED } // Now CONFIRMED.ordinal() == 3 - every row that stored "2" now means WAITLISTED // CORRECT - give the enum its own explicit, stable code, independent // of declaration order or position enum TicketStatusFixed { BOOKED(1), WAITLISTED(2), CONFIRMED(3), CANCELLED(4), COMPLETED(5); private final int code; TicketStatusFixed(int code) { this.code = code; } public int getCode() { return code; } } // Inserting a new constant anywhere does not change any existing code

Mistake 2 - Declaring a Public or Protected Constructor

// WRONG - does not compile enum TrainClass { SLEEPER(1.0), AC_3_TIER(2.5); private final double fareMultiplier; public TrainClass(double fareMultiplier) { // COMPILE ERROR this.fareMultiplier = fareMultiplier; // "Modifier 'public' not allowed here" - enum constructors // can only be private (or have no modifier, which means // the same thing for an enum constructor) } } // CORRECT - omit the modifier, or write 'private' explicitly - // both mean the same thing for an enum constructor enum TrainClassFixed { SLEEPER(1.0), AC_3_TIER(2.5); private final double fareMultiplier; TrainClassFixed(double fareMultiplier) { this.fareMultiplier = fareMultiplier; } }

Mistake 3 - Not Handling IllegalArgumentException From valueOf() on External Input

enum PaymentMethod { UPI, CARD, NET_BANKING, WALLET } // WRONG - 'method' comes from an external source (an HTTP request // parameter, a database value) that might not match any constant // EXACTLY - valueOf() throws, uncaught, if it does not String requestedMethod = "Upi"; // wrong case - does not match "UPI" PaymentMethod method = PaymentMethod.valueOf(requestedMethod); // IllegalArgumentException // CORRECT - validate or normalize before calling valueOf(), and // handle the exception explicitly for genuinely unknown input String requestedMethodFixed = "UPI"; PaymentMethod parsed; try { parsed = PaymentMethod.valueOf(requestedMethodFixed.toUpperCase()); } catch (IllegalArgumentException e) { parsed = null; // or throw a domain-specific exception with a clearer message }

Mistake 4 - Adding a Mutable Field to an Enum Constant

// WRONG - 'lastUsedAt' is a non-final field on a SINGLETON. Every // call to setLastUsedAt() mutates the ONE shared PaymentMethod.CARD // instance that the entire application uses - this is global, // shared, mutable state with no synchronization enum PaymentMethod { UPI, CARD, NET_BANKING, WALLET; private long lastUsedAt; // not final - MUTABLE shared state void setLastUsedAt(long timestamp) { this.lastUsedAt = timestamp; // every thread sees and can overwrite this } } // CORRECT - keep enum constants immutable. Track per-use data // (like "last used at") in a separate structure keyed BY the enum, // such as an EnumMap, owned by whatever component needs that history enum PaymentMethodFixed { UPI, CARD, NET_BANKING, WALLET } class PaymentMethodUsageTracker { private final java.util.Map<PaymentMethodFixed, Long> lastUsedAt = new java.util.EnumMap<>(PaymentMethodFixed.class); void recordUse(PaymentMethodFixed method, long timestamp) { lastUsedAt.put(method, timestamp); } }

Interview Questions

Q1. What is an enum in Java, and what does it extend internally?

An enum is a special class declaration where each named constant in its body becomes a public static final instance of the enum type, created once when the class is loaded. Every enum implicitly extends java.lang.Enum<E>, which provides name(), ordinal(), compareTo(), equals(), hashCode(), and toString() for free. Because extending java.lang.Enum uses Java's single class-inheritance slot, an enum cannot extend any other class - but it can implement any number of interfaces, since interface implementation is unrelated to that slot.

Q2. Why can't an enum extend another class, and can it implement interfaces?

An enum cannot extend another class because it already implicitly extends java.lang.Enum<E>, and Java classes support only single inheritance - that slot is used. It can implement any number of interfaces, exactly like any other class, and this is the basis for constant-specific method bodies: an enum can implement an interface's abstract method, and each constant can provide its own override of that method, with each constant effectively becoming its own small anonymous subclass for that purpose.

Q3. What is a constant-specific method body, and when would you use one over a switch statement?

A constant-specific method body is when an individual enum constant provides its own implementation of a method - either an abstract method declared by the enum itself, or a method from an interface the enum implements - rather than relying on one shared implementation. RefundPolicy.MORE_THAN_48_HOURS and RefundPolicy.AFTER_DEPARTURE each implement calculateRefund differently, with no branching logic anywhere. This is preferable to a switch on the enum when the behavior is intrinsic to what each constant represents - adding a new constant later means adding its method body in one place, rather than finding and updating every switch statement elsewhere in the codebase that branches on this enum.

Q4. Why are enum constructors always private, and what would happen if that were not enforced?

An enum's entire value lies in representing a closed, fixed set of instances - exactly the constants declared in its body, and nothing else, for the lifetime of the JVM. If an enum constructor could be public or protected, external code could call new TrainClass(...) and create additional instances that are not any of the declared constants - instances that would still be of type TrainClass, pass instanceof checks, but not equal any of TrainClass.values(). This would break switch exhaustiveness, EnumMap/EnumSet assumptions, and the entire "this is one of a known set" guarantee. The compiler enforces private (or no modifier, which means the same thing for an enum constructor) specifically to prevent this.

Q5. What is the difference between ordinal() and a custom "code" field, and why does it matter for persistence?

ordinal() is the constant's zero-based position in its declaration in the source file - it is determined entirely by where the constant happens to be written relative to the others, and it changes if constants are reordered or a new one is inserted earlier in the list. A custom field - set explicitly in the constructor, with a name like code - is whatever value you assign it, independent of declaration order, and remains stable even if the enum's declaration is reorganized. For any value that will be stored externally (a database column, a serialized format, an API contract) and needs to remain meaningful across code changes, a custom field is required; ordinal() is appropriate only for in-memory uses like EnumMap/EnumSet indexing, where the JVM recomputes it fresh from the current source every time the class is loaded.

Q6. How does switch work with enums internally, and why is it efficient?

When switch is used on an enum, the compiler does not generate a chain of .equals() or identity comparisons against each case label. Instead, across javac versions, it has generated a small synthetic lookup table that maps each constant's ordinal() to the index of the matching case - so the actual dispatch at runtime is a switch on a small integer, one of the fastest operations the JVM has. This is also why case labels for an enum switch are written as bare constant names (case LOCAL ->, not case DeliveryZone.LOCAL ->) - the compiler already knows the type being switched on is that specific enum, and resolves the bare names against it.

FAQs

Can an enum have a main method?

Yes - an enum is a class, and any class can declare public static void main(String[] args). This is occasionally used for small, self-contained demonstrations of an enum's behavior, exactly as shown in the demos throughout this article, though most projects keep main methods in dedicated classes rather than on the enums themselves.

Can you compare enum constants with == ?

Yes, and it is the normal, recommended way to compare them. Because every constant is a singleton - there is exactly one TicketStatus.CONFIRMED object in the entire JVM - status == TicketStatus.CONFIRMED and status.equals(TicketStatus.CONFIRMED) always produce the same result for enum constants. == is commonly preferred here because it also gives a compile error if the two sides are of unrelated enum types, whereas .equals() would just return false.

What does values() return, and is it safe to modify the returned array?

values() returns an array containing every constant of the enum, in declaration order. Each call to values() returns a freshly cloned array - the compiler-generated implementation calls .clone() on an internal array specifically so that modifying the array you receive (reordering it, setting an element to null) has no effect on the enum itself or on any other caller's array. It is safe to modify the returned array, but doing so has no lasting effect, so there is rarely a reason to.

Can an enum implement multiple interfaces?

Yes. An enum can implement any number of interfaces, exactly like a regular class - declaring enum PaymentMethod implements FeeCalculator, Describable with a body that satisfies both is valid. The "extends java.lang.Enum" restriction only consumes the single class-inheritance slot; interface implementation is unaffected and unlimited.

Is an enum thread-safe by default?

The constants themselves - as objects - are effectively immutable singletons if (and only if) every field on them is final and set in the constructor, which makes reading them from multiple threads inherently safe with no synchronization needed. If an enum constant has a mutable, non-final field (Common Mistake 4 above), that field is shared, mutable state across every thread in the application, and is not thread-safe by default - the enum itself provides no special protection for fields you choose to make mutable.

Can enum constants have different numbers of constructor arguments, or even no constructor call at all?

No - if an enum declares a constructor, every constant must supply arguments matching one of the enum's constructor signatures (if there is only one constructor, every constant must match it). If some constants need no extra data, the common approach is either a single constructor where some constants pass default values, or omitting fields and a constructor entirely if no constant needs per-constant data - shape 1 from the overview. A mix of "some constants call a constructor, others call none" is not possible if a constructor is declared, because a constant with no parentheses after its name calls the no-argument constructor, which must then exist.

Summary

An enum is a class - extending java.lang.Enum<E>, with one public static final singleton instance per declared constant, a constructor that runs exactly once per constant at class-loading time, and a constructor that can never be anything but private. Everything else about enums follows from that: values(), ordinal(), name(), and valueOf() come from java.lang.Enum for free; switch works because the constants are a known, closed set the compiler can reason about completely; and EnumMap/EnumSet exist because "key space is a small, known set of objects" is exactly what an enum guarantees.

The two ideas worth carrying forward past the basics: constant-specific method bodies turn "different behavior per constant" into "one place to add the next constant's behavior," replacing scattered switch statements; and ordinal() is a source-position number, not an identity - anything that needs to outlive the source file's current ordering needs its own explicit field.

The next time a piece of code reaches for int or String constants to represent "one of a few known options," the question worth asking is the one this article opened with: does the compiler currently stop the wrong value from being passed here - and if the answer is no, an enum is very likely the fix.

What to Read Next

Practice Programs

Put this topic into practice — each program has full code, output and a step-by-step explanation you can run online.

Browse all Java programs →