1. The Hook (The "Byte-Sized" Intro)
- In a Nutshell: Records (Java 14+) are data carrier classes—compact syntax for immutable data. record Person(String name, int age) auto-generates: constructor, getters, equals(), hashCode(), toString(). Immutable (all fields final).
- Restrictions: Implicitly final, extends Record, can't extend other classes (can implement interfaces).
- Use for: DTOs, value objects, API responses.
- Replaces: Boilerplate POJOs (100 lines → 1 line!).
- Compact constructor: Custom validation.
- Benefits: Less code, readability, immutability guaranteed!
Think of shipping label. Traditional class = writing all info by hand (sender, receiver, weight, tracking#, date—tedious!). Record = pre-printed label template (just fill blanks, everything else automatic). Same info, 90% less work!
2. Conceptual Clarity (The "Simple" Tier)
💡 The Analogy
- Traditional Class: Building house from scratch (foundation, walls, roof, plumbing)
- Record: Prefab house (arrives complete, just place it)
- Components: The blueprints (what data record holds)
3. Technical Mastery (The "Deep Dive)
Record Auto-Generated Members
| Member | Generated Automatically |
|---|---|
| Constructor | Canonical constructor with all components |
| Getters | name(), age() (not getName()!) |
| equals() | Compares all components |
| hashCode() | Based on all components |
| toString() | Pretty print: Person[name=Alice, age=30] |
4. Interactive & Applied Code
java
// ❌ BEFORE RECORDS: Traditional POJO (50+ 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 getName() { return name; }
public int getAge() { return age; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof PersonOld)) return false;
PersonOld person = (PersonOld) o;
return age == person.age && name.equals(person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public String toString() {
return "PersonOld{name='" + name + "', age=" + age + '}';
}
}
// ✅ AFTER RECORDS: 1 line!
record Person(String name, int age) {}
public class RecordsDemo {
public static void main(String[] args) {
demonstrateBasics();
demonstrateValidation();
demonstrateCustomMethods();
demonstrateNested();
}
// Basic record usage
static void demonstrateBasics() {
System.out.println("=== BASIC RECORDS ===");
// Create record instance
Person alice = new Person("Alice", 30);
// Auto-generated getters (no 'get' prefix!)
System.out.println("Name: " + alice.name());
System.out.println("Age: " + alice.age());
// Auto-generated toString
System.out.println(alice); // Person[name=Alice, age=30]
// Auto-generated equals/hashCode
Person alice2 = new Person("Alice", 30);
System.out.println("Equal? " + alice.equals(alice2)); // true
// Immutable (no setters!)
// alice.name = "Bob"; // ❌ COMPILE ERROR: final field
}
// Compact constructor (validation)
record ValidatedPerson(String name, int age) {
// Compact constructor (no parameter list!)
public ValidatedPerson {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("Name required");
}
if (age < 0 || age > 150) {
throw new IllegalArgumentException("Invalid age");
}
// Auto-assignment happens after this block
}
}
static void demonstrateValidation() {
System.out.println("\n=== VALIDATION ===");
try {
var person = new ValidatedPerson("", 25);
} catch (IllegalArgumentException e) {
System.out.println("Error: " + e.getMessage());
}
var valid = new ValidatedPerson("Bob", 25);
System.out.println(valid);
}
// Custom methods
record Employee(String name, int salary, String dept) {
// Custom method
public boolean isHighEarner() {
return salary > 100000;
}
// Override accessor
@Override
public String name() {
return name.toUpperCase();
}
// Static method
public static Employee createIntern(String name) {
return new Employee(name, 50000, "Internship");
}
}
static void demonstrateCustomMethods() {
System.out.println("\n=== CUSTOM METHODS ===");
var emp = new Employee("alice", 120000, "Engineering");
System.out.println("Name: " + emp.name()); // ALICE (overridden!)
System.out.println("High earner? " + emp.isHighEarner());
var intern = Employee.createIntern("Bob");
System.out.println(intern);
}
// Nested records
record Address(String street, String city) {}
record PersonWithAddress(String name, Address address) {}
static void demonstrateNested() {
System.out.println("\n=== NESTED RECORDS ===");
var address = new Address("Main St", "NYC");
var person = new PersonWithAddress("Alice", address);
System.out.println(person.name());
System.out.println(person.address().city());
}
}
// Real-world example: API Response
record ApiResponse<T>(int status, String message, T data) {}
class ApiDemo {
public static void main(String[] args) {
var response = new ApiResponse<>(
200,
"Success",
List.of(new Person("Alice", 30), new Person("Bob", 25))
);
System.out.println("Status: " + response.status());
System.out.println("Data: " + response.data());
}
}
// Records can implement interfaces
interface Identifiable {
String getId();
}
record User(String id, String name) implements Identifiable {
@Override
public String getId() {
return id;
}
}5. The Comparison & Decision Layer
| Traditional Class | Record | Use When |
|---|---|---|
| Mutable | Immutable | Need immutability |
| 50+ lines | 1 line | Data carriers |
| Can extend classes | Cannot extend | DTOs, value objects |
| Custom getters/setters | Auto-generated | Simple data |
6. The "Interview Corner" (The Edge)
The "Killer" Interview Question: "Why don't record getters follow JavaBean convention (getName() vs name())?" Answer: Records emphasize they're data carriers, not beans!
java
record Person(String name, int age) {}
// ✅ Record style: name()
person.name(); // Direct, concise
// ❌ NOT: getName() (JavaBean style)
// Records aren't beans—no setters, properties are components!
// Benefit: Shorter, clearer they're immutable value objectsPro-Tips:
- Compact vs Canonical constructor:
java
// Compact (validation)
record Point(int x, int y) {
public Point { // No params!
if (x < 0 || y < 0) throw new IllegalArgumentException();
// Implicit: this.x = x; this.y = y;
}
}
// Canonical (transformation)
record Point(int x, int y) {
public Point(int x, int y) { // With params
this.x = Math.max(0, x); // Transform
this.y = Math.max(0, y);
}
}- Records vs classes:
java
// ✅ USE RECORD: Immutable data
record Point(int x, int y) {}
// ❌ USE CLASS: Mutable, complex behavior
class BankAccount {
private double balance;
public void deposit(double amount) { ... }
}