MacMusic  |  PcMusic  |  440 Software  |  440 Forums  |  440TV  |  Zicos
records
Search

Introduction to Java records: Simplified data-centric programming in Java

Thursday September 25, 2025. 11:00 AM , from InfoWorld
Records in Java are a newer kind of class for holding data. Instead of writing boilerplate code for constructors, accessors, equals(), hashCode(), and toString(), you just declare the fields and let the Java compiler handle the rest. This article introduces you to Java records, including examples of basic and advanced use cases and a few programming scenarios where you should not use them.

Note: Java records were finalized in JDK 16.

How the Java compiler handles record classes

Creating simple data classes in Java traditionally required substantial boilerplate code. Consider how we would represent Java’s mascots, Duke and Juggy:

public class JavaMascot {
private final String name;
private final int yearCreated;

public JavaMascot(String name, int yearCreated) {
this.name = name;
this.yearCreated = yearCreated;
}

public String getName() { return name; }
public int getYearCreated() { return yearCreated; }

// equals, hashCode and toString methods omitted for brevity
}

With records, we can reduce the above code to a single line:

public record JavaMascot(String name, int yearCreated) {}

This concise declaration automatically provides private final fields, a constructor, accessor methods, and properly implemented equals(), hashCode(), and toString() methods.

Now that we’ve defined the JavaMascot record, we can put it to work:

public class RecordExample {
public static void main(String[] args) {
JavaMascot duke = new JavaMascot('Duke', 1996);
JavaMascot juggy1 = new JavaMascot('Juggy', 2005);
JavaMascot juggy2 = new JavaMascot('Juggy', 2005);

System.out.println(duke); // JavaMascot[name=Duke, yearCreated=1996]
System.out.println(juggy1.equals(juggy2)); // true
System.out.println(duke.equals(juggy1)); // false
System.out.println('Mascot name: ' + duke.name());
System.out.println('Created in: ' + duke.yearCreated());
}
}

Records automatically provide meaningful string representation, value-based equality comparison, and simple accessor methods that match component names.

Customizing records

While records are concise by design, you can still enhance them with custom behavior. Consider the following examples.

Compact constructors

Records provide a special “compact constructor” syntax that lets you validate or transform input parameters without repeating the parameter list:

record JavaMascot(String name, int yearCreated) {
// Compact constructor with validation
public JavaMascot {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException('Name cannot be empty');
}
if (yearCreated < 1995) {
throw new IllegalArgumentException('Java mascots didn't exist before 1995');
}
}
}

The compact constructor runs after the fields are initialized but before the object is fully constructed, making it ideal for validation. In this example, the parameter declarations are omitted, yet implicitly available within the constructor.

Adding methods

We can also add methods to records:

record JavaMascot(String name, int yearCreated) {
public boolean isOriginalMascot() {
return name.equals('Duke');
}

public int yearsActive() {
return java.time.Year.now().getValue() - yearCreated;
}
}

Methods let records encapsulate behavior related to their data while keeping syntax concise and immutable.

Now let’s look at some more advanced ways to use Java records.

Pattern matching with instanceof and switch

Records became a crucial aspect of pattern matching in Java 21, with support in switch expressions, destructuring of components, nested patterns, and guard conditions.

When paired with the enhanced instanceof operator, records let you concisely extract components during type validation:

record Person(String name, int age) {}

if (obj instanceof Person person) {
System.out.println('Name: ' + person.name());
}

Now let’s consider a more traditional example. Geometric shapes are a classic way to demonstrate how sealed interfaces work with records, and they make pattern matching especially clear. The elegance of this combination is evident in switch expressions (introduced in Java 17), which let you write concise, type‑safe code that resembles algebraic data types in functional languages:

sealed interface Shape permits Rectangle, Circle, Triangle {}

record Rectangle(double width, double height) implements Shape {}
record Circle(double radius) implements Shape {}
record Triangle(double base, double height) implements Shape {}

public class RecordPatternMatchingExample {
public static void main(String[] args) {
Shape shape = new Circle(5);

// Expressive, type-safe pattern matching
double area = switch (shape) {
case Rectangle r -> r.width() * r.height();
case Circle c -> Math.PI * c.radius() * c.radius();
case Triangle t -> t.base() * t.height() / 2;
};

System.out.println('Area = ' + area);
}
}

Here, the Shape type is a sealed interface, permitting only Rectangle, Circle, and Triangle. Because this set is closed, the switch is exhaustive and requires no default branch.

Pattern matching in Java
To further explore Java records with pattern matching, see my recent tutorial, Basic and advanced pattern matching in Java.

Using records as data transfer objects

Records excel as data transfer objects (DTOs) in modern API designs such as REST, GraphQL, gRPC, or inter‑service communication. Their concise syntax and built‑in equality make records ideal for mapping between service layers. Here’s an example:

record UserDTO(String username, String email, Set roles) {}
record OrderDTO(UUID id, UserDTO user, List items, BigDecimal total) {}

DTOs are everywhere in microservices applications. Using a record makes them more robust thanks to immutability, and cleaner since you don’t have to write constructors, getters, or methods like equals() and hashCode().

Records in functional and concurrent programming

Records complement functional and concurrent programming as immutable data containers. They work well as return types from pure functions, within stream pipelines, and for safely sharing data across threads.

Since fields are final and immutable, records avoid a whole class of threading issues. Once constructed, their state cannot change, so they’re thread‑safe without defensive copying or synchronization. Consider this example:

transactions.parallelStream().mapToDouble(Transaction::amount).sum();

Because records are immutable, this parallel computation is inherently thread‑safe.

When you shouldn’t use Java records

So far, we’ve seen where records shine, but they aren’t a universal replacement. As one example, every record implicitly extends java.lang.Record, so records can’t extend any other class (though they can implement interfaces). Records don’t fit in scenarios where class inheritance is required.

Let’s consider some other situations where Java records fall short.

Records are immutable by design

Record components are always final, so they don’t fit in programs that require mutable/stateful objects. The following example shows a mutable class that relies on changing state, which records don’t allow:

public class GameCharacter {
private int health;
private Position position;

public void takeDamage(int amount) {
this.health = Math.max(0, this.health - amount);
}

public void move(int x, int y) {
this.position = new Position(this.position.x() + x, this.position.y() + y);
}
}

They don’t model complex behavior

Designs centered on mutable state, heavy business logic, or patterns like strategy, visitor, or observer will be better served using traditional classes. Here’s an example of complex logic that doesn’t suit a record:

public class TaxCalculator {
private final TaxRateProvider rateProvider;
private final DeductionRegistry deductions;

public TaxAssessment calculateTax(Income income, Residence residence) {
// Complex logic that doesn’t suit a record
}
}

They are incompatible with some frameworks

Some frameworks, especially ORMs, may not handle records well. Serialization or reflection‑heavy tools can also have issues. Always check the compatibility of Java features with your tech stack:

// May not work well with some ORM frameworks
record Employee(Long id, String name, Department department) {}

// Instead, you might need a traditional entity class
@Entity
public class Employee {
@Id
@GeneratedValue
private Long id;
private String name;
@ManyToOne
private Department department;

// Getters, setters, equals, hashCode, etc.
}

These caveats don’t mean records are incomplete; they simply highlight that records are designed for specific roles. In some cases, traditional classes are more practicaladitional classes are the more practical fit.

Records and serialization in Java

Records have been widely adopted across the Java ecosystem, and their immutability makes them appealing for persistence, configuration, and data transfers. A record can implement the Serializable interface like any other class. Serializable record components are a natural fit for use cases like saving configuration, restoring state, sending data across the network, or caching values.

Because record fields are final and immutable, they help you avoid issues that can arise when mutable state changes between serialization and deserialization. For example:

import java.io.Serializable;

record User(String username, int age, Profile profile) implements Serializable {}

class Profile {
private String bio;
}

Here, String and int are fine, but Profile is not serializable, which means User cannot be serialized. If you update Profile to also implement Serializable, User will then be fully serializable:

class Profile implements Serializable {
private String bio;
}

Beyond serialization basics, Java ecosystem support for records has matured quickly. Popular frameworks like Spring Boot, Quarkus, and Jackson all work seamlessly with records, as do most testing tools.

Thanks to this adoption, records excel as DTOs in real‑world APIs:

@RestController
@RequestMapping('/api/orders')
public class OrderController {

@GetMapping('/{id}')
public OrderView getOrder(@PathVariable UUID id) {
// In a real app, this would come from a database or service
return new OrderView(
id,
'Duke',
List.of(new ItemView(UUID.randomUUID(), 2)),
new BigDecimal('149.99')
);
}

// Record DTOs for API response
record OrderView(UUID id, String customerName, List items, BigDecimal total) {}
record ItemView(UUID productId, int quantity) {}
}

Today, most major Java libraries and tools recognize records as first‑class citizens. Early skepticism has largely disappeared, with developers embracing them for their clarity and safety.

Conclusion

Records represent a fundamental advancement in Java’s evolution. They reduce the verbosity of data classes and guarantee immutability and consistent behavior. By eliminating boilerplate for constructors, accessors, and methods like equals() and hashCode(), records make code cleaner, more expressive, and aligned with modern practices while preserving type safety.

They’re not right for every situation, but for immutable data they shine. Combined with pattern matching, they let your code express intent clearly while the Java compiler handles the boilerplate.

With advancements like records, sealed classes, and pattern matching, Java is steadily moving toward a more data‑centric style. Learning to use these tools is one of the clearest ways to write modern, expressive Java.
https://www.infoworld.com/article/4058874/introduction-to-java-records-simplified-data-centric-progr...

Related News

News copyright owned by their original publishers | Copyright © 2004 - 2025 Zicos / 440Network
Current Date
Sep, Thu 25 - 19:05 CEST