Creating custom immutable classes in Java is a fundamental concept for ensuring data integrity and immutability. Immutable classes are those whose instances cannot be modified after creation, providing a strong foundation for thread safety and easy conflation. There are several ways to create such classes, each with its own set of advantages and trade-offs. This article explores the main strategies for creating custom immutable classes in Java, including using final fields, defensive copies, builder patterns, static factory methods, and the record keyword in newer versions of Java.
Using final Fields
The most basic approach to creating an immutable class is by declaring all fields as final, ensuring they can only be assigned once. This method is simple yet effective. You also need to provide a constructor to initialize these fields, and importantly, do not provide setter methods to prevent modification.
Example:
public final class ImmutablePoint {
private final int x;
private final int y;
public ImmutablePoint(int x, int y) {
this.x x;
this.y y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
}
Defensive Copies
Defensive copying is another strategy where your class holds mutable objects, but you return copies of these objects rather than the objects themselves. This prevents external modifications that could affect the immutability of the class.
Example:
public final class ImmutableList {
private final ListString items;
public ImmutableList(ListString items) {
new ArrayList(items);
}
public ListString getItems() {
return new ArrayList(items);
}
}
Builder Pattern
The builder pattern is particularly useful for classes with many fields, as it allows for a more readable and maintainable way to create instances of an immutable class. By encapsulating the construction process, you can delegate complex creation logic to the builder, which can also offer a fluent API.
Example:
public final class ImmutableUser {
private final String name;
private final int age;
private ImmutableUser(Builder builder) {
;
;
}
public static class Builder {
private String name;
private int age;
public Builder setName(String name) {
name;
return this;
}
public Builder setAge(int age) {
age;
return this;
}
public ImmutableUser build() {
return new ImmutableUser(this);
}
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
Static Factory Methods
Static factory methods are a more modern approach, especially useful for readability and flexibility. Instead of using constructors directly, static factory methods can be used to create instances, providing a specific naming convention that often makes the code more readable.
Example:
public final class ImmutableRectangle {
private final int width;
private final int height;
private ImmutableRectangle(int width, int height) {
this.width width;
this.height height;
}
public static ImmutableRectangle of(int width, int height) {
return new ImmutableRectangle(width, height);
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
}
Using record Keyword (Java 14 )
Starting with Java 14, the record keyword provides a more concise and elegant way to define immutable classes. Records are type-safe, immutable, and come with built-in constructors, getters, and hashes, making them a preferred choice for simple immutables.
Example:
public record ImmutableCircle(int radius) {}
Summary
The choice of method for creating custom immutable classes depends on the specific needs of your application, such as complexity, readability, and maintainability. By leveraging these strategies, you can ensure the immutability and reliability of your Java classes, enhancing their usability in various scenarios.