Java Record Implements Interface: Syntax and Use Cases
java record implements interface: Learn how to make a Java record implement an interface, map accessor methods to interface contracts, and handle validation and compat...
When you write java record implements interface, you are combining two distinct language features: the concise data carrier semantics of records and the contract-based abstraction of interfaces. Records were introduced in Java 16 as a final class with state described by components, and they automatically generate equals, hashCode, toString, and accessor methods. An interface, on the other hand, defines a set of methods that a class must implement. A record can implement an interface just like any other class, but the interaction between record components and interface methods requires attention to how the generated accessors map to the interface contract.
Basic Syntax for a Record Implementing an Interface
The syntax is straightforward: after the record name and components, add implements followed by the interface name. For example:
public interface Named { String name(); } public record Person(String name, int age) implements Named { }
The record Person automatically provides an accessor method name() that matches the interface method. Because the record component name generates a public accessor with the same signature, the record satisfies the interface without any extra code. This works because the generated accessor is public and has the same return type. The age component is not part of the interface, but that does not matter.
You can implement multiple interfaces as well:
public interface AgeAware { int age(); } public record Employee(String name, int age, String department) implements Named, AgeAware { }
Here both name() and age() are satisfied by the derived accessors. This is the most common pattern: a record acts as an immutable data holder that conforms to one or more interface contracts.
Why Records Fit Interface Contracts for Data
Records are designed to be transparent carriers of immutable data. When you define an interface that describes data access (like name() or age()), a record provides a natural implementation because its accessors are already public and final. The interface can be used to decouple code from the concrete record type. For example, a method can accept a Named instead of a specific Person or Employee, allowing any record that implements Named to be passed. This promotes polymorphism without sacrificing the immutability and value semantics that records offer.
A common use case is defining a repository interface that returns a record:
public interface UserSummary { String username(); String email(); } public record UserSummaryRecord(String username, String email) implements UserSummary { }
This pattern is useful when you want to expose a read-only view of data from a service layer. The record is immutable, so the caller cannot modify the data after retrieval.
Mapping Accessor Methods to Interface Methods
The generated accessor for a record component is a public method with the same name and return type as the component. For the record to implement an interface, the interface method must match the accessor signature exactly. The method name, parameter list, and return type must align. If the interface method has a different return type, even a covariant one, the record will not compile unless you explicitly override it. For example:
public interface Labeled { String label(); } public record Item(String label, int quantity) implements Labeled { }
This compiles because label() returns String. If the interface had Object label(), the record's String return would be covariant and still satisfy the contract, but Java records generate accessors with the exact component type, so the return type is String. In practice, you should design interface methods to match the component types you intend to expose.
When the interface method has parameters, the record must provide an explicit implementation because the generated accessor takes no arguments. For instance:
public interface Describable { String describe(String prefix); } public record Product(String name, double price) implements Describable { @Override public String describe(String prefix) { return prefix + name + " (" + price + ")"; } }
Here the record must implement describe manually because it is not a simple component accessor. This is a normal method implementation, and you can use the record's components inside it.
Custom Implementations and Default Methods
Sometimes you want to implement an interface method in a way that is not a direct component accessor. You can override the generated accessor or add extra methods. For example, you might want to return a formatted value:
public interface Formatted { String display(); } public record Temperature(double celsius) implements Formatted { @Override public String display() { return celsius + "°C"; } }
The record still has a celsius() accessor, but the display() method is custom. This is fine as long as you do not try to override a generated accessor with a different return type; the compiler will reject that.
Default methods in interfaces work as expected. If the interface provides a default implementation, the record inherits it unless it overrides it:
public interface Greeter { String name(); default String greet() { return "Hello, " + name(); } } public record Guest(String name) implements Greeter { }
Guest automatically gets greet() from the default method, which calls the generated name() accessor. This is a clean way to add behavior to a data record without repeating code.
Canonical Constructor and Interface Validation
One of the strengths of records is the canonical constructor, which you can use to validate or normalize data. This constructor is invoked when the record is instantiated. You can combine it with an interface to enforce invariants. For example:
public interface PositiveAge { int age(); } public record Person(String name, int age) implements PositiveAge { public Person { if (age < 0) { throw new IllegalArgumentException("Age cannot be negative"); } } }
The canonical constructor ensures that every Person instance has a non-negative age. This validation happens before the record is used, so any code that receives a PositiveAge can trust that the age is valid. This is especially valuable when records are used as DTOs or value objects in a domain model.
Compatibility and Serialization Considerations
Records have special serialization behavior. When a record implements an interface, the serialization form is based on the record components, not on the interface. The writeReplace and readResolve methods are automatically generated to serialize the record's state as a single object. This means that adding an interface to a record does not change its serialized form, as long as the components remain the same. However, if the interface declares methods that are not derived from components, those methods are not serialized; they are computed when invoked. This is usually fine, but you should be aware that deserializing a record will call the canonical constructor, so validation logic will run again.
Another compatibility concern is evolving the interface. If you add a new method to an interface that a record implements, the record will no longer compile unless you provide an implementation. This is a breaking change for any implementing record. To avoid this, consider using default methods for new behavior that can be derived from existing components.
Common Pitfalls and How to Avoid Them
One common mistake is trying to implement an interface that requires a mutable setter method. Records are immutable, so they cannot provide a set method. If your interface has a method like void setName(String name), a record cannot implement it because records do not allow mutable state. You would need to use a class instead.
Another pitfall is relying on the generated equals and hashCode when the interface expects a different equality contract. For example, if two records have the same component values, they are equal. If your interface defines a different notion of equality (e.g., based on an ID), you cannot override equals in a record because records prohibit explicit equals and hashCode implementations unless you use the @Override annotation and write them manually. Actually, records allow overriding equals and hashCode if you explicitly declare them, but doing so removes the value-based semantics. It is usually better to keep records as value objects and let the interface reflect that.
A third issue is that record accessors are final. If an interface declares a method that is not final and you want to allow subclasses to override it, a record cannot be subclassed anyway (records are final). So this is not a problem.
Choosing Between a Record and a Class for an Interface
Use a record when the primary purpose is to carry immutable data and the interface methods align with component accessors. This is common for DTOs, query results, and domain value objects. Use a class when you need mutable state, additional constructors, or methods that modify internal state. Also, if you need to extend a base class, records cannot do that because they are final and cannot inherit from other classes (except Object).
Consider the following decision criteria:
| Criterion | Record | Class |
|---|---|---|
| Immutability | Built-in | Requires manual design |
| Boilerplate | Minimal | More boilerplate |
| Subclassing | Not allowed | Allowed |
Custom equals/hashCode | Discouraged but possible | Fully flexible |
| Best for | Data carriers | Behavioral types |
In practice, if your interface is purely a contract for reading data, a record is often the better choice. If the interface includes mutation methods or complex behavior, a class is more appropriate.
Runtime Cost and Performance Notes
Records have a small runtime footprint compared to typical classes because they do not require a separate equals method if the generated one uses component comparison. The generated accessors are simple field reads, which are as fast as a direct field access. When a record implements an interface, the method dispatch is the same as any other class; there is no extra overhead. The main performance consideration is that the canonical constructor runs on every instantiation, including during deserialization. If validation is expensive, it will affect construction time. But for most use cases, this is negligible.
One subtle point: records use invokedynamic for equals, hashCode, and toString generation, which can add a small startup cost when the class is first loaded. This is a one-time cost and does not affect steady-state performance.
Final Technical Consideration: Sealed Interfaces and Records
Sealed interfaces, introduced in Java 17, work well with records. You can restrict which records can implement a sealed interface, ensuring that all implementations are known at compile time. For example:
public sealed interface Shape permits Circle, Rectangle { double area(); } public record Circle(double radius) implements Shape { @Override public double area() { return Math.PI * radius * radius; } } public record Rectangle(double width, double height) implements Shape { @Override public double area() { return width * height; } }
This pattern gives you exhaustive pattern matching and a closed set of record types. It is a powerful combination for algebraic data types. When you use a sealed interface with records, the compiler can verify that all possible implementations are handled in a switch expression, making the code more robust.
When you combine sealed interfaces with records, you get a concise way to model domain hierarchies without the overhead of a class hierarchy. The records remain immutable, and the sealed interface guarantees that no unknown implementations can appear at runtime. This is particularly useful in domain-driven design and when modeling state machines.