Back to Blog
Java

Java Record Methods: Custom Methods and Overrides

java record methods: Learn how Java record methods work: auto-generated accessors, custom instance methods, compact constructors, static methods, and interface impleme...

javarecordsmethodsaccessorsconstructorsimmutability
Diagram showing a Java record with its automatically generated methods and custom methods added by a developer.

Java records automatically generate a set of methods, but they also allow you to define your own. Understanding how java record methods work—what is generated, what you can override, and where the language imposes limits—helps you write concise, predictable data carriers.

What Methods Does a Record Generate Automatically?

When you declare a record, the compiler generates a canonical constructor, accessor methods for each component, and implementations of equals, hashCode, and toString. For a simple record like this:

public record Point(int x, int y) {}

The generated methods are equivalent to:

public Point(int x, int y) { this.x = x; this.y = y; } public int x() { return x; } public int y() { return y; } public boolean equals(Object o) { ... } public int hashCode() { ... } public String toString() { ... }

These methods are derived from the component list. The accessor names match the component names, and the canonical constructor assigns each component exactly as declared. You rarely need to write these yourself, but you can override any of them if your use case demands different behavior.

Adding Custom Instance Methods to a Record

Records can contain instance methods beyond the generated ones. These methods can access the record's components directly. For example, you might add a method that computes a derived value:

public record Rectangle(double width, double height) { public double area() { return width * height; } }

Here, area() is a custom method that uses the width and height components. It behaves like any other instance method in a class, but because records are immutable, it should not modify any state. Custom methods are useful for encapsulating logic that operates on the record's data without exposing internal fields.

Overriding Accessor Methods

You can override the generated accessor methods to change their behavior. For instance, you might want to return a normalized or transformed value:

public record Person(String name) { @Override public String name() { return name.trim(); } }

This overrides the name() accessor to trim whitespace. However, be cautious: the canonical constructor still stores the original value. If you need the stored value to be normalized, use a compact constructor instead. Overriding accessors is appropriate when the transformation is purely a view of the underlying data, not a change to the record's state.

Using Compact Constructors for Validation and Normalization

The canonical constructor can be replaced with a compact constructor, which omits the parameter list and implicitly assigns all components after the body runs. This is the preferred place to validate or normalize input:

public record Range(int min, int max) { public Range { if (min > max) { throw new IllegalArgumentException("min must be <= max"); } } }

In this compact constructor, min and max are the parameters, and the assignment this.min = min; this.max = max; happens automatically after the validation block. You can also modify the parameters before they are assigned, which is useful for normalization:

public record Email(String address) { public Email { address = address.toLowerCase(); } }

Because the compact constructor runs before the implicit assignment, any changes to the parameter variables are reflected in the final component values. This keeps validation and normalization in one place, avoiding duplication across call sites.

Static Methods and Constants in Records

Records can declare static fields, static methods, and static initializers. Static methods are often used as factory methods or utility functions. For example:

public record Temperature(double celsius) { public static Temperature fromFahrenheit(double fahrenheit) { return new Temperature((fahrenheit - 32) * 5 / 9); } public static final Temperature FREEZING = new Temperature(0); }

Static fields must be declared static final if they are initialized inline, but you can also use a static initializer block. This allows you to attach constants and factory methods directly to the record type, keeping related logic together. Note that static methods cannot access instance components, but they can create new record instances.

Implementing Interfaces with Records

Records can implement interfaces, and they must provide implementations for any abstract methods. This is a clean way to give records behavior beyond simple data access. For example:

public interface Shape { double area(); } public record Circle(double radius) implements Shape { @Override public double area() { return Math.PI * radius * radius; } }

The record Circle implements Shape by providing the area() method. You can also implement multiple interfaces. Because records are implicitly final, they cannot be extended, but they can still participate in polymorphism through interfaces. This makes records suitable for use cases like DTOs that must conform to a contract.

Constraints on Record Methods and Fields

Records have specific restrictions that affect how you can define methods and fields. You cannot declare any instance fields other than the record components. This means you cannot add a cache field or a derived value as an instance variable. If you need such state, you must either compute it in a method or use a static field. Similarly, records cannot have instance initializer blocks; the only initialization path is the canonical constructor.

Records are also implicitly final, so you cannot make them abstract or extend them. All accessor methods are public by default, but you can override them with public access only—you cannot reduce visibility. Finally, records cannot declare native methods, and they cannot have finalize methods. These constraints exist to preserve the record's contract of transparent data carrier and immutability.

Runtime and Maintainability Considerations

Because records are immutable, any custom method should be side-effect free. Overriding equals, hashCode, or toString is possible but rarely necessary; the generated versions are based on all components and follow the standard contract. If you override an accessor to return a transformed value, be aware that equals and hashCode still use the raw component values, which can lead to inconsistent behavior if the transformation is not reversible.

From a performance perspective, the generated accessors are simple field reads. Custom methods that perform complex calculations add their own cost, but that is no different from any other Java class. Records do not introduce additional runtime overhead compared to a hand-written class with the same methods. The main operational benefit is that records reduce boilerplate, making the code easier to maintain and less error-prone.

One practical limitation is that you cannot lazily initialize derived data without using a static field or recomputing it each time. For example, if you need a cached hash code, you would have to store it in a static Map keyed by the record instance, which is cumbersome. In most cases, recomputing a value is cheaper than the complexity of caching, so it is usually the right tradeoff.

java record methods: Practical Usage and Code Examples | RYUSLOG DEV