Oracle has released the Java version 24 this year, but still I see the usage of the Java Records (release in Java 16) is limited. This article will be focussed on Java Records what it does and where we can use it. Lets dive into it.
Introduction
Based on JEP:359, Records are kind of type declarations and is a restricted form of class. It gives up the freedom that classes usually enjoy and in return, gain a significant degree of concision. Below code sample tells you, how you can create a record.
public record Person(String name, int age) {}
A record acquires many standard members automatically, like:
- A
private final
field for each component of the state description. - A public read accessor method for each component of the state description, with the same name and type as the component.
- A public constructor, whose signature is the same as the state description, which initialises each field from the corresponding argument.
- Implementations of
equals
andhashCode
that say two records are equal if they are of the same type and contain the same state. - An implementation of
toString
that includes the string representation of all the record components, with their names.
Characteristics
Let's go through some of these characteristics one by one.
🔒 Fields of Records are private and final by Default
In Java Records, all fields are implicitly declared as private and final. This means that once the values are set via the canonical constructor, they cannot be changed. Also, since the fields are private, they cannot be accessed directly from outside the record. Instead, you access them via the automatically generated accessor methods.
This immutability is one of the key traits of records, making them a perfect choice for modeling data carriers or value objects.
public record Person(String name, int age) {
// No need to explicitly declare fields, getters, or constructor
}
💡 Internally, this is roughly equivalent to:
public final class Person {
private final String name;
private final int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String name() {
return name;
}
public int age() {
return age;
}
}
As you can see, the name and age fields are private final, and you get read-only access through the generated methods name() and age().
🔁 equals(), hashCode(), and toString() are Generated by Default
One of the most convenient features of Java Records is that they automatically generate implementations for the equals(), hashCode(), and toString() methods based on all the record fields.
This makes records ideal for use cases like data transfer objects or value types, where equality is based on content rather than identity — and saves you from writing a lot of boilerplate code manually.
🧠 Behind the Scenes, compiler automatically provides:
- equals() — Compares all fields for equality
- hashCode() — Computes hash based on all fields
- toString() — Generates a string like Person[name=John, age=30]
No need to override any of these unless you want to customize the behaviour, records handle it for you out of the box.
🚫 Records Can't Participate in Data Hierarchies
Java Records cannot extend other classes, including other records. This is because records implicitly extend java.lang.Record, and Java does not support multiple inheritance for classes.
This restriction ensures that records remain simple, immutable data carriers without the complexities of inheritance hierarchies.
✅ You can implement interfaces:
public interface Identifiable {
String id();
}
public record User(String id, String name) implements Identifiable {
}
❌ You cannot extend another class or record:
public class Base {
// ...
}
// ❌ This will not compile
public record Derived(String id) extends Base {
}
💡 Why this restriction?
Allowing records to participate in inheritance hierarchies would contradict their design goals of simplicity, transparency, and immutability. They're intended to be final, shallow, and self-contained data holders, not flexible object-oriented components.
📦 Records Can Be Declared Inline
Since Java 16, records can also be declared inline (i.e., as local or nested classes) within methods, constructors, or other blocks of code. This is useful when you need a simple data carrier only within a limited scope — for example, as a helper during data transformation or aggregation.
This feature promotes cleaner code without polluting the top-level class or package namespace.
Example:
✅ Declaring a record inside a method
public class OrderService {
public void processOrder() {
record OrderItem(String name, int quantity) {}
OrderItem item = new OrderItem("Book", 3);
System.out.println("Processing item: " + item);
}
}
⛔ Scope Limitation
Inline/local records are only visible within the block/method they’re defined in. This is very useful for temporary data structures that are not reused elsewhere.
💡 When to Use
- Grouping temporary fields in data processing
- Returning lightweight objects from helper methods
- Avoiding boilerplate for short-lived data carriers
This feature helps to use the full power of records even for short-lived and context-specific use cases.
✨ Records Can Use Compact Constructors
Java Records support a special form of constructor called a compact constructor, which allows you to add validation or custom logic without repeating the assignment of fields.
In compact constructors, you don’t need to explicitly assign parameters to fields — the compiler does it for you automatically. You just focus on preconditions or side effects.
✅ Example: Validating Fields in a Compact Constructor
public record Product(String name, double price) {
public Product {
if (price < 0) {
throw new IllegalArgumentException("Price cannot be negative");
}
name = name.strip(); // You can also transform fields
}
}
🔍 What’s Happening Here?
The compiler automatically generates:
this.name = name;
this.price = price;
after the custom code block runs.
This makes the code cleaner compared to explicitly writing a full canonical constructor.
❗ Restrictions
- You can’t change the fields directly (they’re final)
- You can modify or validate parameters before the assignment happens
- You must use the parameter names exactly as declared in the record header
This makes the java records much more than just “dumb data holders” and allows to enforce invariants and transformations cleanly and concisely.
🟰 Records Use Data Equality, Not Reference Equality
Unlike regular Java classes (where equals() by default compares references), records are designed to represent data, so they use data (value-based) equality out of the box.
This means two record instances are considered equal if all their fields are equal, even if they're different objects in memory.
✅ Example:
public record Point(int x, int y) {}
public class Demo {
public static void main(String[] args) {
Point p1 = new Point(3, 4);
Point p2 = new Point(3, 4);
System.out.println(p1 == p2); // false (reference equality)
System.out.println(p1.equals(p2)); // true (data equality)
}
}
💡 What's Going On?
- "==" checks if p1 and p2 are the same object → returns false
- .equals() (auto-generated by the record) checks if all fields match → returns true
🚀 Why This Matters ?
This behaviour makes records ideal for use cases where:
- You care about what data an object holds, not which instance it is.
- You need reliable comparisons in collections like Set, Map, or during testing
So with records, you get true value semantics just like data classes in Kotlin or case classes in Scala.
🔐 Records Are Strongly Immutable, Even Reflection Can't Modify Them
Unlike regular classes where immutability is often conventional (you just don’t provide setters), with records it is enforced by the JVM.
All fields in a record are private, final and assigned once via the constructor. Even the reflection which can typically bypass access modifiers can't modify record fields, unless you use JVM-breaking hacks like Unsafe or deep instrumentation.
✅ Example:
public record User(String username, int age) {
}
🧪 Trying to Break with Reflection:
import java.lang.reflect.Field;
public class Demo {
public static void main(String[] args) throws Exception {
User user = new User("Ankit", 28);
Field field = User.class.getDeclaredField("age");
field.setAccessible(true);
field.set(user, 35); // Throws IllegalAccessException or InaccessibleObjectException
}
}
💥 Result is an exception like:
java.lang.IllegalAccessException: Cannot modify a final field
Or, on newer JVMs:
java.lang.reflect.InaccessibleObjectException
🚫 Why This Matters
- Ensures data integrity
- Safer to use in multi-threaded environments
- Makes records ideal for cache keys, messages, DTOs, and functional programming patterns
- With records, immutability isn’t just a best practice. It’s a guarantee baked into the language and runtime.
👉 "Hope this article helps other developers get started with Java Records. Let me know what you think. Have you used records in production yet?"
Top comments (0)