Java's Records, introduced in JDK 16, were a monumental step forward for developers. They offered a concise way to model immutable data as simple carriers, eliminating the verbose boilerplate of constructors, getters, equals, hashCode, and toString. But what if you need a record to be more flexible? What if you need to add fields without breaking the world? This is precisely the problem that JEP 429: Extensible Records aims to solve.
The Current Limitation: Sealed by Default
A standard Java record is a nominal tuple. Its state is fixed and declared upfront in its header. This design is perfect for many use cases—like a Point(int x, int y) or a Customer(String name, String email). The compiler generates all the necessary methods based on this fixed set of components.
However, this rigidity becomes a limitation in more dynamic or evolutionary data models. Imagine you are building a JSON parser or a database mapping layer. You might have a base record like JsonValue and want to create more specific types like JsonString or JsonNumber that add a single component.
With today's records, you cannot do this elegantly. You would be forced to either:
- Use a class: Losing the conciseness and guarantees of a record.
- Duplicate components: Defining
valuein bothJsonStringandJsonNumber, which is a maintenance nightmare and goes against DRY principles. - Use composition over inheritance: Leading to more complex and less intuitive code (e.g.,
jsonString.value().string()instead ofjsonString.string()).
The Vision: Records that Can Grow
JEP 429 proposes to make records extensible. The core idea is to allow a record to have subclasses that can add new components, while the core equals, hashCode, and toString methods remain governed by the record's own declared state.
Let's illustrate this with the JSON example, which is a classic use case for extensible records.
// The base record. It's abstract and has no components.
public abstract record JsonValue() {}
// An extended record adding a 'value' component.
public record JsonString(String value) extends JsonValue {}
// Another extended record, also adding a 'value' component, but of a different type.
public record JsonNumber(double value) extends JsonValue {}
// An extended record that adds multiple components.
public record JsonObject(Map<String, JsonValue> properties) extends JsonValue {}
In this model:
JsonString,JsonNumber, andJsonObjectare all full-fledged records.- Each subclass adds its own state. The
JsonStringrecord has a single component,String value. - The compiler-generated methods for
JsonStringare based only on its own component,value. It does not inherit or consider any state from the abstract parentJsonValue.
Key Semantics and Benefits
- State is Local: The fundamental rule is that a record's behavior is defined solely by its own components. An extending record does not inherit the components of its parent record. This prevents the "fragile base class" problem and maintains the simple, predictable semantics of records.
- Polymorphism Made Clean: You can now create clean, hierarchical data structures.
List<JsonValue> jsonElements = List.of( new JsonString("Hello"), new JsonNumber(42.0), new JsonObject(Map.of("key", new JsonString("value"))) ); for (JsonValue element : jsonElements) { // Use pattern matching to handle each specific type switch (element) { case JsonString s -> System.out.println("String: " + s.value()); case JsonNumber n -> System.out.println("Number: " + n.value()); case JsonObject o -> System.out.println("Object with keys: " + o.properties().keySet()); default -> throw new IllegalStateException("Unexpected value: " + element); } } - Perfect Companion to Sealed Classes: This feature synergizes perfectly with sealed classes, allowing you to define a closed, type-safe hierarchy of data carriers.
java public abstract sealed record JsonValue() permits JsonString, JsonNumber, JsonObject, JsonArray, JsonBoolean {}
Challenges and Considerations
The primary challenge JEP 429 must address is the interaction between the generated methods in the class hierarchy. For example, what should happen in this scenario?
JsonValue a = new JsonString("hello");
JsonValue b = new JsonString("hello");
System.out.println(a.equals(b)); // Should this be true?
The answer, following the proposal's logic, is yes. Even though the variables are of type JsonValue, the equals method executed will be that of JsonString, which compares the value component. This maintains the Liskov Substitution Principle.
Conclusion: A More Expressive Java
JEP 429: Extensible Records is not about turning records into traditional classes with inheritance chains of state. Instead, it's about providing a powerful, type-safe mechanism to model families of related data.
By allowing records to extend other records, Java empowers developers to build more expressive, hierarchical data models without sacrificing the simplicity, safety, and conciseness that made records so popular in the first place. It's a natural and necessary evolution of one of modern Java's most beloved features, paving the way for even more robust and maintainable data-oriented code.
Note: As a JEP in draft/proposal stage, the details and syntax of this feature are subject to change as it progresses through the Java Community Process.