Introduced in Java 14 as a preview feature and standardized in Java 16, records provide a compact syntax for declaring classes whose main purpose is to carry immutable data. Records automatically generate much of the boilerplate code that traditional Java classes require, such as constructors, getters, equals()
, hashCode()
, and toString()
. This makes records ideal for modeling simple data aggregates with minimal fuss.
Declaring a record is straightforward using the record
keyword followed by the record name and a header listing its components (fields):
public record Point(int x, int y) { }
This single line defines a record named Point
with two components, x
and y
, both of type int
.
When you declare a record like the above, Java implicitly provides the following:
Point(int x, int y)
), initializing the fields.x()
and y()
), which return the respective values.equals(Object o)
and hashCode()
methods that consider all components, allowing meaningful equality checks and usage in hash-based collections.toString()
method that returns a string representation of the record including component names and values, e.g., Point[x=3, y=5]
.These generated methods ensure your record is a fully functional, immutable data carrier with minimal manual coding.
Here's an example of how to use the Point
record:
public class Main {
public static void main(String[] args) {
Point p1 = new Point(3, 5);
System.out.println(p1.x()); // Prints: 3
System.out.println(p1.y()); // Prints: 5
System.out.println(p1); // Prints: Point[x=3, y=5]
Point p2 = new Point(3, 5);
System.out.println(p1.equals(p2)); // Prints: true
}
}
Notice that to access the values, you call x()
and y()
, not getX()
or getY()
. This naming convention differs from traditional JavaBeans getters but is consistent across all records.
public class Main {
// Record declaration
public record Point(int x, int y) {}
public static void main(String[] args) {
Point p1 = new Point(3, 5);
System.out.println("x: " + p1.x()); // Accessor method
System.out.println("y: " + p1.y());
System.out.println("Point: " + p1); // toString()
Point p2 = new Point(3, 5);
System.out.println("p1 equals p2? " + p1.equals(p2)); // equals()
System.out.println("HashCode of p1: " + p1.hashCode()); // hashCode()
}
}
Records are implicitly final, which means you cannot extend a record class. Each component is also final and private under the hood, so once a record instance is created, its data cannot be changed.
This design enforces immutability, making records ideal for representing values or data transfer objects (DTOs) where thread safety and predictability are important.
Records represent a significant evolution in Java's approach to data modeling:
equals()
, hashCode()
, and toString()
. This streamlines development and reduces potential errors in repetitive code.While records simplify many cases, they are not a replacement for all classes:
In summary, records offer a powerful, concise way to define immutable data structures with built-in functionality, making your Java code cleaner, more maintainable, and expressive. This modern feature aligns Java with other languages that have had similar constructs for years, such as Kotlin's data classes or C#'s records.
Records in Java automatically generate a canonical constructor matching the record components, which assigns parameter values to fields. However, sometimes you need to add validation or transformation logic when creating instances. For this purpose, Java allows you to define a compact constructor inside a record.
A compact constructor is a concise way to add code to the canonical constructor without repeating the parameter list or field assignments. It uses the record's component names directly and automatically assigns parameters to fields unless you explicitly override the assignments.
public record Person(String name, int age) {
public Person {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("Name cannot be null or blank");
}
if (age < 0) {
throw new IllegalArgumentException("Age cannot be negative");
}
// Fields 'name' and 'age' are automatically assigned after this block
}
}
In this example:
public Person { ... }
.this.name = name; this.age = age;
because this happens implicitly after the constructor body unless overridden.A canonical constructor requires full parameter declaration and explicit field assignment:
public record Person(String name, int age) {
public Person(String name, int age) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("Name cannot be null or blank");
}
if (age < 0) {
throw new IllegalArgumentException("Age cannot be negative");
}
this.name = name;
this.age = age;
}
}
While this works, it is more verbose and repetitive, especially for records with many components.
public class Main {
public record Person(String name, int age) {
public Person {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("Name cannot be null or blank");
}
if (age < 0) {
throw new IllegalArgumentException("Age cannot be negative");
}
// Fields are assigned automatically
}
}
public static void main(String[] args) {
try {
Person p1 = new Person("Alice", 30);
System.out.println("Created person: " + p1);
Person p2 = new Person("", 25); // Triggers exception
System.out.println("Created person: " + p2);
} catch (IllegalArgumentException e) {
System.out.println("Validation failed: " + e.getMessage());
}
try {
Person p3 = new Person("Bob", -5); // Triggers exception
System.out.println("Created person: " + p3);
} catch (IllegalArgumentException e) {
System.out.println("Validation failed: " + e.getMessage());
}
}
}
Compact constructors keep records concise yet flexible by allowing you to insert validation, normalization, or other logic directly at the construction phase without sacrificing the brevity that makes records attractive. They promote clean and maintainable code, keeping the focus on data immutability and integrity.
With the introduction of records in Java 14, a new concise syntax for data-carrying classes has emerged. Records provide a lightweight way to model immutable data aggregates while reducing boilerplate code common in plain old Java objects (POJOs). This section compares records with traditional classes, highlighting their syntax differences, capabilities, and limitations, and reflects on when to prefer records over classes.
Traditional Class:
public 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;
}
@Override
public String toString() {
return "Person[name=" + name + ", age=" + age + "]";
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Person)) return false;
Person other = (Person) o;
return age == other.age && name.equals(other.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
Record:
public record Person(String name, int age) {}
Notice how a record declaration is dramatically more concise, automatically generating:
equals()
, hashCode()
, and toString()
implementationsprivate
and final
.equals()
, hashCode()
, and toString()
are generated.java.lang.Record
and cannot extend any other class.final
; you cannot create subclasses of a record.Records introduce a powerful tool for Java developers to write clearer, safer, and more maintainable code by focusing on immutable data and automatically handling common tasks like equality and string representation. They promote value-based design and reduce boilerplate, which leads to fewer errors and more concise code.
However, they do not replace traditional classes for all scenarios. The limitations around immutability, inheritance, and customization mean classes are still essential for more complex or mutable domain models.
In summary, use records when your class primarily models data with fixed state, and prefer classes when your design requires richer behavior or mutability. This thoughtful use of both enables you to write idiomatic, clean, and robust Java code.
Records are an elegant and concise way to group related data into a single, immutable object. This feature makes them ideal for data aggregationβcombining multiple values into one logical unit that can be passed around your application safely and clearly.
Imagine you are working on a program that processes points on a 2D plane. Traditionally, you might create a class like this:
public class Point {
private final double x;
private final double y;
public Point(double x, double y) {
this.x = x;
this.y = y;
}
public double getX() { return x; }
public double getY() { return y; }
@Override
public String toString() {
return "(" + x + ", " + y + ")";
}
}
With records, this can be simplified drastically:
public record Point(double x, double y) {}
This single line creates a fully immutable type with two components, x
and y
, and automatically generates constructor, accessor methods, equals()
, hashCode()
, and toString()
.
Records can make method signatures and implementations clearer by returning multiple values without needing to create separate classes manually:
public class Geometry {
public static Point midpoint(Point a, Point b) {
return new Point(
(a.x() + b.x()) / 2,
(a.y() + b.y()) / 2
);
}
}
Here, midpoint
returns a Point
record that clearly groups the two coordinates. Before records, you might have returned an array or a custom class, both less clear or more verbose.
Records also work well for aggregating more complex data, such as user profiles:
public record User(String username, String email, int age) {}
In this example, a User
record can be used throughout your application to represent user data consistently, safely, and without risk of accidental modification.
Records are especially powerful when used in collections like lists or maps:
List<User> users = List.of(
new User("alice", "alice@example.com", 30),
new User("bob", "bob@example.com", 25)
);
Map<String, Point> cityCoordinates = Map.of(
"New York", new Point(40.7128, -74.0060),
"Los Angeles", new Point(34.0522, -118.2437)
);
Because records provide reliable implementations of equals()
and hashCode()
, they work seamlessly as keys or values in collections without requiring extra code.
Records promote functional programming principles in Java by emphasizing immutability and value-based equality. Since record components are final
, you cannot change the state after construction, eliminating side effects and making your programs easier to reason about.
When paired with streams and lambda expressions, records enable concise, declarative code for data transformation and aggregation:
List<User> adults = users.stream()
.filter(user -> user.age() >= 18)
.collect(Collectors.toList());
This code filters a list of immutable User
records to those who are adults, leveraging the safety and clarity records provide.
import java.util.*;
import java.util.stream.Collectors;
public class Main {
public record Point(double x, double y) {}
public record User(String username, String email, int age) {}
public static class Geometry {
public static Point midpoint(Point a, Point b) {
return new Point(
(a.x() + b.x()) / 2,
(a.y() + b.y()) / 2
);
}
}
public static void main(String[] args) {
// Example 1: Returning a record from a method
Point p1 = new Point(2, 4);
Point p2 = new Point(6, 8);
Point mid = Geometry.midpoint(p1, p2);
System.out.println("Midpoint: " + mid);
// Example 2: Grouping data with a record
List<User> users = List.of(
new User("alice", "alice@example.com", 30),
new User("bob", "bob@example.com", 17),
new User("carol", "carol@example.com", 22)
);
System.out.println("\nAll users:");
users.forEach(System.out::println);
// Example 3: Using records in collections and functional style
List<User> adults = users.stream()
.filter(user -> user.age() >= 18)
.collect(Collectors.toList());
System.out.println("\nAdults:");
adults.forEach(System.out::println);
Map<String, Point> cityCoordinates = Map.of(
"New York", new Point(40.7128, -74.0060),
"Los Angeles", new Point(34.0522, -118.2437)
);
System.out.println("\nCity coordinates:");
cityCoordinates.forEach((city, point) ->
System.out.println(city + ": " + point));
}
}
Records fill an important gap in Java by providing a native, language-level construct optimized for grouping data. They reduce boilerplate, improve readability, and support immutability, all of which are hallmarks of modern software design.
Using records for data aggregation helps developers:
In summary, records provide a streamlined way to represent aggregates of related data in Java, perfectly suited to both simple and complex use cases. Their design aligns with contemporary programming styles that prioritize immutability, clarity, and expressive code, making them an essential tool in any modern Java programmer's toolkit.