Records & Enums

Java · Lesson 19 of 32 6 min read Updated

A lot of classes exist only to carry data: a product with a name and price, a student's marks, a point on a map. Writing such a class by hand means private final fields, a constructor, getters, equals, hashCode and toString: often fifty lines for three fields. A record, added in Java 16, does all of that in one line.

Another common need is a variable that may hold only a few fixed values: a pizza size, a day of the week, a train class. An enum (enumerated type) defines exactly those values, so nothing else can sneak in. In this lesson you will use both, from the simplest form to records with validation and enums with their own fields and methods.

Records: data classes in one line

You declare a record with its components in brackets. Java generates the rest.

Java
public class Records {
    record Product(String name, int priceRupees, int stock) {}

    public static void main(String[] args) {
        Product p1 = new Product("Steel water bottle", 449, 30);
        Product p2 = new Product("Steel water bottle", 449, 30);

        System.out.println(p1);
        System.out.println(p1.name() + " costs Rs " + p1.priceRupees());
        System.out.println(p1.equals(p2));
        System.out.println(p1 == p2);

        Product cheaper = new Product(p1.name(), 399, p1.stock());
        System.out.println(cheaper);
    }
}
Output
Product[name=Steel water bottle, priceRupees=449, stock=30]
Steel water bottle costs Rs 449
true
false
Product[name=Steel water bottle, priceRupees=399, stock=30]

From that single line, Java created:

  • a private final field for each component,
  • a canonical constructor that takes all components in order,
  • accessor methods named exactly like the components: name(), priceRupees() (no get prefix),
  • equals and hashCode that compare the values of all components, which is why p1.equals(p2) is true,
  • a readable toString.

Records are immutable: there are no setters, and p1.x = 5 style assignments fail with error: cannot assign a value to final variable. To "change" a record, create a new one, as with cheaper. A record cannot extend another class (it already extends java.lang.Record), but it can implement interfaces. Here the record is declared inside the class Records; records can also be top-level types in their own files, or even declared inside a method.

Validation and extra methods

You can add a compact constructor to check or tidy the values. It has no parameter list: the parameters are implied, and the fields are assigned automatically at the end. You can also add ordinary methods and static factory methods.

Java
public class Compact {
    record Marks(String student, int maths, int science, int english) {
        Marks {                                    // compact constructor
            if (student == null || student.isBlank()) {
                throw new IllegalArgumentException("student name is required");
            }
            for (int m : new int[]{maths, science, english}) {
                if (m < 0 || m > 100) {
                    throw new IllegalArgumentException("marks must be 0-100, got " + m);
                }
            }
            student = student.strip();             // normalise before the fields are set
        }

        int total() {
            return maths + science + english;
        }

        double percentage() {
            return total() / 3.0;
        }

        static Marks absent(String student) {
            return new Marks(student, 0, 0, 0);
        }
    }

    public static void main(String[] args) {
        Marks isha = new Marks("  Isha ", 91, 88, 79);
        System.out.println(isha);
        System.out.printf("%s: total %d, %.1f%%%n", isha.student(), isha.total(), isha.percentage());
        System.out.println(Marks.absent("Rohan"));
        new Marks("Kabir", 105, 70, 60);
    }
}
Output
Marks[student=Isha, maths=91, science=88, english=79]
Isha: total 258, 86.0%
Marks[student=Rohan, maths=0, science=0, english=0]
Exception in thread "main" java.lang.IllegalArgumentException: marks must be 0-100, got 105
	at Compact$Marks.<init>(Compact.java:11)

The compact constructor rejected Kabir's 105 before the record existed, so invalid marks can never be stored. In printf, %% prints a literal percent sign.

Use a record whenever a class is mainly a group of values: data from a form, a row from a database, a result returned by a method. Use a normal class when the object has changing state or behaviour that should stay hidden, such as a bank account. You can even destructure records in switch, which you will see in Sealed Classes & Pattern Matching.

Enums: a fixed set of values

An enum lists every allowed value, called constants, in capital letters. A variable of the enum type can hold only one of them (or null).

Java
public class Enums {
    enum Size { SMALL, MEDIUM, LARGE }

    public static void main(String[] args) {
        Size mine = Size.MEDIUM;
        System.out.println(mine);
        System.out.println(mine.name() + " is at position " + mine.ordinal());
        System.out.println(mine == Size.MEDIUM);

        for (Size s : Size.values()) {
            System.out.print(s + " ");
        }
        System.out.println();

        Size fromText = Size.valueOf("LARGE");
        int price = switch (fromText) {
            case SMALL -> 149;
            case MEDIUM -> 249;
            case LARGE -> 349;
        };
        System.out.println(fromText + " pizza costs Rs " + price);
    }
}
Output
MEDIUM
MEDIUM is at position 1
true
SMALL MEDIUM LARGE 
LARGE pizza costs Rs 349

Every enum gets these methods for free: values() returns all constants in order, valueOf("LARGE") converts text to a constant, name() gives the constant's name and ordinal() its position from 0. Each constant exists exactly once, so comparing with == is correct and safe for enums.

Notice the switch expression has no default. Because it covers every constant of Size, the compiler knows it is exhaustive. If someone later adds EXTRA_LARGE, this switch stops compiling until it is handled, which is exactly the safety you want.

Enums with fields and methods

Enums are full classes. Each constant can carry data passed to a constructor, and the enum can have methods. The constants must come first, followed by a semicolon.

Java
public class Coach {
    enum TrainClass {
        SLEEPER("SL", 1.0, false),
        THIRD_AC("3A", 2.6, true),
        SECOND_AC("2A", 3.8, true),
        FIRST_AC("1A", 6.4, true);

        private final String code;
        private final double multiplier;
        private final boolean airConditioned;

        TrainClass(String code, double multiplier, boolean airConditioned) {
            this.code = code;
            this.multiplier = multiplier;
            this.airConditioned = airConditioned;
        }

        int fareFor(int baseFare) {
            return (int) Math.round(baseFare * multiplier);
        }

        boolean isAirConditioned() {
            return airConditioned;
        }

        static TrainClass fromCode(String code) {
            for (TrainClass c : values()) {
                if (c.code.equals(code)) {
                    return c;
                }
            }
            throw new IllegalArgumentException("Unknown class code: " + code);
        }
    }

    public static void main(String[] args) {
        int base = 450;
        for (TrainClass c : TrainClass.values()) {
            System.out.printf("%-10s %s  Rs %5d  AC: %s%n",
                    c, c.code, c.fareFor(base), c.isAirConditioned());
        }
        System.out.println(TrainClass.fromCode("2A"));
    }
}
Output
SLEEPER    SL  Rs   450  AC: false
THIRD_AC   3A  Rs  1170  AC: true
SECOND_AC  2A  Rs  1710  AC: true
FIRST_AC   1A  Rs  2880  AC: true
SECOND_AC

The multipliers here are made up for the example, but the design is real: all the information about each class lives in one place. Adding a new class means adding one line. Enum constructors are always private; you cannot write new TrainClass(...). The java.util package also offers EnumMap and EnumSet, very fast collections for enum keys.

Common mistakes

  • Writing getName() for a record. Record accessors use the component name: product.name().
  • Trying to change a record. Records are immutable; create a new record with the changed value.
  • Using a record for an object with changing state. A bank account or a shopping cart is better as a normal class.
  • valueOf with the wrong case. Size.valueOf("medium") throws IllegalArgumentException: No enum constant ... medium. Convert first: valueOf(text.toUpperCase()).
  • Storing ordinal() in a database. If the constants are reordered, saved numbers change meaning. Store name() instead.

Key points

  • record Name(Type a, Type b) {} creates an immutable data class with constructor, accessors, equals, hashCode and toString.
  • Record accessors are named after the components, without get.
  • A compact constructor Name { ... } validates or normalises values.
  • An enum defines a fixed set of constants; use values(), valueOf(), name() and ordinal().
  • Enums can have fields, constructors and methods; compare constants with ==.
  • A switch expression over an enum that covers every constant needs no default.

Exercise

Create an enum Payment with constants UPI, CARD and CASH, each with a fee percentage (0.0, 1.5 and 0.0) and a method fee(int amount). Then create a record Order(String customer, int amount, Payment payment) whose compact constructor rejects amounts of 0 or less, with a method totalToPay() that adds the fee. Create orders for Priya (Rs 1200, CARD) and Farhan (Rs 560, UPI) and print each order and its total.

Show solution
Java
public class Shop {
    enum Payment {
        UPI(0.0), CARD(1.5), CASH(0.0);

        private final double feePercent;

        Payment(double feePercent) {
            this.feePercent = feePercent;
        }

        double fee(int amount) {
            return amount * feePercent / 100;
        }
    }

    record Order(String customer, int amount, Payment payment) {
        Order {
            if (amount <= 0) {
                throw new IllegalArgumentException("amount must be positive");
            }
        }

        double totalToPay() {
            return amount + payment.fee(amount);
        }
    }

    public static void main(String[] args) {
        Order[] orders = {
            new Order("Priya", 1200, Payment.CARD),
            new Order("Farhan", 560, Payment.UPI)
        };
        for (Order o : orders) {
            System.out.println(o + " -> pay Rs " + o.totalToPay());
        }
    }
}
Output
Order[customer=Priya, amount=1200, payment=CARD] -> pay Rs 1218.0
Order[customer=Farhan, amount=560, payment=UPI] -> pay Rs 560.0

The enum keeps the fee rules, and the record keeps the order data. The record's toString prints the enum constant by name automatically.

Quick Quiz

1. For record Train(String name, int number) {}, how do you read the number?

2. Two records have exactly the same component values. What does equals return?

3. What does Size.values() return?

4. Why can a switch expression over an enum skip default?

Learn Java with a mentor

Free lessons get you started. Our Java, C & C++ Programming training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call