In Java, an enum (short for enumeration) is a special type used to define a fixed set of constants. Introduced in Java 5, enums provide a type-safe and object-oriented way to model a limited number of possible values, such as days of the week, directions, states, or operation types.
Instead of using arbitrary strings or integers, enums improve readability, enforce compile-time checking, and offer a structured way to encapsulate related behavior alongside the constants.
A basic enum looks like this:
public enum Day {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;
}
You can use it in code as:
Day today = Day.MONDAY;
if (today == Day.MONDAY) {
System.out.println("Start of the week!");
}
This approach ensures type safety—you can’t assign a value outside the predefined set—and avoids typos or invalid constants often seen with string or integer constants.
public class EnumDemo {
// Define the enum
public enum Day {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;
}
public static void main(String[] args) {
Day today = Day.MONDAY;
if (today == Day.MONDAY) {
System.out.println("Start of the week!");
} else {
System.out.println("Not Monday");
}
}
}
Java enums are more powerful than simple constants. You can associate fields and behavior with each enum constant.
public enum Operation {
ADD("+") {
public double apply(double x, double y) {
return x + y;
}
},
SUBTRACT("-") {
public double apply(double x, double y) {
return x - y;
}
};
private final String symbol;
Operation(String symbol) {
this.symbol = symbol;
}
public String getSymbol() {
return symbol;
}
public abstract double apply(double x, double y);
}
Usage:
double result = Operation.ADD.apply(3, 5);
System.out.println("Result: " + result); // Output: Result: 8.0
This pattern allows enums to behave like classes, encapsulating logic related to each constant.
public class EnumOperationDemo {
public enum Operation {
ADD("+") {
public double apply(double x, double y) {
return x + y;
}
},
SUBTRACT("-") {
public double apply(double x, double y) {
return x - y;
}
};
private final String symbol;
Operation(String symbol) {
this.symbol = symbol;
}
public String getSymbol() {
return symbol;
}
public abstract double apply(double x, double y);
}
public static void main(String[] args) {
double result = Operation.ADD.apply(3, 5);
System.out.println("Result: " + result); // Output: Result: 8.0
}
}
Enums are often used in object-oriented design to model fixed categories of behavior or states. They are especially helpful in the following design contexts:
You can represent finite states and transitions between them using enums.
enum TrafficLight {
RED, GREEN, YELLOW;
}
Pairing this with switch statements or state-specific behavior methods can create clean, understandable models for systems with well-defined states.
Enums can serve as lightweight implementations of the Command pattern, particularly when each constant represents a distinct action:
enum Command {
START {
public void execute() { System.out.println("Starting..."); }
},
STOP {
public void execute() { System.out.println("Stopping..."); }
};
public abstract void execute();
}
This provides a concise and readable way to encapsulate commands without requiring separate classes for each.
public class CommandEnumDemo {
enum Command {
START {
public void execute() { System.out.println("Starting..."); }
},
STOP {
public void execute() { System.out.println("Stopping..."); }
};
public abstract void execute();
}
public static void main(String[] args) {
Command.START.execute(); // Output: Starting...
Command.STOP.execute(); // Output: Stopping...
}
}
switch
statements for branching logic.Enums are a powerful tool in Java’s object-oriented toolkit. They elevate constants into rich, expressive types that can carry behavior, state, and data. Whether modeling a fixed set of values, implementing state machines, or organizing command logic, enums improve both safety and clarity. When used thoughtfully, they reduce bugs, enhance maintainability, and align perfectly with the principles of clean, robust design.
Introduced in Java 14 as a preview and standardized in Java 16, records are a special kind of class in Java designed to model immutable data. They drastically reduce boilerplate by automatically generating constructors, accessors, equals()
, hashCode()
, and toString()
methods for you.
Records are particularly useful when you need simple data carriers—also called value objects—that are defined more by the data they contain than the behavior they perform.
Here’s how you declare a record:
public record Person(String name, int age) {}
Behind the scenes, Java automatically generates:
final
class.public Person(String name, int age)
).name()
and age()
).equals()
, hashCode()
, and toString()
methods.Usage:
Person p = new Person("Alice", 30);
System.out.println(p.name()); // prints "Alice"
System.out.println(p); // prints "Person[name=Alice, age=30]"
This eliminates the need for verbose code often found in traditional POJOs.
Consider a typical POJO:
public class Person {
private final String name;
private final int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
@Override
public boolean equals(Object o) { /* standard implementation */ }
@Override
public int hashCode() { /* standard implementation */ }
@Override
public String toString() { /* standard implementation */ }
}
Now compare this to:
public record Person(String name, int age) {}
Both provide the same core functionality, but the record version is cleaner, more concise, and easier to maintain.
All fields in a record are implicitly final, and records themselves are final classes. This means:
This immutability makes records ideal for use in functional programming, multi-threaded code, or data transfer objects (DTOs) where consistency and thread safety are important.
You can still add methods, static fields, and custom constructors:
public record Rectangle(int width, int height) {
public int area() {
return width * height;
}
}
You can also customize the constructor to enforce invariants:
public record Rectangle(int width, int height) {
public Rectangle {
if (width <= 0 || height <= 0) {
throw new IllegalArgumentException("Dimensions must be positive");
}
}
}
This flexibility lets records participate in robust design without losing their simplicity.
public record Rectangle(int width, int height) {
// Custom constructor to enforce positive dimensions
public Rectangle {
if (width <= 0 || height <= 0) {
throw new IllegalArgumentException("Dimensions must be positive");
}
}
// Custom method to calculate area
public int area() {
return width * height;
}
public static void main(String[] args) {
Rectangle rect = new Rectangle(5, 10);
System.out.println("Area: " + rect.area()); // Output: Area: 50
// Uncommenting the following line will throw IllegalArgumentException
// Rectangle invalidRect = new Rectangle(-3, 4);
}
}
Records are best suited for:
equals()
and hashCode()
are important.Example:
record Address(String city, String country) {}
record Customer(String name, Address address) {}
Here, both Customer
and Address
act as pure data containers, well-suited for serialization, database interaction, or API contracts.
Java records streamline the creation of simple, immutable data models, bringing clarity and conciseness to object-oriented design. By auto-generating common boilerplate and enforcing immutability, they encourage clean code and reduce opportunities for error. When designing classes whose identity is purely based on their data, records are a modern, elegant alternative to traditional POJOs.
In traditional Java inheritance, any class marked as public
or protected
can be extended by any other class—unless it’s marked as final
. This flexible model is powerful but sometimes too open. In cases where you want explicit control over subclassing, Java 15 (as a preview) and Java 17 (as a standard feature) introduced sealed classes.
A sealed class lets you define a closed set of permitted subclasses. It allows superclass authors to declare, “Only these specific classes can extend me,” providing a safer and more predictable class hierarchy.
sealed
, permits
, and non-sealed
Sealed class declarations involve three main keywords:
sealed
– Marks the class whose inheritance is restricted.permits
– Explicitly lists the permitted subclasses.non-sealed
– Marks a subclass that removes sealing, allowing unrestricted subclassing.final
– A permitted subclass can still be marked final
to prohibit further extension.Here’s a basic example:
public sealed class Vehicle permits Car, Truck {}
public final class Car extends Vehicle {}
public non-sealed class Truck extends Vehicle {}
Car
is final
— no more inheritance allowed.Truck
is non-sealed
— others can extend Truck
, but not Vehicle
.permits
will cause a compile-time error if it tries to extend Vehicle
.Let’s model a controlled payment system:
public sealed class Payment permits CreditCard, PayPal, Crypto {}
public final class CreditCard extends Payment {
// implementation
}
public final class PayPal extends Payment {
// implementation
}
public non-sealed class Crypto extends Payment {
// allows further extensions like Bitcoin or Ethereum
}
This ensures only CreditCard
, PayPal
, and Crypto
are valid payment types. It avoids unauthorized extensions and keeps the class hierarchy tightly scoped.
By explicitly declaring permitted subclasses, sealed classes help ensure that your hierarchy cannot be arbitrarily extended. This is especially important in:
Knowing exactly which classes belong to a sealed hierarchy simplifies reasoning about the code. There’s no need to anticipate unforeseen subclasses when applying logic across types.
For instance, consider a switch
expression:
static String describe(Payment payment) {
return switch (payment) {
case CreditCard c -> "Paid by Credit Card";
case PayPal p -> "Paid with PayPal";
case Crypto crypto -> "Paid with Crypto";
};
}
Because Payment
is sealed and the compiler knows all its subtypes, the switch
is exhaustive. If a new subtype is added, the compiler will require the switch to be updated, preventing silent omissions.
// Sealed class example demonstrating sealed, permits, non-sealed, and final
sealed class Vehicle permits Car, Truck {}
final class Car extends Vehicle {
// Car is final - no further subclassing allowed
}
non-sealed class Truck extends Vehicle {
// Truck allows unrestricted subclassing
}
class PickupTruck extends Truck {
// This is allowed because Truck is non-sealed
}
public class Demo {
static String describe(Vehicle vehicle) {
return switch (vehicle) {
case Car c -> "This is a Car";
case Truck t -> "This is a Truck";
default -> throw new IllegalArgumentException("Unexpected value: " + vehicle);
};
}
public static void main(String[] args) {
Vehicle car = new Car();
Vehicle truck = new Truck();
Vehicle pickup = new PickupTruck();
System.out.println(describe(car)); // Output: This is a Car
System.out.println(describe(truck)); // Output: This is a Truck
System.out.println(describe(pickup)); // Output: This is a Truck
}
}
Java’s recent enhancements in pattern matching work elegantly with sealed types. You can use sealed hierarchies to write concise, type-safe matching logic that the compiler can verify for completeness.
final
subclasses when you want to fully lock the hierarchy.non-sealed
when extending the hierarchy is acceptable downstream—but in a controlled fashion.final
, sealed
, or non-sealed
.Sealed classes represent a powerful enhancement to Java’s type system, allowing developers to create more robust and predictable class hierarchies. By limiting which classes can extend a given type, sealed classes provide greater control, improve pattern matching safety, and reduce the chance of misuse in large codebases. They’re a natural choice when modeling domain-specific hierarchies where only certain subclasses make logical sense.