Back to Blog
Java

Java Record Declaration Explained

java record declaration: Learn how to declare Java records, what the compiler generates, how to add validation, and when records are the right choice for immutable dat...

Java recordsimmutable dataJava 16data classesboilerplate reduction
Java record declaration showing a compact data class with generated methods

A Java record declaration is a concise way to define a class whose primary purpose is to carry immutable data. Introduced as a final feature in Java 16, records let the compiler generate the constructor, accessors, equals, hashCode, and toString methods, removing the boilerplate that usually accompanies simple data carriers.

The Syntax of a Java Record Declaration

The simplest record declaration specifies the record name and its components in parentheses. Each component is a field with an implicit private final modifier.

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

This one line replaces an entire class with a constructor, getters, equals, hashCode, and toString. The getters are named after the components, so x() and y() return the values. The canonical constructor takes all components in the declared order.

You can use records anywhere a class can be used, including as local records inside a method:

public void printDistance(int x1, int y1, int x2, int y2) { record Point(int x, int y) {} Point p1 = new Point(x1, y1); Point p2 = new Point(x2, y2); // compute distance }

Local records are useful for grouping data within a method without polluting the surrounding class.

What the Compiler Generates for You

The compiler generates the following members for a record:

  • A canonical constructor whose parameters match the record components.
  • Accessor methods with the same name as each component.
  • equals and hashCode based on all components.
  • A toString that includes the record name and each component.

Because these methods are derived from the components, they stay consistent when you add or remove a component. This removes the risk of forgetting to update equals or hashCode when a field changes.

You can override any of these generated methods, but doing so requires care. For example, overriding equals to ignore a component violates the contract that equal records have equal hash codes. In practice, you rarely need to override them unless you want a custom toString representation.

Declaring a Record with Custom Validation

The canonical constructor gives you a natural place to add validation. Because it is called for every construction path, any check placed there runs before the object becomes available.

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

Notice that the compact constructor does not declare parameters. The record components are automatically assigned after the constructor body runs. You can also normalize the data inside the compact constructor by reassigning the parameter:

public record NormalizedText(String value) { public NormalizedText { value = value.trim(); } }

The assignment to value updates the component before the record is fully constructed. This pattern is useful for ensuring invariants without exposing a separate factory method.

Records and Inheritance: What Is Not Allowed

Records cannot extend another class. They implicitly extend java.lang.Record, which is an abstract class. This means you cannot add a superclass to a record, and you cannot make a record abstract.

Records can implement interfaces. This is a common way to give records behavior that is not tied to their data.

public interface Shape { double area(); } public record Circle(double radius) implements Shape { public Circle { if (radius < 0) { throw new IllegalArgumentException("Radius must be non-negative"); } } @Override public double area() { return Math.PI * radius * radius; } }

Because records are implicitly final, you cannot create subclasses of a record. This is intentional: records are designed to be simple data carriers, and inheritance would complicate the value semantics that records provide.

Records, Serialization, and Reflection

Records have special serialization behavior. If a record implements Serializable, its serialized form is based on the component values, and deserialization always invokes the canonical constructor. This means validation in the compact constructor also runs during deserialization, which is a security advantage over ordinary serializable classes that can be deserialized without calling any constructor.

Reflection also treats records specially. The Class.isRecord() method returns true for records, and getRecordComponents() returns an array of RecordComponent objects that describe the components. This allows frameworks to inspect records without relying on naming conventions or annotations.

One practical consequence is that libraries like Jackson or Gson can serialize and deserialize records without additional configuration, as long as the JSON property names match the record component names.

When to Use Records and When to Stick with Classes

Records are ideal for data transfer objects, value objects, and immutable data holders. If your class is mostly fields with getters and no behavior, a record is usually the right choice.

However, records are not a drop-in replacement for all classes. If you need to extend another class, maintain mutable state, or use lazy initialization, a regular class is still necessary. Records also cannot have additional instance fields beyond the components, so any derived state must be computed on the fly or stored in a static context.

Consider a class that caches a derived value. A record cannot hold that cache as an instance field. You would either recompute the value each time or move the cache to a separate map, which introduces synchronization concerns. In such cases, a class with a private field and a getter is simpler.

Compatibility and Migration Considerations

Records are a final feature in Java 16, so they require a Java 16 or later compiler and runtime. If your project targets an older Java version, records are not available. Many build tools and IDEs support records when the correct language level is configured.

When migrating existing classes to records, check whether the class has any of the following:

  • Mutable fields
  • Additional constructors that do not initialize all fields
  • Inheritance from a non-Object class
  • Custom serialization logic

If any of these apply, converting the class to a record may not be straightforward. In a clean codebase, records can replace many hand-written value classes, but they work best when the design already treats the class as an immutable data carrier.

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