What is the Record Class in Java 14/17?
Records are immutable data carriers that provide a concise way to create classes that are simple data aggregates. Introduced in Java 14 as preview, finalized in Java 16/17.
Record Basics
// Before Records (verbose - ~40 lines)
class PersonOld {
private final String name;
private final int age;
public PersonOld(String name, int age) {
this.name = name;
this.age = age;
}
public String name() { return name; }
public int age() { return age; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PersonOld person = (PersonOld) o;
return age == person.age && Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public String toString() {
return "PersonOld[name=" + name + ", age=" + age + "]";
}
}
// With Records (concise - 1 line!)
record Person(String name, int age) {}
// A record is equivalent to a final class with:
// - Private final fields for each component
// - Canonical constructor
// - Public accessor methods (name(), age())
// - equals(), hashCode(), toString()
public class RecordBasics {
public static void main(String[] args) {
Person person1 = new Person("Alice", 25);
Person person2 = new Person("Alice", 25);
Person person3 = new Person("Bob", 30);
System.out.println(person1); // Person[name=Alice, age=25]
System.out.println("Name: " + person1.name()); // Accessor (NOT getter)
System.out.println("Age: " + person1.age());
System.out.println("Equals: " + person1.equals(person2)); // true
System.out.println("Same object? " + (person1 == person2)); // false
System.out.println("HashCode: " + person1.hashCode());
// Records are final - cannot extend
// class ExtendedPerson extends Person { } // ERROR
// Records can implement interfaces
// record NamedPerson(String name) implements Serializable {}
// Records are immutable - cannot change fields
// person1.name = "Bob"; // ERROR - final fields
// person1.age = 30; // ERROR - final fields
// To "change" a record, create a new one with modified values
Person older = new Person(person1.name(), person1.age() + 1);
System.out.println("Older: " + older);
}
}
Customizing Records
import java.util.*;
// 1. Compact constructor (validation/normalization)
record Employee(String name, int id, double salary) {
// Compact constructor - automatically invoked before field assignment
public Employee {
// Validation
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("Name cannot be blank");
}
if (id <= 0) {
throw new IllegalArgumentException("ID must be positive");
}
if (salary < 0) {
throw new IllegalArgumentException("Salary cannot be negative");
}
// Normalization
name = name.trim();
if (salary == 0) {
salary = 30000; // Minimum salary
}
// Fields are automatically assigned
}
// 2. Additional constructor
public Employee(String name, int id) {
this(name, id, 30000); // Default salary
}
// 3. Custom methods
public double annualSalary() {
return salary * 12;
}
public String greeting() {
return "Hello, I'm " + name + " (ID: " + id + ")";
}
// 4. Override accessor (rare)
@Override
public String name() {
return name.toUpperCase();
}
// 5. Static fields and methods
static String company = "Tech Corp";
static void setCompany(String c) { company = c; }
}
// 2. Record with generics
record Pair(K key, V value) {
public Pair {
Objects.requireNonNull(key);
Objects.requireNonNull(value);
}
public Pair(K key) {
this(key, null);
}
@Override
public String toString() {
return key + "=" + value;
}
}
// 3. Record with annotations
record User(
@NotNull String username,
@Email String email,
@Min(18) int age
) {}
// 4. Local record (inside method)
public class RecordCustomization {
public static void main(String[] args) {
System.out.println("=== Record Customization ===
");
// Using custom constructors
Employee emp1 = new Employee(" Alice ", 101, 50000);
Employee emp2 = new Employee("Bob", 102); // Default salary
Employee emp3 = new Employee("Charlie", 103, 0); // Min salary applied
System.out.println(emp1);
System.out.println("Annual salary: $" + emp1.annualSalary());
System.out.println(emp1.greeting());
System.out.println("Name (overridden): " + emp1.name()); // UPPERCASE
System.out.println("
" + emp2);
System.out.println(emp3);
// Generic record
Pair pair1 = new Pair<>("Age", 25);
Pair pair2 = new Pair<>("Count"); // Value null
System.out.println("
" + pair1);
System.out.println(pair2);
// Local record (inside method)
record Point(double x, double y) {
double distanceFromOrigin() {
return Math.sqrt(x * x + y * y);
}
Point add(Point other) {
return new Point(x + other.x, y + other.y);
}
}
Point p1 = new Point(3, 4);
Point p2 = new Point(1, 2);
Point p3 = p1.add(p2);
System.out.println("
Point distance: " + p1.distanceFromOrigin());
System.out.println("p1 + p2 = " + p3);
// Records in collections
List users = List.of(
new User("john_doe", "john@example.com", 25),
new User("jane_smith", "jane@example.com", 30)
);
System.out.println("
Users:");
users.forEach(System.out::println);
}
}
Records vs Traditional Classes
import java.util.*;
import java.util.stream.*;
public class RecordsVsClasses {
// Traditional mutable class (not recommended for data)
static class MutablePerson {
private String name;
private int age;
MutablePerson(String name, int age) {
this.name = name;
this.age = age;
}
void setName(String name) { this.name = name; }
void setAge(int age) { this.age = age; }
String getName() { return name; }
int getAge() { return age; }
@Override
public String toString() {
return name + "(" + age + ")";
}
}
// Record (immutable by design)
record ImmutablePerson(String name, int age) {}
public static void main(String[] args) {
System.out.println("=== Records vs Traditional Classes ===
");
// Mutable - can be changed (may cause bugs)
MutablePerson mPerson = new MutablePerson("Alice", 25);
System.out.println("Mutable: " + mPerson);
mPerson.setName("Bob"); // Changed! Could be unintended
System.out.println("After mutation: " + mPerson);
// Immutable - cannot change
ImmutablePerson iPerson = new ImmutablePerson("Alice", 25);
System.out.println("
Immutable: " + iPerson);
// iPerson.name = "Bob"; // No such field - immutable
// To change, create new record
ImmutablePerson updated = new ImmutablePerson("Bob", iPerson.age());
System.out.println("Updated: " + updated);
// Use cases for records
System.out.println("
=== Use Cases for Records ===");
System.out.println("✓ Data Transfer Objects (DTOs)");
System.out.println("✓ Value objects");
System.out.println("✓ Keys in maps (immutable)");
System.out.println("✓ Multiple return values from methods");
System.out.println("✓ Tuple-like structures");
System.out.println("✓ Configuration objects");
System.out.println("✓ Event/Message objects");
// Map with record as key
System.out.println("
=== Record as Map Key ===");
Map employeeMap = new HashMap<>();
employeeMap.put(new ImmutablePerson("Alice", 25), "Developer");
employeeMap.put(new ImmutablePerson("Bob", 30), "Manager");
employeeMap.put(new ImmutablePerson("Charlie", 35), "Architect");
System.out.println("Alice: " + employeeMap.get(new ImmutablePerson("Alice", 25)));
// Multiple return values
System.out.println("
=== Multiple Return Values ===");
var result = divideAndRemainder(10, 3);
System.out.println("10 / 3 = " + result.quotient() + " remainder " + result.remainder());
// Stream operations with records
System.out.println("
=== Stream Operations ===");
List people = List.of(
new ImmutablePerson("Alice", 25),
new ImmutablePerson("Bob", 30),
new ImmutablePerson("Charlie", 25),
new ImmutablePerson("David", 35),
new ImmutablePerson("Eve", 28)
);
// Group by age
Map> byAge = people.stream()
.collect(Collectors.groupingBy(ImmutablePerson::age));
System.out.println("Group by age: " + byAge);
// Filter and map
List namesOver28 = people.stream()
.filter(p -> p.age() > 28)
.map(ImmutablePerson::name)
.collect(Collectors.toList());
System.out.println("Names over 28: " + namesOver28);
}
static DivResult divideAndRemainder(int a, int b) {
return new DivResult(a / b, a % b);
}
record DivResult(int quotient, int remainder) {}
}
Records in Advanced Scenarios
import java.io.*;
import java.time.*;
import java.util.stream.*;
// 1. Serializable Record
record SerializablePerson(String name, int age) implements Serializable {
// Records are automatically serializable if components are serializable
private static final long serialVersionUID = 1L;
}
// 2. Record with defensive copying for collections
record Order(String id, List items) {
// Defensive copy in compact constructor
public Order {
// Create immutable copy to prevent external modification
items = List.copyOf(items);
}
public List getItems() {
return items; // Already immutable
}
}
// 3. Record implementing interface
interface HasName {
String name();
String fullName();
}
record Student(String firstName, String lastName, int grade) implements HasName {
@Override
public String name() {
return firstName;
}
@Override
public String fullName() {
return firstName + " " + lastName;
}
}
// 4. Nested Records
record Company(String name, Address address) {
record Address(String street, String city, String zipCode, String country) {
public String fullAddress() {
return street + ", " + city + ", " + zipCode + ", " + country;
}
}
}
// 5. Record with LocalDate
record Event(String name, LocalDate date, EventType type) {
enum EventType { CONFERENCE, WORKSHOP, MEETUP }
public boolean isUpcoming() {
return date.isAfter(LocalDate.now());
}
}
public class AdvancedRecords {
public static void main(String[] args) throws IOException, ClassNotFoundException {
System.out.println("=== Advanced Record Usage ===
");
// 1. Serialization
System.out.println("1. Record Serialization:");
SerializablePerson person = new SerializablePerson("Alice", 25);
// Serialize
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(person);
// Deserialize
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
SerializablePerson deserialized = (SerializablePerson) ois.readObject();
System.out.println(" Deserialized: " + deserialized);
System.out.println(" Same object? " + (person.equals(deserialized)));
// 2. Defensive copying
System.out.println("
2. Defensive Copying:");
List items = new ArrayList<>();
items.add("Item1");
items.add("Item2");
Order order = new Order("ORD-001", items);
System.out.println(" Order: " + order);
// Trying to modify original list doesn't affect the record
items.add("Item3");
System.out.println(" Modified original list: " + items);
System.out.println(" Order unchanged: " + order);
// 3. Interface implementation
System.out.println("
3. Interface Implementation:");
Student student = new Student("John", "Doe", 12);
System.out.println(" Name: " + student.name());
System.out.println(" Full name: " + student.fullName());
// 4. Nested records
System.out.println("
4. Nested Records:");
Company.Address address = new Company.Address(
"123 Main St", "New York", "10001", "USA"
);
Company company = new Company("Tech Corp", address);
System.out.println(" Company: " + company);
System.out.println(" Full address: " + company.address().fullAddress());
// 5. Pattern matching with records (Java 17+ preview)
System.out.println("
5. Pattern Matching (requires preview):");
Object obj = new Student("Alice", "Smith", 10);
// Traditional instanceof
if (obj instanceof Student) {
Student s = (Student) obj;
System.out.println(" Traditional: " + s.firstName());
}
// Pattern matching (Java 16+)
// if (obj instanceof Student(String firstName, String lastName, int grade)) {
// System.out.println(" Pattern matching: " + firstName);
// }
// 6. Records in streams
System.out.println("
6. Records in Streams:");
List events = List.of(
new Event("Java Conference", LocalDate.of(2024, 6, 15), Event.EventType.CONFERENCE),
new Event("Spring Workshop", LocalDate.of(2024, 7, 20), Event.EventType.WORKSHOP),
new Event("Meetup", LocalDate.of(2024, 8, 10), Event.EventType.MEETUP)
);
events.stream()
.filter(Event::isUpcoming)
.forEach(e -> System.out.println(" Upcoming: " + e));
}
}
When to Use Records - Best Practices
public class RecordsBestPractices {
// GOOD: Simple data carrier
record Point(int x, int y) {}
// GOOD: Value object
record Money(String currency, BigDecimal amount) {
public Money add(Money other) {
if (!currency.equals(other.currency)) {
throw new IllegalArgumentException("Currency mismatch");
}
return new Money(currency, amount.add(other.amount));
}
}
// GOOD: DTO for API
record UserDTO(Long id, String name, String email, LocalDateTime createdAt) {}
// GOOD: Key in cache
record CacheKey(String userId, String resourceType) {}
// BAD: Complex behavior (use class instead)
// record BankAccount(String number, double balance) {
// void transfer(double amount) { ... } // Behavior in record - not ideal
// }
// GOOD: Simple operations are fine
record Temperature(double celsius) {
public double toFahrenheit() {
return celsius * 9/5 + 32;
}
public double toKelvin() {
return celsius + 273.15;
}
}
// BAD: Mutable component without defensive copying
// record Cache(String key, List values) {
// // List is mutable if not copied
// }
// GOOD: Proper defensive copying
record SafeCache(String key, List values) {
public SafeCache {
values = List.copyOf(values); // Immutable copy
}
}
// BAD: Extending record (impossible)
// class MyPoint extends Point { } // ERROR - records are final
// GOOD: Records as keys in maps
record GeoLocation(double lat, double lon) {
public GeoLocation {
if (lat < -90 || lat > 90) throw new IllegalArgumentException("Invalid latitude");
if (lon < -180 || lon > 180) throw new IllegalArgumentException("Invalid longitude");
}
}
public static void main(String[] args) {
System.out.println("=== When to Use Records - Best Practices ===
");
System.out.println("IDEAL USE CASES:");
System.out.println("✓ Data transfer between layers (DTOs)");
System.out.println("✓ Temporary grouping of values");
System.out.println("✓ Multiple return values from methods");
System.out.println("✓ Keys in maps (immutable + proper equals/hashCode)");
System.out.println("✓ Value objects in domain modeling");
System.out.println("✓ Configuration/property objects");
System.out.println("✓ Event/message objects");
System.out.println("✓ Result objects from computations");
System.out.println("
NOT IDEAL FOR:");
System.out.println("✗ Objects with complex state that changes");
System.out.println("✗ Objects that need to be mutated frequently");
System.out.println("✗ JPA entities (requires mutable fields)");
System.out.println("✗ Large inheritance hierarchies (records are final)");
System.out.println("✗ Objects with many fields (records are best for 2-5 components)");
System.out.println("✗ When you need lazy initialization");
System.out.println("✗ When you need to control serialization format");
System.out.println("
=== Performance Considerations ===");
System.out.println("Records are generally as fast as regular classes");
System.out.println("- Field access: same performance as final fields");
System.out.println("- equals/hashCode: optimized by compiler");
System.out.println("- Memory: similar to regular objects");
// Example usage
System.out.println("
=== Example: Temperature Conversion ===");
Temperature temp = new Temperature(25.0);
System.out.println("25°C = " + temp.toFahrenheit() + "°F");
System.out.println("25°C = " + temp.toKelvin() + "K");
System.out.println("
=== Example: Money Operations ===");
Money m1 = new Money("USD", new BigDecimal("10.50"));
Money m2 = new Money("USD", new BigDecimal("5.25"));
Money total = m1.add(m2);
System.out.println(m1.amount() + " + " + m2.amount() + " = " + total.amount());
System.out.println("
=== Example: GeoLocation Validation ===");
try {
GeoLocation loc = new GeoLocation(100, 200); // Invalid
} catch (IllegalArgumentException e) {
System.out.println("Validation works: " + e.getMessage());
}
}
}
Master Java Records with Online Learner!
0
likes
Your Feedback
Help us improve by sharing your thoughts
Online Learner helps developers master programming, database concepts, interview preparation, and real-world implementation through structured learning paths.
Quick Links
© 2023 - 2026 OnlineLearner.in | All Rights Reserved.
