Back to Blog
Java

Java Record Accessor: Defaults and Customization

java record accessor: Explains how Java record accessor methods are generated, when to customize them, and how defensive copies protect mutable components.

Java RecordsAccessor MethodsDefensive CopyCompact Constructor
Diagram showing a Java record component mapped to its generated accessor method with a defensive copy shield.

When you declare a Java record, the compiler generates an accessor method for each component. For record Point(int x, int y), the compiler produces x() and y() methods whose names match the component names exactly. The java record accessor is the only public read path for a component, so understanding how it behaves and when to override it matters for correctness and maintainability.

How the Default Accessor Is Generated

The compiler synthesizes a public, final accessor for every component. The generated code is equivalent to this:

public record Point(int x, int y) { }
public int x() { return this.x; } public int y() { return this.y; }

The accessor returns the component value directly. For immutable components such as primitives, String, and other records, returning the stored reference is safe because the value cannot change after construction. The private fields are not directly readable outside the record, so the accessor defines the observable behavior of each component.

Customizing the Accessor Method

You can replace the default accessor with your own implementation. The custom method must have the same name, an empty parameter list, and the same return type as the component. When you provide one, the compiler does not generate a default accessor for that component.

public record Temperature(double celsius) { public double celsius() { return Math.round(celsius * 10) / 10.0; } }

Here the accessor rounds the stored value to one decimal place on every read. The field still holds the original value passed to the constructor; only the read path changes. This is useful when the stored representation differs from the public representation.

A separate concern is adding derived methods that are not accessors. For example:

public record Distance(double meters) { public double kilometers() { return meters / 1000; } }

kilometers() is an additional method, not an accessor override. Only a method named meters() would override the generated accessor. Adding derived convenience methods is fine, but it is distinct from customizing the accessor itself.

Validation in the Accessor vs the Compact Constructor

A common mistake is placing validation logic inside the accessor. The accessor runs on every read, so validation there is repeated work and does not prevent invalid instances from being created.

public record Temperature(double celsius) { public Temperature { if (celsius < -273.15) { throw new IllegalArgumentException("Temperature below absolute zero"); } } }

The compact constructor runs once, during construction, and is the correct place to enforce invariants. Every instance must pass through it, and because record fields are final, the invariant holds for the lifetime of the object.

If you validate only in the accessor, a caller can still construct an invalid instance and read it without triggering an error until the accessor is called. Code that never calls the accessor silently holds an invalid object. Validation in the accessor also adds cost to every read, which is wasteful when the value is read frequently.

There is one theoretical case where accessor validation makes sense: when the stored value can become invalid after construction. That cannot happen with a record's own final fields, so in practice keep validation in the compact constructor and keep accessors simple.

Returning Defensive Copies for Mutable Components

Records provide shallow immutability. The fields are final, but the objects they reference are not automatically immutable. If a component is an array, a List, or another mutable type, the default accessor returns the internal reference.

public record Basket(String[] items) { }

Callers can mutate the array through the accessor:

Basket basket = new Basket(new String[]{"apple", "pear"}); basket.items()[0] = "rotten";

This changes the record's internal state and breaks the immutability expectation. The fix has two parts. First, copy the input in the compact constructor so the record does not hold a reference to the caller's array:

public record Basket(String[] items) { public Basket { items = items.clone(); } public String[] items() { return items.clone(); } }

The constructor copy prevents the caller from mutating the array after construction. The accessor override returns a fresh copy on every read, so callers cannot mutate the internal array either. This pattern is the main legitimate reason to override an accessor.

For List components, List.copyOf is a better fit because it rejects null elements and returns an unmodifiable list:

public record Basket(List<String> items) { public Basket { items = List.copyOf(items); } }

Because the list is unmodifiable, the default accessor can return the field directly without copying on every read. No accessor override is needed.

Runtime Behavior and Performance Considerations

The default accessor is a trivial getter that returns a field. The JIT compiler can inline it, so in practice the cost of calling x() is negligible in hot code paths.

Custom accessors change that. A clone in the accessor allocates a new array or collection on every call. If the accessor is invoked in a loop that processes many elements, that allocation cost is paid repeatedly. The defensive-copy pattern trades allocation cost for safety. When the component is large or the accessor is called frequently, consider whether an unmodifiable view can replace a fresh copy.

Formatting or computation in the accessor, such as the rounding example earlier, also runs on every read. If the value is read many times, the same computation repeats. If the computation is expensive and the value is read often, normalize the value in the compact constructor so the accessor stays trivial.

Records do not cache accessor results. The accessor does not memoize its return value. If you need to cache an expensive derived value, a record is not the right tool; a class with a lazily initialized field is a better fit.

Compatibility and Maintainability Tradeoffs

Records are implicitly final. You cannot extend a record or override its accessors from a subclass. The accessor contract is fixed by the component declaration: the name, return type, and absence of parameters are all determined by the record header.

Changing a component name changes the accessor name. That is a breaking change for any code that calls the accessor. Renaming a component is not a purely internal refactor; it changes the public API.

Custom accessors must preserve the observable contract reasonably. If the accessor returns a transformed value, callers that expect the exact stored value will observe different behavior. This is fine when the transformation is intentional, but it can hide bugs if the transformation is subtle. Keep custom accessors predictable and document any deviation from the raw component value.

The defensive-copy pattern is the main place where accessors do real work. It is worth the cost for mutable components, but it should be applied deliberately. For immutable components, the default accessor is correct and should be left alone.

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