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.

1// File: EnumBasicsDemo.java 2 3public class EnumBasicsDemo { 4 5 enum DeliveryZone { 6 LOCAL, REGIONAL, NATIONAL, INTERNATIONAL 7 } 8 9 static String estimateDays(DeliveryZone zone) { 10 return switch (zone) { 11 case LOCAL -> "1 day"; 12 case REGIONAL -> "2-3 days"; 13 case NATIONAL -> "4-6 days"; 14 case INTERNATIONAL -> "10-15 days"; 15 }; 16 } 17 18 public static void main(String[] args) { 19 20 System.out.println("=== All constants via values() ==="); 21 for (DeliveryZone zone : DeliveryZone.values()) { 22 System.out.printf(" %-13s ordinal=%d delivery: %s%n", 23 zone.name(), zone.ordinal(), estimateDays(zone)); 24 } 25 26 System.out.println(); 27 28 System.out.println("=== valueOf() - converting a String to an enum constant ==="); 29 DeliveryZone zone = DeliveryZone.valueOf("REGIONAL"); 30 System.out.println("Parsed: " + zone); 31 32 System.out.println(); 33 34 System.out.println("=== compareTo() - ordering follows declaration order ==="); 35 System.out.println("LOCAL.compareTo(NATIONAL) = " + DeliveryZone.LOCAL.compareTo(DeliveryZone.NATIONAL)); 36 System.out.println("NATIONAL.compareTo(LOCAL) = " + DeliveryZone.NATIONAL.compareTo(DeliveryZone.LOCAL)); 37 38 System.out.println(); 39 40 System.out.println("=== valueOf() with an unknown name throws IllegalArgumentException ==="); 41 try { 42 DeliveryZone.valueOf("GLOBAL"); 43 } catch (IllegalArgumentException e) { 44 System.out.println("Caught: " + e.getMessage()); 45 } 46 } 47}
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.

1// File: EnumFieldsDemo.java 2 3public class EnumFieldsDemo { 4 5 enum TrainClass { 6 SLEEPER(1.0, 72), 7 AC_3_TIER(2.5, 64), 8 AC_2_TIER(3.8, 48), 9 AC_1_TIER(6.0, 24); 10 11 private final double fareMultiplier; 12 private final int berthsPerCoach; 13 14 // Runs ONCE per constant, at class-loading time, in the order 15 // the constants are declared above. Implicitly private - this 16 // cannot be called from anywhere else. 17 TrainClass(double fareMultiplier, int berthsPerCoach) { 18 this.fareMultiplier = fareMultiplier; 19 this.berthsPerCoach = berthsPerCoach; 20 } 21 22 double calculateFare(double baseFare) { 23 return baseFare * fareMultiplier; 24 } 25 26 int getBerthsPerCoach() { 27 return berthsPerCoach; 28 } 29 } 30 31 public static void main(String[] args) { 32 double baseFare = 450.0; 33 34 System.out.println("=== Fare for each class on a base fare of Rs.450 ==="); 35 for (TrainClass trainClass : TrainClass.values()) { 36 System.out.printf(" %-10s Rs.%-8.2f %d berths/coach%n", 37 trainClass, trainClass.calculateFare(baseFare), trainClass.getBerthsPerCoach()); 38 } 39 } 40}
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.

1// File: EnumInterfaceDemo.java 2 3public class EnumInterfaceDemo { 4 5 interface FeeCalculator { 6 double calculateFee(double amount); 7 } 8 9 enum PaymentMethod implements FeeCalculator { 10 11 UPI { 12 @Override 13 public double calculateFee(double amount) { 14 return 0.0; // UPI transactions are fee-free 15 } 16 }, 17 CARD { 18 @Override 19 public double calculateFee(double amount) { 20 return amount * 0.02; // 2% processing fee 21 } 22 }, 23 NET_BANKING { 24 @Override 25 public double calculateFee(double amount) { 26 return 10.0; // flat fee regardless of amount 27 } 28 }, 29 WALLET { 30 @Override 31 public double calculateFee(double amount) { 32 return amount * 0.01; // 1% processing fee 33 } 34 }; 35 36 // Every constant above supplies its OWN body for calculateFee - 37 // PaymentMethod never needs a single shared implementation, and 38 // no branching on "which constant is this" appears anywhere 39 } 40 41 public static void main(String[] args) { 42 double amount = 2000.0; 43 44 System.out.println("=== Processing fee for Rs.2000 by payment method ==="); 45 for (PaymentMethod method : PaymentMethod.values()) { 46 System.out.printf(" %-12s fee = Rs.%.2f%n", method, method.calculateFee(amount)); 47 } 48 } 49}
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.

Real-World Example - IRCTC Ticket Cancellation

An Indian Railways-style booking system needs two things working together: a status for every ticket that can only ever be one of a known set of values, and a refund calculation that depends on how close to departure a cancellation happens - with genuinely different logic per time window, not just a different number. The status set becomes a simple enum; the refund logic becomes an enum with constant-specific bodies; and EnumMap plus EnumSet enforce which status transitions are even allowed in the first place.

1// File: TicketStatus.java 2 3public enum TicketStatus { 4 BOOKED, WAITLISTED, CONFIRMED, CANCELLED, COMPLETED 5}
1// File: RefundPolicy.java 2 3public enum RefundPolicy { 4 5 MORE_THAN_48_HOURS { 6 @Override 7 public double calculateRefund(double fare) { 8 return fare * 0.95; // 5% cancellation charge 9 } 10 }, 11 BETWEEN_24_AND_48_HOURS { 12 @Override 13 public double calculateRefund(double fare) { 14 return fare * 0.75; // 25% cancellation charge 15 } 16 }, 17 LESS_THAN_24_HOURS { 18 @Override 19 public double calculateRefund(double fare) { 20 return fare * 0.50; // 50% cancellation charge 21 } 22 }, 23 AFTER_DEPARTURE { 24 @Override 25 public double calculateRefund(double fare) { 26 return 0.0; // no refund once the train has departed 27 } 28 }; 29 30 public abstract double calculateRefund(double fare); 31 32 // Static factory - maps a raw input (hours before departure) onto 33 // the correct constant. Keeping this INSIDE the enum keeps the 34 // mapping logic next to the constants it maps onto. 35 public static RefundPolicy fromHoursBeforeDeparture(long hours) { 36 if (hours > 48) return MORE_THAN_48_HOURS; 37 if (hours >= 24) return BETWEEN_24_AND_48_HOURS; 38 if (hours > 0) return LESS_THAN_24_HOURS; 39 return AFTER_DEPARTURE; 40 } 41}
1// File: TicketCancellationService.java 2 3import java.util.EnumMap; 4import java.util.EnumSet; 5import java.util.Map; 6import java.util.Set; 7 8public class TicketCancellationService { 9 10 // EnumMap - the set of statuses a ticket can move TO from a given 11 // status. Iterates in TicketStatus declaration order and stores 12 // entries in an array indexed by ordinal() internally. 13 private static final Map<TicketStatus, Set<TicketStatus>> VALID_TRANSITIONS = 14 new EnumMap<>(TicketStatus.class); 15 16 static { 17 VALID_TRANSITIONS.put(TicketStatus.BOOKED, EnumSet.of(TicketStatus.CONFIRMED, TicketStatus.CANCELLED)); 18 VALID_TRANSITIONS.put(TicketStatus.WAITLISTED, EnumSet.of(TicketStatus.CONFIRMED, TicketStatus.CANCELLED)); 19 VALID_TRANSITIONS.put(TicketStatus.CONFIRMED, EnumSet.of(TicketStatus.CANCELLED, TicketStatus.COMPLETED)); 20 VALID_TRANSITIONS.put(TicketStatus.CANCELLED, EnumSet.noneOf(TicketStatus.class)); 21 VALID_TRANSITIONS.put(TicketStatus.COMPLETED, EnumSet.noneOf(TicketStatus.class)); 22 } 23 24 public boolean canTransition(TicketStatus from, TicketStatus to) { 25 return VALID_TRANSITIONS.get(from).contains(to); 26 } 27 28 public double cancelTicket(TicketStatus currentStatus, double fare, long hoursBeforeDeparture) { 29 if (!canTransition(currentStatus, TicketStatus.CANCELLED)) { 30 throw new IllegalStateException("Cannot cancel a ticket with status: " + currentStatus); 31 } 32 RefundPolicy policy = RefundPolicy.fromHoursBeforeDeparture(hoursBeforeDeparture); 33 double refund = policy.calculateRefund(fare); 34 System.out.printf(" Status %s -> CANCELLED | Policy: %s | Refund: Rs.%.2f%n", 35 currentStatus, policy, refund); 36 return refund; 37 } 38 39 public static void main(String[] args) { 40 TicketCancellationService service = new TicketCancellationService(); 41 42 System.out.println("=== Cancelling a CONFIRMED ticket, 60 hours before departure ==="); 43 service.cancelTicket(TicketStatus.CONFIRMED, 1200.0, 60); 44 45 System.out.println(); 46 System.out.println("=== Cancelling a BOOKED ticket, 10 hours before departure ==="); 47 service.cancelTicket(TicketStatus.BOOKED, 850.0, 10); 48 49 System.out.println(); 50 System.out.println("=== Cancelling a WAITLISTED ticket, 30 hours before departure ==="); 51 service.cancelTicket(TicketStatus.WAITLISTED, 500.0, 30); 52 53 System.out.println(); 54 System.out.println("=== Attempting to cancel an already CANCELLED ticket ==="); 55 try { 56 service.cancelTicket(TicketStatus.CANCELLED, 1200.0, 60); 57 } catch (IllegalStateException e) { 58 System.out.println(" Rejected: " + e.getMessage()); 59 } 60 61 System.out.println(); 62 System.out.println("=== Checking valid transitions directly ==="); 63 System.out.println("BOOKED -> CONFIRMED valid? " + service.canTransition(TicketStatus.BOOKED, TicketStatus.CONFIRMED)); 64 System.out.println("CANCELLED -> CONFIRMED valid? " + service.canTransition(TicketStatus.CANCELLED, TicketStatus.CONFIRMED)); 65 } 66}
Output:
=== Cancelling a CONFIRMED ticket, 60 hours before departure ===
  Status CONFIRMED -> CANCELLED | Policy: MORE_THAN_48_HOURS | Refund: Rs.1140.00

=== Cancelling a BOOKED ticket, 10 hours before departure ===
  Status BOOKED -> CANCELLED | Policy: LESS_THAN_24_HOURS | Refund: Rs.425.00

=== Cancelling a WAITLISTED ticket, 30 hours before departure ===
  Status WAITLISTED -> CANCELLED | Policy: BETWEEN_24_AND_48_HOURS | Refund: Rs.375.00

=== Attempting to cancel an already CANCELLED ticket ===
  Rejected: Cannot cancel a ticket with status: CANCELLED

=== Checking valid transitions directly ===
BOOKED -> CONFIRMED valid?    true
CANCELLED -> CONFIRMED valid? false

TicketStatus is shape 1 - a closed set of values with no extra data. RefundPolicy is shapes 3 and 4 together - each constant calculates its own refund, with no if chain anywhere checking "which policy is this." And VALID_TRANSITIONS shows EnumMap and EnumSet doing exactly what they are for: a compact, declaration-ordered map from one enum to a set of another (here, the same) enum - rejecting CANCELLED -> CANCELLED and any other transition nobody declared as valid, with no special-case code in canTransition at all.

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

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

Mistake 2 - Declaring a Public or Protected Constructor

1// WRONG - does not compile 2enum TrainClass { 3 SLEEPER(1.0), AC_3_TIER(2.5); 4 5 private final double fareMultiplier; 6 7 public TrainClass(double fareMultiplier) { // COMPILE ERROR 8 this.fareMultiplier = fareMultiplier; 9 // "Modifier 'public' not allowed here" - enum constructors 10 // can only be private (or have no modifier, which means 11 // the same thing for an enum constructor) 12 } 13} 14 15// CORRECT - omit the modifier, or write 'private' explicitly - 16// both mean the same thing for an enum constructor 17enum TrainClassFixed { 18 SLEEPER(1.0), AC_3_TIER(2.5); 19 20 private final double fareMultiplier; 21 22 TrainClassFixed(double fareMultiplier) { 23 this.fareMultiplier = fareMultiplier; 24 } 25}

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

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

Mistake 4 - Adding a Mutable Field to an Enum Constant

1// WRONG - 'lastUsedAt' is a non-final field on a SINGLETON. Every 2// call to setLastUsedAt() mutates the ONE shared PaymentMethod.CARD 3// instance that the entire application uses - this is global, 4// shared, mutable state with no synchronization 5enum PaymentMethod { 6 UPI, CARD, NET_BANKING, WALLET; 7 8 private long lastUsedAt; // not final - MUTABLE shared state 9 10 void setLastUsedAt(long timestamp) { 11 this.lastUsedAt = timestamp; // every thread sees and can overwrite this 12 } 13} 14 15// CORRECT - keep enum constants immutable. Track per-use data 16// (like "last used at") in a separate structure keyed BY the enum, 17// such as an EnumMap, owned by whatever component needs that history 18enum PaymentMethodFixed { 19 UPI, CARD, NET_BANKING, WALLET 20} 21 22class PaymentMethodUsageTracker { 23 private final java.util.Map<PaymentMethodFixed, Long> lastUsedAt = 24 new java.util.EnumMap<>(PaymentMethodFixed.class); 25 26 void recordUse(PaymentMethodFixed method, long timestamp) { 27 lastUsedAt.put(method, timestamp); 28 } 29}

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