What Is an Enum?
An enum is a type with a fixed set of named constants. It replaces "magic numbers" and loose strings with something the compiler can verify.
The Problem Enums Solve
Imagine you're writing code for the days of the week. Without enums, you might use integers or strings:
// Using integers — what does "3" mean? int today = 3; if (today == 8) { // ← compiles fine. "8" is not a valid day! System.out.println("Holiday!"); } // Using Strings — typos go undetected String day = "TUSDAY"; // ← typo! compiles fine. if (day.equals("TUESDAY")) { ... } // never true
Both compile without error. The mistakes only surface at runtime — or not at all, causing silent incorrect behaviour.
public enum Day { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY } Day today = Day.MONDAY; // today = Day.TUSDAY; ← ❌ compile error — doesn't exist // today = 8; ← ❌ compile error — wrong type if (today == Day.MONDAY) { System.out.println("Start of the week!"); }
enum keyword, Java enums are full classes under the hood. They can have fields, methods, and constructors. They also implicitly extend java.lang.Enum and cannot be subclassed.
Basic Syntax
enum keyword. Constants are UPPER_SNAKE_CASE by convention.EnumName.CONSTANT — the enum type plus the constant name.== to compare enum values — they're singletons, so reference equality is safe.Enum Methods & Fields
Enums aren't just named constants. Each constant is an instance of the enum class, and the class can carry data and behaviour.
Enums Can Have Fields and Methods
A common real-world use: an enum for planets, each carrying its mass and radius, with a method to compute surface gravity.
public enum Planet { // Each constant calls the constructor with (mass, radius) MERCURY(3.303e+23, 2.4397e6), VENUS (4.869e+24, 6.0518e6), EARTH (5.976e+24, 6.37814e6), MARS (6.421e+23, 3.3972e6); private final double mass; // in kilograms private final double radius; // in metres // Private constructor (always private in enums) Planet(double mass, double radius) { this.mass = mass; this.radius = radius; } static final double G = 6.67300E-11; public double surfaceGravity() { return G * mass / (radius * radius); } public double surfaceWeight(double otherMass) { return otherMass * surfaceGravity(); } } // Usage: double earthWeight = 75.0; double mass = earthWeight / Planet.EARTH.surfaceGravity(); for (Planet p : Planet.values()) { System.out.printf("Weight on %s: %.2f%n", p, p.surfaceWeight(mass)); }
new Planet(...) — the constants are created automatically when the class loads. The constructor syntax appears inside the constant list, not in your calling code. If you omit private, Java treats it as private anyway.
Abstract Methods on Enums
Each constant can provide its own implementation of an abstract method — essentially giving every constant different behaviour:
public enum Operation { PLUS("+") { public double apply(double x, double y) { return x + y; } }, MINUS("-") { public double apply(double x, double y) { return x - y; } }, TIMES("*") { public double apply(double x, double y) { return x * y; } }; private final String symbol; Operation(String symbol) { this.symbol = symbol; } public abstract double apply(double x, double y); @Override public String toString() { return symbol; } } // Usage: double x = 4, y = 2; for (Operation op : Operation.values()) { System.out.printf("%.0f %s %.0f = %.0f%n", x, op, y, op.apply(x, y)); } // 4 + 2 = 6 // 4 - 2 = 2 // 4 * 2 = 8
Enums in switch
Enums and switch expressions are a natural pair. The compiler can warn you if you miss a constant — making your code exhaustive and safe.
Classic switch Statement
public enum Season { SPRING, SUMMER, AUTUMN, WINTER } Season s = Season.SUMMER; switch (s) { case SPRING: System.out.println("Flowers bloom"); break; case SUMMER: System.out.println("It's hot!"); break; case AUTUMN: System.out.println("Leaves fall"); break; case WINTER: System.out.println("Snow falls"); break; } // It's hot!
Modern switch Expression (Java 14+)
The newer arrow-syntax switch is cleaner — no break, no fall-through, and it can return a value:
Season season = Season.AUTUMN; String description = switch (season) { case SPRING -> "Flowers bloom"; case SUMMER -> "It's hot!"; case AUTUMN -> "Leaves fall"; case WINTER -> "Snow falls"; }; // No default needed — compiler knows all Season values are covered System.out.println(description); // Leaves fall
default. Better still — if you add a new constant to the enum later, the compiler will tell you every switch that doesn't handle it yet.
Real-World Example: HTTP Status
public enum HttpStatus { OK(200), NOT_FOUND(404), SERVER_ERROR(500), UNAUTHORIZED(401); private final int code; HttpStatus(int code) { this.code = code; } public int getCode() { return code; } } public static String handle(HttpStatus status) { return switch (status) { case OK -> "Request successful"; case NOT_FOUND -> "Resource not found"; case UNAUTHORIZED -> "Please log in"; case SERVER_ERROR -> "Something went wrong on our end"; }; } System.out.println(handle(HttpStatus.NOT_FOUND)); // Resource not found
default case?Enum with Constructor
Adding a constructor to an enum lets each constant carry associated data — turning a simple label into a rich type.
The Pattern
private final — enum state shouldn't change.CONSTANT(arg1, arg2).public enum CoffeeSize { SMALL ("Small", 8, 2.50), MEDIUM("Medium", 12, 3.25), LARGE ("Large", 16, 4.00), VENTI ("Venti", 20, 4.75); private final String label; private final int ounces; private final double price; CoffeeSize(String label, int ounces, double price) { this.label = label; this.ounces = ounces; this.price = price; } public String getLabel() { return label; } public int getOunces() { return ounces; } public double getPrice() { return price; } } // Usage: CoffeeSize order = CoffeeSize.LARGE; System.out.printf("%s (%d oz) — $%.2f%n", order.getLabel(), order.getOunces(), order.getPrice()); // Large (16 oz) — $4.00 // Loop all sizes: for (CoffeeSize size : CoffeeSize.values()) { System.out.printf("%-8s %2d oz $%.2f%n", size.getLabel(), size.getOunces(), size.getPrice()); }
CoffeeSize.LARGE in the JVM — forever. That's why == comparison is safe for enums.
Enum Built-in Methods
Every enum automatically inherits a set of useful methods from java.lang.Enum. You don't write them — they're always there.
name()
Returns the constant's name as declared — exact string, e.g. "MONDAY"
ordinal()
Returns the zero-based position in the declaration order. MONDAY=0, TUESDAY=1, etc.
values()
Static method — returns an array of all constants in declaration order.
valueOf(String)
Static method — returns the constant with that name. Throws if not found.
toString()
By default returns name(). Can be overridden for custom output.
compareTo()
Compares by ordinal. Lets you sort enums naturally.
public enum Direction { NORTH, EAST, SOUTH, WEST } Direction d = Direction.EAST; System.out.println(d.name()); // EAST System.out.println(d.ordinal()); // 1 (NORTH=0, EAST=1, SOUTH=2, WEST=3) System.out.println(d.toString()); // EAST // All values for (Direction dir : Direction.values()) { System.out.println(dir.ordinal() + ": " + dir); } // From a String Direction parsed = Direction.valueOf("NORTH"); // Direction.NORTH // Direction.valueOf("NORTHWEST"); ← ❌ IllegalArgumentException // EnumSet and EnumMap — optimised collections for enums import java.util.*; EnumSet<Direction> cardinals = EnumSet.of(Direction.NORTH, Direction.SOUTH); System.out.println(cardinals); // [NORTH, SOUTH]
ordinal() values in databases or files. If you reorder or insert constants, the ordinals shift and your stored data becomes wrong. If you need a stable integer ID, give the enum a field — like the HttpStatus(code) example earlier.
What Is a Record?
Records, introduced in Java 16, are a concise way to create immutable data carrier classes — without the boilerplate of a normal class.
The Boilerplate Problem
A standard Java class for holding a point (x, y) requires a lot of ceremony:
public class Point { private final int x; private final int y; public Point(int x, int y) { this.x = x; this.y = y; } public int x() { return x; } public int y() { return y; } @Override public boolean equals(Object o) { /* ... 10 lines */ } @Override public int hashCode() { /* ... */ } @Override public String toString() { return "Point[" + x + ", " + y + "]"; } }
public record Point(int x, int y) {}
That single line automatically gives you: a constructor, getters (x() and y()), equals(), hashCode(), and toString().
Record Anatomy
What Gets Generated Automatically
| Generated automatically | Equivalent manual code |
|---|---|
| Canonical constructor | public Person(String name, int age) |
| Accessor methods | public String name(), public int age() |
equals() | compares all components by value |
hashCode() | based on all components |
toString() | Person[name=Alice, age=30] |
private final. There are no setters — once created, a record's state cannot change. If you need a modified copy, create a new record instance. This immutability makes records safe to share across threads and store in collections.
Records in Practice
Records shine as data carriers — method return types, stream elements, map entries, and API response objects.
public record Person(String name, int age) {} public record Address(String street, String city) {} public class Main { public static void main(String[] args) { // Create — canonical constructor Person alice = new Person("Alice", 30); Person bob = new Person("Bob", 25); // Access via generated accessor methods (not getX — just x()) System.out.println(alice.name()); // Alice System.out.println(alice.age()); // 30 // toString — already useful System.out.println(alice); // Person[name=Alice, age=30] // equals — value-based Person aliceCopy = new Person("Alice", 30); System.out.println(alice.equals(aliceCopy)); // true System.out.println(alice == aliceCopy); // false (different objects) // Records work great in collections and streams List<Person> people = List.of(alice, bob, new Person("Carol", 35)); people.stream() .filter(p -> p.age() >= 30) .map(Person::name) .forEach(System.out::println); // Alice // Carol } }
Records as Method Return Types
Instead of returning parallel values awkwardly, wrap them in a record:
public record Stats(double min, double max, double average) {} public static Stats compute(List<Integer> nums) { return new Stats( nums.stream().mapToInt(Integer::intValue).min().orElse(0), nums.stream().mapToInt(Integer::intValue).max().orElse(0), nums.stream().mapToInt(Integer::intValue).average().orElse(0) ); } Stats s = compute(List.of(4, 8, 15, 16, 23)); System.out.printf("min=%.0f max=%.0f avg=%.1f%n", s.min(), s.max(), s.average()); // min=4 max=23 avg=13.2
Customising Records
Records aren't completely rigid. You can add validation, custom constructors, additional methods, and implement interfaces.
Compact Constructors — Built-in Validation
A compact constructor runs before the canonical one. Use it for validation without repeating the assignment code:
public record Range(int min, int max) { // Compact constructor — no parameter list, fields auto-assigned after Range { if (min > max) { throw new IllegalArgumentException( "min (" + min + ") must be <= max (" + max + ")"); } } } var r1 = new Range(1, 10); // ✓ Range[min=1, max=10] var r2 = new Range(10, 1); // ❌ IllegalArgumentException
Adding Methods to a Record
public record Money(double amount, String currency) { // Compact constructor — normalise currency code Money { if (amount < 0) throw new IllegalArgumentException("Amount cannot be negative"); currency = currency.toUpperCase(); // normalise before assignment } // Additional instance methods public Money add(Money other) { if (!currency.equals(other.currency)) throw new IllegalArgumentException("Currency mismatch"); return new Money(amount + other.amount, currency); } public boolean isZero() { return amount == 0; } @Override public String toString() { return String.format("%.2f %s", amount, currency); } } Money a = new Money(10.00, "usd"); // currency → "USD" Money b = new Money(5.50, "USD"); System.out.println(a.add(b)); // 15.50 USD
Records Can Implement Interfaces
public interface Shape { double area(); } public record Circle(double radius) implements Shape { public double area() { return Math.PI * radius * radius; } } public record Rectangle(double width, double height) implements Shape { public double area() { return width * height; } } List<Shape> shapes = List.of(new Circle(5), new Rectangle(4, 6)); shapes.forEach(s -> System.out.printf("%.2f%n", s.area())); // 78.54 // 24.00
• Extend another class (they implicitly extend
java.lang.Record)• Have non-static mutable fields declared outside the component list
• Be subclassed (they're implicitly
final)
Enum vs Record vs Class
All three are ways to define a type. Picking the right one is a design decision — here's the decision framework.
| Enum | Record | Class | |
|---|---|---|---|
| Purpose | Fixed set of named constants | Immutable data carrier | Mutable or complex behaviour |
| Instances | Compiler-defined, finite | Created with new, unlimited |
Created with new, unlimited |
| Mutable? | No | No (fields are final) | Yes (by default) |
| equals() | Identity (== safe) |
Value-based (auto-generated) | Identity unless overridden |
| Inheritance | Cannot extend or be extended | Implements interfaces; cannot be subclassed | Full inheritance |
| Boilerplate | Low for constants | Minimal — auto-generates constructor, accessors, equals, hashCode, toString | High — write everything manually |
| Use when | The type has a fixed, known set of values | You need a data holder — DTO, return type, stream element | You need mutability, inheritance, or complex behaviour |
Decision Guide
Ask: "Is the set of possible values fixed at compile time?"
Yes → Enum (days, seasons, HTTP verbs, directions).
No → "Is this just data I need to pass around, with no mutation?"
Yes → Record (a user from a DB query, a coordinate, a stats result).
No → Class (a bank account that changes balance, a connection that manages state).
// Enum — fixed set of HTTP methods enum HttpMethod { GET, POST, PUT, DELETE, PATCH } // Record — immutable HTTP response data record HttpResponse(HttpStatus status, String body) {} // Class — HTTP connection with mutable state class HttpClient { private String baseUrl; private int timeout; public void setTimeout(int t) { this.timeout = t; } // mutable public HttpResponse get(String path) { /* ... */ return null; } }
Quiz & Coding Challenge
Test your understanding of enums and records.
Final Quiz
record Point(int x, int y) {} automatically provide?Coding Challenges
TrafficLight enum with RED, AMBER, GREENnext() method that returns the next light in the cyclepublic enum TrafficLight { RED(30), AMBER(5), GREEN(25); private final int durationSeconds; TrafficLight(int d) { this.durationSeconds = d; } public int getDuration() { return durationSeconds; } public TrafficLight next() { TrafficLight[] vals = TrafficLight.values(); return vals[(this.ordinal() + 1) % vals.length]; } } // Main TrafficLight light = TrafficLight.RED; for (int i = 0; i < 9; i++) { System.out.printf("%s (%ds)%n", light, light.getDuration()); light = light.next(); }
Product record with name, category, and pricegroupingBy + maxBy)public record Product(String name, String category, double price) { Product { if (price <= 0) throw new IllegalArgumentException("Price must be > 0"); } } List<Product> catalogue = List.of( new Product("Laptop", "Electronics", 999.99), new Product("Mouse", "Electronics", 29.99), new Product("Keyboard","Electronics", 79.99), new Product("Desk", "Furniture", 349.00), new Product("Chair", "Furniture", 199.00), new Product("Lamp", "Furniture", 49.00) ); catalogue.stream() .collect(Collectors.groupingBy( Product::category, Collectors.maxBy(Comparator.comparingDouble(Product::price)) )) .forEach((cat, p) -> p.ifPresent(prod -> System.out.printf("%s: %s (%.2f)%n", cat, prod.name(), prod.price()))); // Electronics: Laptop (999.99) // Furniture: Desk (349.00)