The Ultimate Guide for Records in Java 21
Representative image of records in Java

The Ultimate Guide for Records in Java 21

Java Records, introduced in Java 17, simplify data encapsulation by eliminating boilerplate code in immutable data structures. If you've ever written a POJO (Plain Old Java Object) with multiple fields, constructors, getters, and implementations for equals(), hashCode() and toString(), you know how tedious and repetitive it can be.

Records address this by automatically generating these methods while ensuring immutability. But how exactly do they work, how can they be used effectively, and what rules should you be aware of? Let's break it down.

This article is based on OCP Oracle Certified Professional Java SE 21 Developer Study, specifically Chapter 7: Beyond Classes – Encapsulating Data with Records.


Context: Immutable Object Pattern

The Immutable Object Pattern is a design principle in object-oriented programming where an object's state cannot be modified after it is created. This pattern is widely used in Java to provide thread safety, simplify code maintenance, and prevent unintended side effects. Immutable objects eliminate issues related to concurrent modifications since their internal state remains unchanged throughout their lifetime.

Java provides built-in support for immutability through common classes like String, Integer, and LocalDate. When designing custom immutable objects, developers typically mark fields as private final, avoid setter methods, and make sure that mutable fields (such as collections) are copied rather than directly referenced. This approach enforces a predictable and stable object state.

Snippet of an immutable class, with extra

Why Use Records?

With records, you can achieve the same practical effects from the last snippet in a single line:

Basic record syntax

By default, Java Records provide:

  • Immutable fields (automatically private and final)
  • A constructor that initializes all fields
  • Getters (named after the fields, like person.name())
  • Automatically generated equals(), hashCode(), and toString()


Customizing Constructors

In Java, records provide an implicit constructor, but you can override it using a compact constructor. This allows you to validate input without explicitly reassigning fields or redefining parameters in the constructor signature:

Compact constructor on a record

Additionally, custom constructors can be defined, but they must always delegate to the primary constructor using this() as their first statement:

Additional constructor on a record

One important limitation is that record fields are immutable. While constructor parameters can be modified inside the constructor, the actual record fields cannot be reassigned. The following example would result in a compilation error:

Compilation error trying to access instance member on a record

Records and Pattern Matching

One of the most powerful yet complex aspects of records is their integration with pattern matching. As a quick recap, pattern matching allows checking an object's type and automatically binding it to a variable if the test succeeds, eliminating the need for explicit casting. If you want to know more about pattern matching, particularly in switch statements and expressions, check my previous article.

Example of pattern matching

Records naturally complement pattern matching by simplifying object decomposition. Here are the key rules to keep in mind when using records with pattern matching:

  1. If any field declared in the record is included, then all field must be included.
  2. The order of fields must be the same as in the record.
  3. The names of the fields do not have to match.
  4. At compile time, the type of the field must be compatible with the type declared in the record.
  5. The pattern may not match at runtime if the record supports elements of various types.

The 5 main rules when dealing with records and pattern matching

For Rule 4, the type does not need to be an exact match. For example, since String implements CharSequence, pattern matching can work with a CharSequence pattern on a String record field.

Pattern matching with records and polymorphism

Regarding Rule 5, you can use the var keyword when working with generics (or any type). This removes the need to define an explicit class at compile time, making it highly useful in certain cases:

Generics with the

Although var is often a good choice, when working with generics, you may also explore other approaches (though sticking to explicit types or var is generally recommended for clarity):

All possible ways to work with generics and pattern mathing. All conditions are matched.

Nested Record Patterns

Records can contain other records, allowing structured decomposition in pattern matching, in a somewhat recursive fashion:

Pattern matching with nested records

Pattern Matching in switch and Records

Records improve switch expressions with records by eliminating manual casting and boilerplate checks:

Pattern matching with records in a switch expression

Usual rules are applied here:

  1. A default case is not required if all possible patterns are explicitly covered. However, including one is often a good practice for future scalability, especially if new cases are introduced later.
  2. If a when clause is used, the type of the matched object matters. For example, attempting to call x() on an Object as if it were a Point will not compile unless the pattern explicitly makes sure the object is of type Point.

Compilation error with improper

Common Questions

?? Are Records Just Plain Java Classes?

Yes and no. Records are classes, but with built-in immutability and automatic method generation.

?? Can I Add Methods to a Record?

Yes! You can override methods, define instance methods, and even add nested classes.

?? Can Records Have Mutable Fields?

No. Fields are implicitly final, meaning once a record is created, its values cannot be modified.

?? Are Records Good for Every Use Case?

Records are best for data-holding classes (like DTOs) but not ideal for mutable objects or complex business logic.


Conclusion

Java Records are a powerful feature for simplifying immutable objects, eliminating unnecessary boilerplate, and improving pattern matching. If you're working with data-centric classes, DTOs, or API models, Records help make your code cleaner, safer, and more maintainable.

Beyond reducing verbosity, Records integrate quite well with pattern matching, enabling concise object decomposition. This eliminates the need for explicit casting, enhances readability, and allows safer, more expressive switch statements and instanceof conditions. By using pattern matching with records, you can write more declarative and less error-prone code, making complex conditionals more intuitive.

If you want immutable, self-contained objects that work efficiently with pattern matching, Records are an excellent choice! ??

Ewerton Lima

Backend Engineer | Kotlin | Java | Spring Boot | JUnit | Docker | AWS

4 天前

Reading this, I can't help but think how similar the intent is to Kotlin's Data Classes.

Daivid Sim?es

Senior QA Automation Engineer | SDET | Java | Selenium | Rest Assured | Robot Framework | Cypress | Appium

3 周

Very helpful

Eric Ferreira Schmiele

Senior Software Engineer | Java | Spring | AWS | Angular | React | Docker | Fullstack Developer

3 周

Awesome content

Kaique Perez

Fullstack Software Engineer | Node | Typescript | React | Next.js | AWS | Tailwind | Nest.js | TDD | Docker

3 周

Nice content and approach Bruno Monteiro Thanks for sharing!

要查看或添加评论,请登录

Bruno Monteiro的更多文章

社区洞察

其他会员也浏览了