Java records provide a concise way to create classes whose primary purpose is to store immutable data. Although records first appeared as a preview feature in Java 14, they became a permanent part of the language with the release of Java 16.
Before records, developers often had to write constructors, accessor methods, and standard object methods manually. lerecords remove much of this repetitive code while preserving clear structure, type safety, and readability.
What Are Java Records?
A record is a special type of Java class designed to represent a fixed collection of data. It automatically generates several common class members based on the components declared in its header.
A Java record automatically provides:
- Private final fields for its components
- A public constructor
- Public accessor methods
- An
equals()method - A
hashCode()method - A readable
toString()method
According to the official JDK 16 documentation, records became a permanent Java language feature in Java 16 after being introduced and refined through earlier preview releases.
Traditional Java Class vs Java Record
Consider a simple class that stores a user’s name and age.
public final class User {
private final String name;
private final int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
@Override
public boolean equals(Object obj) {
// Equality implementation
}
@Override
public int hashCode() {
// Hash code implementation
}
@Override
public String toString() {
return "User[name=" + name + ", age=" + age + "]";
}
}
This class requires considerable boilerplate code. The same data model can be written as a Java record in a single line:
public record User(String name, int age) {
}
The Java compiler automatically creates the constructor, component fields, accessor methods, equals(), hashCode(), and toString().
Basic Syntax of a Java Record
The general syntax of a record is:
public record RecordName(Type componentName) {
}
Here is an example with multiple components:
public record Product(
long id,
String name,
double price
) {
}
A new Product record can be created like this:
Product product = new Product(101, "Laptop", 899.99);
Its values are accessed through component accessor methods:
System.out.println(product.id());
System.out.println(product.name());
System.out.println(product.price());
Unlike conventional JavaBeans, record accessors do not begin with get. For example, a record uses product.name() rather than product.getName().
Understanding Record Components
The values listed inside the record header are called record components.
public record Employee(
int employeeId,
String employeeName,
String department
) {
}
For each component, Java automatically creates:
- A private final field
- A public accessor method with the same name
- A matching parameter in the canonical constructor
The fields are final, meaning their references cannot be reassigned after the record object has been created.
Are Java Records Immutable?
Java records are shallowly immutable. Their component fields are final, so they cannot be assigned new values after construction.
For example:
User user = new User("David", 30);
// This is not allowed
// user.name = "Michael";
However, a record may contain a reference to a mutable object such as a list.
public record Team(String name, List<String> members) {
}
Although the members reference cannot be replaced, the original list may still be modified:
List<String> names = new ArrayList<>();
names.add("Ali");
Team team = new Team("Development", names);
names.add("Sara");
The record now reflects the modified list. Developers who need stronger immutability should create a defensive copy.
public record Team(String name, List<String> members) {
public Team {
members = List.copyOf(members);
}
}
This prevents outside code from modifying the record’s internal list through the original reference.

Constructors in Java Records
Records support different forms of constructors, including canonical and compact constructors.
Canonical Constructor
A canonical constructor includes parameters that match every record component.
public record Account(String username, double balance) {
public Account(String username, double balance) {
if (username == null || username.isBlank()) {
throw new IllegalArgumentException("Username is required");
}
if (balance < 0) {
throw new IllegalArgumentException("Balance cannot be negative");
}
this.username = username;
this.balance = balance;
}
}
This constructor provides complete control over validation and field assignment.
Compact Constructor
A compact constructor removes the parameter list and automatic field assignments from the source code.
public record Account(String username, double balance) {
public Account {
if (username == null || username.isBlank()) {
throw new IllegalArgumentException("Username is required");
}
if (balance < 0) {
throw new IllegalArgumentException("Balance cannot be negative");
}
}
}
Java automatically assigns the validated parameters to their corresponding fields after the compact constructor body finishes.
Adding Methods to a Record
Records are not limited to storing values. They can contain instance methods, static methods, static fields, and nested types.
public record Rectangle(double width, double height) {
public double calculateArea() {
return width * height;
}
public boolean isSquare() {
return width == height;
}
}
Usage:
Rectangle rectangle = new Rectangle(10, 5);
System.out.println(rectangle.calculateArea());
System.out.println(rectangle.isSquare());
This allows records to keep behavior close to the data it represents.
Static Members in Java Records
A record cannot declare additional instance fields, but it can contain static fields and methods.
public record Currency(String code, String symbol) {
public static final Currency USD =
new Currency("USD", "$");
public static Currency defaultCurrency() {
return USD;
}
}
Static members are useful for constants, factory methods, and operations that do not depend on a specific record instance.
Customizing Accessor Methods
The automatically generated accessor methods can be customized when necessary.
public record Person(String name) {
@Override
public String name() {
return name.trim();
}
}
The custom method must return the same type as the declared component. Developers should use customized accessors carefully because unexpected transformations can make record behavior less predictable.
Implementing Interfaces With Records
A Java record can implement one or more interfaces.
public interface Printable {
void print();
}
public record Invoice(
String invoiceNumber,
double total
) implements Printable {
@Override
public void print() {
System.out.println(
"Invoice: " + invoiceNumber + ", Total: " + total
);
}
}
This makes records useful in systems that depend on interface-based design.
Can Java Records Extend Other Classes?
A record cannot explicitly extend another class because every record implicitly extends java.lang.Record.
The following declaration is not allowed:
// Invalid Java code
public record User(String name) extends Person {
}
Records are also implicitly final, which means another class cannot extend a record.
// Invalid Java code
public class Admin extends User {
}
A record can still implement interfaces, making composition and interface-based programming the preferred approaches.
Equality in Java Records
Java records automatically implement value-based equality. Two record objects are equal when they are created from the same record type and their corresponding component values are equal.
User firstUser = new User("Emma", 25);
User secondUser = new User("Emma", 25);
System.out.println(firstUser.equals(secondUser));
Output:
true
The generated hashCode() method follows the same component-based logic, making records suitable for use as keys in hash-based collections when their components are appropriate for that purpose.
The Generated toString() Method
Records automatically generate a readable toString() representation.
User user = new User("Emma", 25);
System.out.println(user);
Typical output:
User[name=Emma, age=25]
This is particularly useful during development, testing, debugging, and logging.
Annotations and Java Records
Annotations can be applied to record declarations and components.
public record Customer(
@NotNull String name,
@Email String email
) {
}
Depending on the annotation’s declared target, Java may propagate a component annotation to the generated field, constructor parameter, or accessor method.
This capability helps records work with validation, serialization, documentation, and persistence frameworks, provided those frameworks support records correctly.
Common Use Cases for Java Records
Java records work best when a type primarily represents data rather than a complex mutable entity.
Common applications include:
Data Transfer Objects
Records can represent information transferred between application layers.
public record UserResponse(
long id,
String username,
String email
) {
}
API Request and Response Models
Records are useful for REST API payloads when the framework and serialization library support them.
public record LoginRequest(
String username,
String password
) {
}
Database Query Results
A record can represent a read-only projection returned by a database query.
public record SalesSummary(
String region,
long orderCount,
double totalRevenue
) {
}
Configuration Values
Records can group related configuration values into a clear type.
public record ServerConfig(
String host,
int port,
boolean secure
) {
}
Compound Map Keys
Because records automatically implement consistent equality and hashing, they can work well as multi-value map keys.
public record LocationKey(
String country,
String city
) {
}
Limitations of Java Records
Records are convenient, but they are not suitable for every class.
Important limitations include:
- A record cannot extend another class.
- A record is implicitly final.
- Record component fields are final.
- Additional instance fields cannot be declared.
- Records provide shallow rather than deep immutability.
- Generated accessors do not follow traditional JavaBean getter naming.
- Some older libraries may require extra configuration or may not support records.
A conventional class may be more appropriate when an object requires inheritance, extensive mutable state, lifecycle management, or a hidden representation that differs significantly from its public data model.
Records vs Lombok
Lombok annotations such as @Data and @Value can also reduce boilerplate, but records are a native Java language feature.
Records offer several advantages:
- No external dependency is required.
- Their meaning is recognized by the compiler and JVM.
- Their structure is standardized.
- Development tools can understand them consistently.
- Their generated behavior is clearly defined by Java.
Lombok remains useful when a project needs conventional classes, configurable code generation, builders, mutable properties, or framework-specific patterns that records do not provide.
Records vs Regular Classes
| Feature | Java Record | Regular Class |
|---|---|---|
| Boilerplate code | Minimal | Usually more |
| Component fields | Automatically final | Developer controlled |
| Accessors | Automatically generated | Usually written manually |
| Inheritance | Cannot extend classes | Can extend another class |
| Additional instance fields | Not allowed | Allowed |
| Best suited for | Transparent data carriers | Complex objects and behavior |
equals() and hashCode() | Automatically generated | Usually implemented manually |
Best Practices for Using Records
Follow these practices when adding records to a Java application:
- Use records for transparent data models with a fixed set of components.
- Validate component values in a compact constructor.
- Create defensive copies of mutable collections.
- Avoid placing excessive business logic inside records.
- Do not expose passwords, access tokens, or confidential values through generated
toString()output. - Confirm that serializers and frameworks in the project support records.
- Choose meaningful component names because they become part of the record’s public API.
Frequently Asked Questions
When Were Records Added to Java?
Records were first introduced as a preview feature in Java 14. They received further refinement and became a permanent language feature in Java 16.
Do Java Records Require Java 16?
Permanent record support begins with Java 16. Earlier preview implementations required preview-feature settings and should not be treated as the final standardized version.
Do Records Generate Getter Methods?
Records generate accessor methods, but they do not use the conventional get prefix. A component named email creates an email() accessor rather than getEmail().
Can a Java Record Have a Constructor?
Yes. A record may declare a canonical constructor, compact constructor, and additional overloaded constructors. Any alternative constructor must eventually invoke the canonical constructor.
Can a Record Contain Null Values?
Yes. Records permit null reference values unless validation in the constructor rejects them.
Can Records Be Serialized?
Records can work with Java serialization and many JSON libraries. Actual compatibility depends on the library and version being used, so framework support should be verified before adoption.
Are Java Records Faster Than Regular Classes?
Records are primarily a language-design and code-maintenance feature. Developers should not assume that replacing a regular class with a record will automatically produce a meaningful performance improvement.
Conclusion
Java records in Java 16 offer a clear and compact way to model immutable data carriers. By generating constructors, accessors, equality methods, hash codes, and string representations automatically, records reduce repetitive code without sacrificing type safety.
They are particularly effective for DTOs, API models, query results, configuration objects, and compound keys. However, conventional classes remain the better option when a design requires inheritance, mutable state, extra instance fields, or strict control over internal representation. Choosing records for the right data-focused use cases can make a Java codebase shorter, clearer, and easier to maintain.

2 Comments
Pingback: Types of Sensors and Actuators in IoT Explained
Pingback: Formatting Strings in Java: String.format() Method