Java 14: Records and Pattern Matching Basics

Java 14: Records and the Start of Pattern Matching
Java 14 (March 2020) introduced records, a powerful way to create immutable data classes with less boilerplate. It also began the journey toward pattern matching, a major feature coming in future releases.
1. Records (Preview)
Records automatically generate the boilerplate code for immutable data carriers.
Before Records:
// Traditional data class with lots of boilerplate
public final class User {
private final long id;
private final String name;
private final String email;
public User(long id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
public long getId() { return id; }
public String getName() { return name; }
public String getEmail() { return email; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User)) return false;
User user = (User) o;
return id == user.id &&
Objects.equals(name, user.name) &&
Objects.equals(email, user.email);
}
@Override
public int hashCode() {
return Objects.hash(id, name, email);
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", email='" + email + '\'' +
'}';
}
}With Records (Java 14+):
// Compact and clean - all boilerplate auto-generated!
public record User(long id, String name, String email) {}
// Usage is identical to the old class
User user = new User(1, "John", "john@example.com");
System.out.println(user.id()); // Accessing fields
System.out.println(user.name());
System.out.println(user); // toString() auto-generated
System.out.println(user.hashCode()); // hashCode() auto-generated
// Equals works automatically
User user2 = new User(1, "John", "john@example.com");
System.out.println(user.equals(user2)); // trueAdvanced Record Features:
// Records with custom methods
public record Product(String id, String name, double price) {
// Compact constructor for validation
public Product {
if (price < 0) {
throw new IllegalArgumentException("Price cannot be negative");
}
name = name.trim().toUpperCase();
}
// Custom methods are allowed
public String getFormattedPrice() {
return String.format("$%.2f", price);
}
// Static methods and fields
public static Product fromString(String data) {
String[] parts = data.split(",");
return new Product(parts[0], parts[1], Double.parseDouble(parts[2]));
}
}
// Usage
Product p = new Product("PROD-123", "laptop", 999.99);
System.out.println(p.getFormattedPrice()); // $999.99
Product p2 = Product.fromString("PROD-456,Mouse,25.50");
System.out.println(p2); // Product[id=PROD-456, name=MOUSE, price=25.5]Records with Annotations:
// Using annotations with records
@interface Persisted {
String table();
}
@Persisted(table = "users")
public record User(
@NotNull String name,
@Email String email,
@Positive long age
) {}2. Pattern Matching for instanceof (Preview)
The first step toward pattern matching - type patterns in instanceof.
Before Java 14:
// Traditional type checking and casting
Object obj = "Hello";
if (obj instanceof String) {
String str = (String) obj; // Redundant casting
System.out.println("String length: " + str.length());
}
// More complex code when multiple types
if (obj instanceof String) {
String str = (String) obj;
processString(str);
} else if (obj instanceof Integer) {
Integer num = (Integer) obj;
processNumber(num);
}After Java 14:
// Cleaner pattern matching
Object obj = "Hello";
if (obj instanceof String str) {
System.out.println("String length: " + str.length()); // str is ready to use
}
// More elegant control flow
if (obj instanceof String str) {
processString(str);
} else if (obj instanceof Integer num) {
processNumber(num);
}
// Pattern matching in compound conditions
if (obj instanceof String str && str.length() > 0) {
System.out.println("Non-empty string: " + str);
}3. Text Blocks (Final)
Text blocks became a standard feature in Java 14.
// Text blocks are now standard (no preview needed)
String json = """
{
"status": "success",
"data": {
"id": 123,
"message": "Request processed"
}
}
""";
String htmlEmail = """
<html>
<body>
<h2>Welcome!</h2>
<p>Thank you for joining us.</p>
<footer>
<p>© 2024 MyCompany</p>
</footer>
</body>
</html>
""";4. Foreign-Memory Access API (Incubating)
Safe access to off-heap memory without JNI.
// Experimental feature for performance-critical code
import jdk.incubator.foreign.*;
public class MemoryAccess {
public static void main(String[] args) throws Throwable {
// Allocate off-heap memory
MemorySegment segment = MemorySegment.allocateNative(
Layout.ofSequence(10, CLinker.C_INT)
);
// Access memory safely
VarHandle handle = Layout.ofSequence(
10, CLinker.C_INT
).varHandle(MemoryLayout.PathElement.sequenceElement());
// Write values
handle.set(segment, 0, 42);
// Read values
System.out.println(handle.get(segment, 0)); // 42
}
}5. Helpful NullPointerExceptions
Better error messages for NullPointerException.
// Before Java 14
String result = person.getAddress().getCity().toUpperCase();
// Exception: NullPointerException (which part was null?)
// After Java 14: Descriptive error message
// Exception: Cannot invoke "String.toUpperCase()" because
// "person.getAddress().getCity()" is null
// Helpful for debugging long chains!
object.method1().method2().method3().method4();
// Error tells you exactly which method returned null6. Switch Expressions (Finalized)
Switch expressions graduated from preview to standard feature.
// Now standard, not preview
int daysInMonth = switch (month) {
case JANUARY, MARCH, MAY, JULY, AUGUST, DECEMBER -> 31;
case APRIL, JUNE, SEPTEMBER, NOVEMBER -> 30;
case FEBRUARY -> 28;
};
String season = switch (month) {
case 12, 1, 2 -> "Winter";
case 3, 4, 5 -> "Spring";
case 6, 7, 8 -> "Summer";
case 9, 10, 11 -> "Fall";
default -> throw new IllegalArgumentException("Invalid month: " + month);
};Developer Impact
Positive:
- Records: Eliminates vast amounts of boilerplate code
- Pattern Matching: More readable type checks
- Less Code: 50%+ reduction in data class code
- Type Safety: Automatic equals/hashCode generation
- Better Errors: NullPointerException messages are helpful
Challenges:
- Learning Curve: Records require different thinking
- Records Preview: Still subject to changes
- Migration: Old projects with data classes
- Feature Preview: Pattern matching still incomplete
Pros and Cons
Pros ✅
- Records: Dramatically reduces boilerplate
- Immutability: Records promote immutable data
- Auto-generated: equals(), hashCode(), toString()
- Pattern Matching(instanceof): Cleaner type checking
- Text Blocks Standard: Multi-line strings fully supported
- Helpful NPE: Better debugging with precise error messages
- Switch Finalized: Switch expressions are production-ready
Cons ❌
- Records Limitation: Must be immutable (no mutable setters)
- Records Preview: May still change in future versions
- Learning Curve: Different paradigm for data classes
- No LTS: Non-LTS release with short support
- Pattern Matching: Incomplete - only instanceof supported
Practical Example: API Response Handler
// Using records for API responses
public record ApiResponse<T>(
int status,
String message,
T data
) {
// Compact constructor with validation
public ApiResponse {
if (status < 100 || status >= 600) {
throw new IllegalArgumentException("Invalid HTTP status");
}
}
// Custom methods
public boolean isSuccess() {
return status >= 200 && status < 300;
}
}
public record UserData(long id, String name, String email) {}
public record ErrorData(List<String> errors) {}
// Usage with pattern matching
public void handleResponse(Object response) {
if (response instanceof ApiResponse<?> r) {
System.out.println("Status: " + r.status());
if (r.data() instanceof UserData u) {
System.out.println("User: " + u.name());
} else if (r.data() instanceof ErrorData e) {
System.out.println("Errors: " + e.errors());
}
}
}Conclusion
Java 14 is transformative for Java developers. Records eliminate the tedious data class boilerplate that has plagued Java for years. Combined with pattern matching (beginning with instanceof), Java becomes more concise and expressive. While not LTS, Java 14 features paved the road for Java 17 LTS.
Recommendation:
- Adopt records for all new data classes
- Migrate old data classes to records (where possible)
- Use pattern matching instanceof for cleaner type checks
- Plan to stay current with new releases for new features