Core Java · Enums & Records · Section 1 of 10

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.

Beginner-friendly
~8 min read
10 sections

The Problem Enums Solve

Imagine you're writing code for the days of the week. Without enums, you might use integers or strings:

BadDays.java⚠ Using Strings/Ints
// 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.

GoodDays.java✓ Using an Enum
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!");
}
✓ Enums Are a Class
Despite the 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

Declaration
Use enum keyword. Constants are UPPER_SNAKE_CASE by convention.
Usage
EnumName.CONSTANT — the enum type plus the constant name.
Comparison
Use == to compare enum values — they're singletons, so reference equality is safe.
Section 1 of 10
Core Java · Enums & Records · Section 2 of 10

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.

Planet.javaEnum with Fields
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));
}
📐 Enum Constructor is Always Private
You never write 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:

Operation.javaPer-Constant 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
Section 2 of 10
Core Java · Enums & Records · Section 3 of 10

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

SwitchEnum.javaswitch + enum
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:

SwitchExpression.java✓ Modern switch Expression
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
✓ Exhaustiveness Checking
When you switch on an enum with the modern expression syntax and cover all constants, you don't need a 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

HttpStatus.javaPractical Enum + switch
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
🧠 Quick Check
When switching on an enum with the modern arrow syntax and all constants are covered, do you need a default case?
Section 3 of 10
Core Java · Enums & Records · Section 4 of 10

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

1
Declare fields
Usually private final — enum state shouldn't change.
2
Write a constructor
Always private (or package-private, which Java treats the same). Takes the field values.
3
Pass values in the constant list
Each constant calls the constructor: CONSTANT(arg1, arg2).
4
Add getters
Expose the data with regular getter methods.
CoffeeSize.javaEnum with Constructor
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());
}
🔑 Key Point
Each enum constant is effectively a singleton instance of the enum class, created with the arguments you specify. There is exactly one CoffeeSize.LARGE in the JVM — forever. That's why == comparison is safe for enums.
Section 4 of 10
Core Java · Enums & Records · Section 5 of 10

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.

EnumMethods.javaAll Built-in Methods
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]
⚠ Avoid Relying on ordinal()
Don't store or use 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.
Section 5 of 10
Core Java · Enums & Records · Section 6 of 10

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:

PointClass.java⚠ 30 lines for 2 fields
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 + "]"; }
}
PointRecord.java✓ Record — 1 line
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

public record Person(String name, int age) {}
keyword
record name
component list
type
name

What Gets Generated Automatically

Generated automaticallyEquivalent manual code
Canonical constructorpublic Person(String name, int age)
Accessor methodspublic String name(), public int age()
equals()compares all components by value
hashCode()based on all components
toString()Person[name=Alice, age=30]
📐 Records Are Immutable
Record fields are implicitly 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.
Section 6 of 10
Core Java · Enums & Records · Section 7 of 10

Records in Practice

Records shine as data carriers — method return types, stream elements, map entries, and API response objects.

RecordUsage.javaCreating & Using Records
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:

Stats.javaRecord as Return Type
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
✓ Perfect for DTOs
Records are ideal for Data Transfer Objects — the objects you use to move data between layers of an application (e.g. deserialising JSON from an API, carrying query results). In Spring Boot, you'll see records used constantly for request/response bodies.
Section 7 of 10
Core Java · Enums & Records · Section 8 of 10

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:

Range.javaCompact Constructor
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

MoneyRecord.javaRecord with Methods
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

Shape.javaRecord implements Interface
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
⚠ What Records Cannot Do
Records cannot:
• 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)
Section 8 of 10
Core Java · Enums & Records · Section 9 of 10

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.

EnumRecordClass
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

Quick Rule

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).

Decision.javaRight Tool for the Job
// 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; }
}
Section 9 of 10
Core Java · Enums & Records · Section 10 of 10

Quiz & Coding Challenge

Test your understanding of enums and records.

Final Quiz

Question 1 of 3
Which statement about enum constructors is true?
Question 2 of 3
What does record Point(int x, int y) {} automatically provide?
Question 3 of 3
You need to represent the four card suits (HEARTS, DIAMONDS, CLUBS, SPADES) in a card game. Which type should you use?

Coding Challenges

🏋 Challenge 1 — Enum
Traffic Light System
Model a traffic light with an enum.
01Create a TrafficLight enum with RED, AMBER, GREEN
02Each constant carries a duration in seconds (RED=30, AMBER=5, GREEN=25)
03Add a next() method that returns the next light in the cycle
04Print the full cycle three times using a loop
TrafficLight.javaSolution
public 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();
}
🏋🏋 Challenge 2 — Record
Product Catalogue
Model a simple product catalogue using records and streams.
01Create a Product record with name, category, and price
02Add validation: price must be > 0
03Create a list of at least 6 products across 2+ categories
04Use streams to find the most expensive product in each category (hint: groupingBy + maxBy)
Product.javaSolution
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)
✓ What You've Learned
Enums: replacing magic strings/ints · fields and methods on constants · per-constant behaviour · switch expressions · built-in methods (values, valueOf, ordinal, name). Records: eliminating boilerplate · auto-generated constructor, accessors, equals, hashCode, toString · compact constructors for validation · adding methods · implementing interfaces. Decision framework: enum vs record vs class.
Section 10 of 10
Roadmap