Enums & Records
Advanced enums, record auto-boilerplate, sealed classes
Basic enum
enum defines a fixed set of constant values. Each enum constant is essentially a static instance of that enum type, fixed at compile time, type-safe (the compiler blocks invalid values), and usable in switch.
// EnumBasic.java
public class EnumBasic {
enum Status { PENDING, ACTIVE, DONE, CANCELLED }
public static void main(String[] args) {
Status s = Status.ACTIVE;
System.out.println(s); // ACTIVE
System.out.println(s.name()); // ACTIVE
System.out.println(s.ordinal()); // 1 (declaration order)
// iterate all values
for (Status v : Status.values()) {
System.out.println(v);
}
// restore from a string (throws IllegalArgumentException if not found)
Status parsed = Status.valueOf("DONE");
System.out.println(parsed);
}
}enum + switch Expression
The switch expression (Java 14+) pairs especially well with enum: the compiler checks exhaustiveness — if all cases are listed, default can be omitted; a missing case is a compile error.
// EnumSwitch.java
public class EnumSwitch {
enum Color { RED, GREEN, BLUE }
static String hex(Color c) {
return switch (c) {
case RED -> "#ff0000";
case GREEN -> "#00ff00";
case BLUE -> "#0000ff";
};
}
public static void main(String[] args) {
for (Color c : Color.values()) {
System.out.println(c + " " + hex(c));
}
}
}enum with Fields and Methods
An enum is also a class — it can have fields, constructors, and methods. Each constant calls the constructor via NAME(args) at declaration. Common for "enums with metadata".
// HttpStatus.java
public class HttpStatus {
enum Code {
OK(200, "OK"),
NOT_FOUND(404, "Not Found"),
SERVER_ERROR(500, "Internal Server Error");
final int value;
final String reason;
Code(int value, String reason) {
this.value = value;
this.reason = reason;
}
boolean isError() {
return value >= 400;
}
}
public static void main(String[] args) {
for (Code c : Code.values()) {
System.out.printf("%d %s (error=%b)%n", c.value, c.reason, c.isError());
}
}
}enum Implementing an Interface
An enum can implements an interface so each constant has specific behavior — an elegant "strategy pattern" implementation.
// EnumStrategy.java
public class EnumStrategy {
interface BinaryOp {
int apply(int a, int b);
}
enum Op implements BinaryOp {
ADD { @Override public int apply(int a, int b) { return a + b; } },
SUB { @Override public int apply(int a, int b) { return a - b; } },
MUL { @Override public int apply(int a, int b) { return a * b; } };
}
public static void main(String[] args) {
for (Op op : Op.values()) {
System.out.println(op + "(2,3) = " + op.apply(2, 3));
}
}
}record (Java 14+, final in Java 16)
record is syntactic sugar for an "immutable data carrier". One line of declaration gives you for free: private final fields, the canonical constructor, accessors (not getX but x()), equals, hashCode, toString. It massively reduces DTO boilerplate.
// RecordBasic.java
public class RecordBasic {
record Point(int x, int y) {}
public static void main(String[] args) {
Point p1 = new Point(1, 2);
Point p2 = new Point(1, 2);
System.out.println(p1.x()); // the accessor is x(), not getX()
System.out.println(p1); // Point[x=1, y=2] (auto toString)
System.out.println(p1.equals(p2)); // true (auto equals)
System.out.println(p1.hashCode() == p2.hashCode()); // true (auto hashCode)
}
}record Compact Constructor (Validation)
A record's default constructor assigns parameters to same-named fields. To add validation, write a "compact constructor" — no parameter list, just validation code inside { }. Fields are assigned automatically at the end.
// RecordValidate.java
public class RecordValidate {
record Range(int low, int high) {
public Range { // compact constructor
if (low > high) {
throw new IllegalArgumentException("low > high");
}
}
// you can add extra methods
public boolean contains(int x) {
return x >= low && x <= high;
}
}
public static void main(String[] args) {
Range r = new Range(1, 10);
System.out.println(r.contains(5)); // true
try {
new Range(10, 1);
} catch (IllegalArgumentException e) {
System.out.println("bad: " + e.getMessage());
}
}
}sealed Classes and Interfaces (Java 17+)
sealed restricts "which classes can extend me". Combined with record and switch pattern matching, it can express "algebraic data types" — a feature common in Kotlin / Rust that Java finally has.
// SealedDemo.java
public class SealedDemo {
sealed interface Shape permits Circle, Square, Triangle {}
record Circle(double r) implements Shape {}
record Square(double side) implements Shape {}
record Triangle(double base, double height) implements Shape {}
static double area(Shape s) {
// Java 17 uses the instanceof pattern; since Java 21 you can use case Circle c -> in a switch expression
if (s instanceof Circle c) return Math.PI * c.r() * c.r();
else if (s instanceof Square sq) return sq.side() * sq.side();
else if (s instanceof Triangle t) return 0.5 * t.base() * t.height();
throw new IllegalStateException("unreachable");
}
public static void main(String[] args) {
Shape[] shapes = { new Circle(2), new Square(3), new Triangle(4, 5) };
for (Shape s : shapes) {
System.out.printf("%s -> %.2f%n", s, area(s));
}
}
}