Modern Java Types
Records
Concise Data Classes
You need a Point class with x and y. Writing constructor, getters, equals,
hashCode, and toString is tedious. Records do all this in one line:
record Point(int x, int y) {}. Immutable data carriers with no boilerplate.
Basic record
Define a data class in one line.
// Basic record definition and usage
// Concept: record - immutable data carrier
// Define a record
record Point(int x, int y) {
}
public class BasicRecord {
public static void main(String[] args) {
// Create record instance
int pointX = ;
Point p1 = new Point(pointX, 20);
// Access fields via accessor methods
System.out.println("x: " + p1.x());
System.out.println("y: " + p1.y());
// Automatic toString()
System.out.println("Point: " + p1);
// Create another instance
Point p2 = new Point(10, 20);
Point p3 = new Point(15, 25);
// Automatic equals()
System.out.println("\np1 equals p2: " + p1.equals(p2));
System.out.println("p1 equals p3: " + p1.equals(p3));
// Use == for reference comparison
System.out.println("p1 == p2: " + (p1 == p2));
// More record examples
record Person(String name, int age) {}
Person alice = new Person("Alice", 30);
System.out.println("\nPerson: " + alice);
System.out.println("Name: " + alice.name());
System.out.println("Age: " + alice.age());
}
}
// Basic record definition and usage
// Concept: record - immutable data carrier
// Define a record
record Point(int x, int y) {
}
public class BasicRecord {
public static void main(String[] args) {
// Create record instance
int pointX = ;
Point p1 = new Point(pointX, 20);
// Access fields via accessor methods
System.out.println("x: " + p1.x());
System.out.println("y: " + p1.y());
// Automatic toString()
System.out.println("Point: " + p1);
// Create another instance
Point p2 = new Point(10, 20);
Point p3 = new Point(15, 25);
// Automatic equals()
System.out.println("\np1 equals p2: " + p1.equals(p2));
System.out.println("p1 equals p3: " + p1.equals(p3));
// Use == for reference comparison
System.out.println("p1 == p2: " + (p1 == p2));
// More record examples
record Person(String name, int age) {}
Person alice = new Person("Alice", 30);
System.out.println("\nPerson: " + alice);
System.out.println("Name: " + alice.name());
System.out.println("Age: " + alice.age());
}
}
// Basic record definition and usage
// Concept: record - immutable data carrier
// Define a record
record Point(int x, int y) {
}
public class BasicRecord {
public static void main(String[] args) {
// Create record instance
int pointX = ;
Point p1 = new Point(pointX, 20);
// Access fields via accessor methods
System.out.println("x: " + p1.x());
System.out.println("y: " + p1.y());
// Automatic toString()
System.out.println("Point: " + p1);
// Create another instance
Point p2 = new Point(10, 20);
Point p3 = new Point(15, 25);
// Automatic equals()
System.out.println("\np1 equals p2: " + p1.equals(p2));
System.out.println("p1 equals p3: " + p1.equals(p3));
// Use == for reference comparison
System.out.println("p1 == p2: " + (p1 == p2));
// More record examples
record Person(String name, int age) {}
Person alice = new Person("Alice", 30);
System.out.println("\nPerson: " + alice);
System.out.println("Name: " + alice.name());
System.out.println("Age: " + alice.age());
}
}
record Name(Type field1, Type field2) {} - complete data class.
Automatic methods
What records give you for free.
// Automatic methods in records
// Concept: automatic methods - equals, hashCode, toString
record Book(String title, String author, int year) {
}
public class AutomaticMethods {
public static void main(String[] args) {
// Create book instances
Book b1 = new Book("1984", "Orwell", 1949);
Book b2 = new Book("1984", "Orwell", 1949);
Book b3 = new Book("Dune", "Herbert", 1965);
// toString() - readable representation
System.out.println("Book 1: " + b1);
System.out.println("Book 2: " + b2);
System.out.println("Book 3: " + b3);
// equals() - value-based equality
System.out.println("\nb1.equals(b2): " + b1.equals(b2));
System.out.println("b1.equals(b3): " + b1.equals(b3));
// hashCode() - consistent with equals
System.out.println("\nHash codes:");
System.out.println("b1: " + b1.hashCode());
System.out.println("b2: " + b2.hashCode());
System.out.println("b3: " + b3.hashCode());
System.out.println("\nSame hash? " + (b1.hashCode() == b2.hashCode()));
// Use in collections
java.util.Set<Book> books = new java.util.HashSet<>();
books.add(b1);
books.add(b2); // Won't add (equals b1)
books.add(b3);
System.out.println("\nUnique books in set: " + books.size());
// Use as map key
java.util.Map<Book, Integer> stock = new java.util.HashMap<>();
stock.put(b1, 10);
stock.put(b2, 5); // Updates b1's value (same key)
stock.put(b3, 7);
System.out.println("\nStock levels:");
for (var entry : stock.entrySet()) {
System.out.println(" " + entry.getKey().title() + ": " + entry.getValue());
}
// Compare different types
record Movie(String title, String director, int year) {}
Movie m1 = new Movie("1984", "Radford", 1984);
// Different types, even with same values
System.out.println("\nBook equals Movie: " + b1.equals(m1));
}
}
Accessor methods: point.x(). Plus equals, hashCode, toString.
Compact constructor
Validate fields in a compact constructor.
// Compact constructor for validation
// Concept: compact constructor - validate record creation
record Temperature(double celsius) {
// Compact constructor for validation
public Temperature {
if (celsius < -273.15) {
throw new IllegalArgumentException(
"Temperature below absolute zero: " + celsius);
}
}
// Add computed methods
public double fahrenheit() {
return celsius * 9.0 / 5.0 + 32;
}
public double kelvin() {
return celsius + 273.15;
}
}
record Range(int min, int max) {
// Compact constructor with normalization
public Range {
if (min > max) {
// Swap if needed
int temp = min;
min = max;
max = temp;
}
}
public boolean contains(int value) {
return value >= min && value <= max;
}
public int size() {
return max - min + 1;
}
}
public class CompactConstructor {
public static void main(String[] args) {
// Create temperature with validation
Temperature t1 = ;
System.out.println("Temperature: " + t1.celsius() + "°C");
System.out.println("Fahrenheit: " + t1.fahrenheit() + "°F");
System.out.println("Kelvin: " + t1.kelvin() + "K");
// Validation prevents invalid state
try {
Temperature invalid = new Temperature(-300);
System.out.println("Created: " + invalid);
} catch (IllegalArgumentException e) {
System.out.println("\nError: " + e.getMessage());
}
// Range with auto-correction
Range r1 = new Range(1, 10);
Range r2 = new Range(10, 1); // Will be swapped
System.out.println("\nr1: " + r1);
System.out.println("r2: " + r2);
System.out.println("r1 equals r2: " + r1.equals(r2));
// Use range methods
Range range = new Range(5, 15);
System.out.println("\nRange: " + range);
System.out.println("Size: " + range.size());
System.out.println("Contains 10: " + range.contains(10));
System.out.println("Contains 20: " + range.contains(20));
// Multiple validations
record Email(String address) {
public Email {
if (address == null || address.isEmpty()) {
throw new IllegalArgumentException("Email cannot be empty");
}
if (!address.contains("@")) {
throw new IllegalArgumentException("Invalid email format");
}
// Normalize to lowercase
address = address.toLowerCase();
}
}
Email e1 = new Email("Alice@Example.COM");
Email e2 = new Email("alice@example.com");
System.out.println("\ne1: " + e1.address());
System.out.println("e2: " + e2.address());
System.out.println("Equal: " + e1.equals(e2));
}
}
// Compact constructor for validation
// Concept: compact constructor - validate record creation
record Temperature(double celsius) {
// Compact constructor for validation
public Temperature {
if (celsius < -273.15) {
throw new IllegalArgumentException(
"Temperature below absolute zero: " + celsius);
}
}
// Add computed methods
public double fahrenheit() {
return celsius * 9.0 / 5.0 + 32;
}
public double kelvin() {
return celsius + 273.15;
}
}
record Range(int min, int max) {
// Compact constructor with normalization
public Range {
if (min > max) {
// Swap if needed
int temp = min;
min = max;
max = temp;
}
}
public boolean contains(int value) {
return value >= min && value <= max;
}
public int size() {
return max - min + 1;
}
}
public class CompactConstructor {
public static void main(String[] args) {
// Create temperature with validation
Temperature t1 = ;
System.out.println("Temperature: " + t1.celsius() + "°C");
System.out.println("Fahrenheit: " + t1.fahrenheit() + "°F");
System.out.println("Kelvin: " + t1.kelvin() + "K");
// Validation prevents invalid state
try {
Temperature invalid = new Temperature(-300);
System.out.println("Created: " + invalid);
} catch (IllegalArgumentException e) {
System.out.println("\nError: " + e.getMessage());
}
// Range with auto-correction
Range r1 = new Range(1, 10);
Range r2 = new Range(10, 1); // Will be swapped
System.out.println("\nr1: " + r1);
System.out.println("r2: " + r2);
System.out.println("r1 equals r2: " + r1.equals(r2));
// Use range methods
Range range = new Range(5, 15);
System.out.println("\nRange: " + range);
System.out.println("Size: " + range.size());
System.out.println("Contains 10: " + range.contains(10));
System.out.println("Contains 20: " + range.contains(20));
// Multiple validations
record Email(String address) {
public Email {
if (address == null || address.isEmpty()) {
throw new IllegalArgumentException("Email cannot be empty");
}
if (!address.contains("@")) {
throw new IllegalArgumentException("Invalid email format");
}
// Normalize to lowercase
address = address.toLowerCase();
}
}
Email e1 = new Email("Alice@Example.COM");
Email e2 = new Email("alice@example.com");
System.out.println("\ne1: " + e1.address());
System.out.println("e2: " + e2.address());
System.out.println("Equal: " + e1.equals(e2));
}
}
// Compact constructor for validation
// Concept: compact constructor - validate record creation
record Temperature(double celsius) {
// Compact constructor for validation
public Temperature {
if (celsius < -273.15) {
throw new IllegalArgumentException(
"Temperature below absolute zero: " + celsius);
}
}
// Add computed methods
public double fahrenheit() {
return celsius * 9.0 / 5.0 + 32;
}
public double kelvin() {
return celsius + 273.15;
}
}
record Range(int min, int max) {
// Compact constructor with normalization
public Range {
if (min > max) {
// Swap if needed
int temp = min;
min = max;
max = temp;
}
}
public boolean contains(int value) {
return value >= min && value <= max;
}
public int size() {
return max - min + 1;
}
}
public class CompactConstructor {
public static void main(String[] args) {
// Create temperature with validation
Temperature t1 = ;
System.out.println("Temperature: " + t1.celsius() + "°C");
System.out.println("Fahrenheit: " + t1.fahrenheit() + "°F");
System.out.println("Kelvin: " + t1.kelvin() + "K");
// Validation prevents invalid state
try {
Temperature invalid = new Temperature(-300);
System.out.println("Created: " + invalid);
} catch (IllegalArgumentException e) {
System.out.println("\nError: " + e.getMessage());
}
// Range with auto-correction
Range r1 = new Range(1, 10);
Range r2 = new Range(10, 1); // Will be swapped
System.out.println("\nr1: " + r1);
System.out.println("r2: " + r2);
System.out.println("r1 equals r2: " + r1.equals(r2));
// Use range methods
Range range = new Range(5, 15);
System.out.println("\nRange: " + range);
System.out.println("Size: " + range.size());
System.out.println("Contains 10: " + range.contains(10));
System.out.println("Contains 20: " + range.contains(20));
// Multiple validations
record Email(String address) {
public Email {
if (address == null || address.isEmpty()) {
throw new IllegalArgumentException("Email cannot be empty");
}
if (!address.contains("@")) {
throw new IllegalArgumentException("Invalid email format");
}
// Normalize to lowercase
address = address.toLowerCase();
}
}
Email e1 = new Email("Alice@Example.COM");
Email e2 = new Email("alice@example.com");
System.out.println("\ne1: " + e1.address());
System.out.println("e2: " + e2.address());
System.out.println("Equal: " + e1.equals(e2));
}
}
Constructor without parameters - validates/normalizes before assignment.
Custom methods
Add your own methods to records.
// Custom methods in records
// Concept: custom methods - add behavior to records
record Circle(double radius) {
// Add custom methods
public double area() {
return Math.PI * radius * radius;
}
public double circumference() {
return 2 * Math.PI * radius;
}
public double diameter() {
return 2 * radius;
}
// Static factory method
public static Circle fromDiameter(double diameter) {
return new Circle(diameter / 2);
}
}
record Money(double amount, String currency) {
// Methods with logic
public Money add(Money other) {
if (!currency.equals(other.currency)) {
throw new IllegalArgumentException("Currency mismatch");
}
return new Money(amount + other.amount, currency);
}
public Money multiply(double factor) {
return new Money(amount * factor, currency);
}
public String formatted() {
return String.format("%.2f %s", amount, currency);
}
}
public class CustomMethods {
public static void main(String[] args) {
// Use custom methods
Circle c1 = ;
System.out.println("Circle: " + c1);
System.out.println("Radius: " + c1.radius());
System.out.printf("Area: %.2f%n", c1.area());
System.out.printf("Circumference: %.2f%n", c1.circumference());
System.out.printf("Diameter: %.2f%n", c1.diameter());
// Use static factory
Circle c2 = Circle.fromDiameter(20);
System.out.println("\nFrom diameter 20: " + c2);
System.out.printf("Radius: %.1f%n", c2.radius());
// Money operations
Money price1 = new Money(10.50, "USD");
Money price2 = new Money(5.25, "USD");
System.out.println("\nPrice 1: " + price1.formatted());
System.out.println("Price 2: " + price2.formatted());
Money total = price1.add(price2);
System.out.println("Total: " + total.formatted());
Money doubled = price1.multiply(2);
System.out.println("Doubled: " + doubled.formatted());
// Currency mismatch
try {
Money euros = new Money(10, "EUR");
Money combined = price1.add(euros);
System.out.println("Combined: " + combined);
} catch (IllegalArgumentException e) {
System.out.println("\nError: " + e.getMessage());
}
// Complex record with methods
record Rectangle(double width, double height) {
public double area() {
return width * height;
}
public double perimeter() {
return 2 * (width + height);
}
public boolean isSquare() {
return width == height;
}
public Rectangle scale(double factor) {
return new Rectangle(width * factor, height * factor);
}
}
Rectangle rect = new Rectangle(10, 5);
System.out.println("\nRectangle: " + rect);
System.out.printf("Area: %.1f%n", rect.area());
System.out.printf("Perimeter: %.1f%n", rect.perimeter());
System.out.println("Is square: " + rect.isSquare());
Rectangle scaled = rect.scale(2);
System.out.println("Scaled 2x: " + scaled);
}
}
// Custom methods in records
// Concept: custom methods - add behavior to records
record Circle(double radius) {
// Add custom methods
public double area() {
return Math.PI * radius * radius;
}
public double circumference() {
return 2 * Math.PI * radius;
}
public double diameter() {
return 2 * radius;
}
// Static factory method
public static Circle fromDiameter(double diameter) {
return new Circle(diameter / 2);
}
}
record Money(double amount, String currency) {
// Methods with logic
public Money add(Money other) {
if (!currency.equals(other.currency)) {
throw new IllegalArgumentException("Currency mismatch");
}
return new Money(amount + other.amount, currency);
}
public Money multiply(double factor) {
return new Money(amount * factor, currency);
}
public String formatted() {
return String.format("%.2f %s", amount, currency);
}
}
public class CustomMethods {
public static void main(String[] args) {
// Use custom methods
Circle c1 = ;
System.out.println("Circle: " + c1);
System.out.println("Radius: " + c1.radius());
System.out.printf("Area: %.2f%n", c1.area());
System.out.printf("Circumference: %.2f%n", c1.circumference());
System.out.printf("Diameter: %.2f%n", c1.diameter());
// Use static factory
Circle c2 = Circle.fromDiameter(20);
System.out.println("\nFrom diameter 20: " + c2);
System.out.printf("Radius: %.1f%n", c2.radius());
// Money operations
Money price1 = new Money(10.50, "USD");
Money price2 = new Money(5.25, "USD");
System.out.println("\nPrice 1: " + price1.formatted());
System.out.println("Price 2: " + price2.formatted());
Money total = price1.add(price2);
System.out.println("Total: " + total.formatted());
Money doubled = price1.multiply(2);
System.out.println("Doubled: " + doubled.formatted());
// Currency mismatch
try {
Money euros = new Money(10, "EUR");
Money combined = price1.add(euros);
System.out.println("Combined: " + combined);
} catch (IllegalArgumentException e) {
System.out.println("\nError: " + e.getMessage());
}
// Complex record with methods
record Rectangle(double width, double height) {
public double area() {
return width * height;
}
public double perimeter() {
return 2 * (width + height);
}
public boolean isSquare() {
return width == height;
}
public Rectangle scale(double factor) {
return new Rectangle(width * factor, height * factor);
}
}
Rectangle rect = new Rectangle(10, 5);
System.out.println("\nRectangle: " + rect);
System.out.printf("Area: %.1f%n", rect.area());
System.out.printf("Perimeter: %.1f%n", rect.perimeter());
System.out.println("Is square: " + rect.isSquare());
Rectangle scaled = rect.scale(2);
System.out.println("Scaled 2x: " + scaled);
}
}
// Custom methods in records
// Concept: custom methods - add behavior to records
record Circle(double radius) {
// Add custom methods
public double area() {
return Math.PI * radius * radius;
}
public double circumference() {
return 2 * Math.PI * radius;
}
public double diameter() {
return 2 * radius;
}
// Static factory method
public static Circle fromDiameter(double diameter) {
return new Circle(diameter / 2);
}
}
record Money(double amount, String currency) {
// Methods with logic
public Money add(Money other) {
if (!currency.equals(other.currency)) {
throw new IllegalArgumentException("Currency mismatch");
}
return new Money(amount + other.amount, currency);
}
public Money multiply(double factor) {
return new Money(amount * factor, currency);
}
public String formatted() {
return String.format("%.2f %s", amount, currency);
}
}
public class CustomMethods {
public static void main(String[] args) {
// Use custom methods
Circle c1 = ;
System.out.println("Circle: " + c1);
System.out.println("Radius: " + c1.radius());
System.out.printf("Area: %.2f%n", c1.area());
System.out.printf("Circumference: %.2f%n", c1.circumference());
System.out.printf("Diameter: %.2f%n", c1.diameter());
// Use static factory
Circle c2 = Circle.fromDiameter(20);
System.out.println("\nFrom diameter 20: " + c2);
System.out.printf("Radius: %.1f%n", c2.radius());
// Money operations
Money price1 = new Money(10.50, "USD");
Money price2 = new Money(5.25, "USD");
System.out.println("\nPrice 1: " + price1.formatted());
System.out.println("Price 2: " + price2.formatted());
Money total = price1.add(price2);
System.out.println("Total: " + total.formatted());
Money doubled = price1.multiply(2);
System.out.println("Doubled: " + doubled.formatted());
// Currency mismatch
try {
Money euros = new Money(10, "EUR");
Money combined = price1.add(euros);
System.out.println("Combined: " + combined);
} catch (IllegalArgumentException e) {
System.out.println("\nError: " + e.getMessage());
}
// Complex record with methods
record Rectangle(double width, double height) {
public double area() {
return width * height;
}
public double perimeter() {
return 2 * (width + height);
}
public boolean isSquare() {
return width == height;
}
public Rectangle scale(double factor) {
return new Rectangle(width * factor, height * factor);
}
}
Rectangle rect = new Rectangle(10, 5);
System.out.println("\nRectangle: " + rect);
System.out.printf("Area: %.1f%n", rect.area());
System.out.printf("Perimeter: %.1f%n", rect.perimeter());
System.out.println("Is square: " + rect.isSquare());
Rectangle scaled = rect.scale(2);
System.out.println("Scaled 2x: " + scaled);
}
}
Records can have static and instance methods. Fields stay immutable.
Record vs class
When to use each.
// Record vs class comparison
// Concept: record vs class - when to use each
// Traditional class
class MutablePoint {
private int x;
private int y;
public MutablePoint(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() { return x; }
public int getY() { return y; }
public void setX(int x) { this.x = x; }
public void setY(int y) { this.y = y; }
public void move(int dx, int dy) {
x += dx;
y += dy;
}
}
// Record (immutable)
record ImmutablePoint(int x, int y) {
public ImmutablePoint move(int dx, int dy) {
return new ImmutablePoint(x + dx, y + dy);
}
}
// Class with inheritance
class Shape {
protected String color;
public Shape(String color) {
this.color = color;
}
public String getColor() {
return color;
}
}
class ColoredCircle extends Shape {
private double radius;
public ColoredCircle(String color, double radius) {
super(color);
this.radius = radius;
}
public double getRadius() {
return radius;
}
}
// Record implementing interface
interface Describable {
String describe();
}
record Product(String name, double price) implements Describable {
@Override
public String describe() {
return name + " ($" + price + ")";
}
}
public class RecordVsClass {
public static void main(String[] args) {
// Mutable class
MutablePoint mp = new MutablePoint(10, 20);
System.out.println("Mutable point: (" + mp.getX() + ", " + mp.getY() + ")");
mp.setX(15);
mp.move(5, 10);
System.out.println("After changes: (" + mp.getX() + ", " + mp.getY() + ")");
// Immutable record
ImmutablePoint ip = new ImmutablePoint(10, 20);
System.out.println("\nImmutable point: " + ip);
ImmutablePoint ip2 = ip.move(5, 10);
System.out.println("Original: " + ip);
System.out.println("After move: " + ip2);
// Inheritance example
ColoredCircle circle = new ColoredCircle("red", 5.0);
System.out.println("\nColored circle:");
System.out.println(" Color: " + circle.getColor());
System.out.println(" Radius: " + circle.getRadius());
// Records cannot extend classes (always final)
// record cannot be used here
// Record with interface
Product product = new Product("Laptop", 999.99);
System.out.println("\nProduct: " + product.describe());
// When to use records
System.out.println("\nUse records for:");
System.out.println(" ✓ DTOs (Data Transfer Objects)");
System.out.println(" ✓ Immutable data models");
System.out.println(" ✓ Return multiple values");
System.out.println(" ✓ Map keys, set elements");
System.out.println("\nUse classes for:");
System.out.println(" ✓ Mutable state");
System.out.println(" ✓ Inheritance hierarchies");
System.out.println(" ✓ Private fields with logic");
System.out.println(" ✓ Complex initialization");
// Practical examples
// Good use of record
record ApiResponse(int status, String message, Object data) {}
ApiResponse success = new ApiResponse(200, "OK", "Hello");
ApiResponse error = new ApiResponse(404, "Not Found", null);
System.out.println("\nAPI responses:");
System.out.println(" " + success);
System.out.println(" " + error);
// Good use of class
class Counter {
private int count = 0;
public void increment() { count++; }
public int getCount() { return count; }
}
Counter counter = new Counter();
counter.increment();
counter.increment();
System.out.println("\nCounter: " + counter.getCount());
}
}
Record: pure data, immutable. Class: mutable state, complex behavior.
Exercise: Practical.java
Model API responses with records