🃏 Pattern Matching with switch (Java 21)
Guarded Patterns: Use when
to add conditions to case labels.
static String categorize(Object obj) {
return switch (obj) {
case String s when s.length() > 5 -> "Long string: " + s;
case String s when s.isEmpty() -> "Empty string";
case String s -> "Short string: " + s;
case Integer i when i > 100 -> "Big number: " + i;
case Integer i -> "Small number: " + i;
case null -> "Null value";
default -> "Unknown type: " + obj.getClass().getSimpleName();
};
}
// Testing:
System.out.println(categorize("Hi")); // Short string: Hi
System.out.println(categorize("Hello World")); // Long string: Hello World
System.out.println(categorize(150)); // Big number: 150
System.out.println(categorize(50)); // Small number: 50
System.out.println(categorize(null)); // Null value
⚠️ Dangerous Example - Missing default:
static String broken(Object obj) {
return switch (obj) {
case String s when s.startsWith("A") -> "A-String";
case String s when s.startsWith("B") -> "B-String";
// ❌ What if string starts with "C"? MatchException at runtime!
};
}
Pattern matching with records (Java 21):
record Point(int x, int y) {}
static String describePoint(Object obj) {
return switch (obj) {
case Point(int x, int y) when x == 0 && y == 0 -> "Origin";
case Point(int x, int y) when x == y -> "Diagonal point";
case Point(int x, int y) -> "Point at (" + x + ", " + y + ")";
default -> "Not a point";
};
}
💡 Learning Tip: Guarded patterns are checked in order. Always have a fallback case or default to avoid MatchException.
🃏 Sealed Classes (Java 21)
Purpose: Restrict which classes can extend/implement a type.
// Sealed class - only specific classes can extend
public sealed class Shape
permits Circle, Rectangle, Triangle {
}
// Permitted subclasses must be: final, sealed, or non-sealed
final class Circle extends Shape {
private final double radius;
Circle(double radius) { this.radius = radius; }
}
sealed class Rectangle extends Shape
permits Square {
protected final double width, height;
Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
}
final class Square extends Rectangle {
Square(double side) { super(side, side); }
}
non-sealed class Triangle extends Shape {
// non-sealed allows further extension
}
class IsoscelesTriangle extends Triangle {} // ✅ OK - Triangle is non-sealed
// class Pentagon extends Shape {} // ❌ Compile error - not permitted
Sealed interfaces:
public sealed interface Vehicle
permits Car, Truck, Motorcycle {
}
record Car(String model) implements Vehicle {}
record Truck(int capacity) implements Vehicle {}
record Motorcycle(boolean hasSidecar) implements Vehicle {}
Pattern matching with sealed types:
static double calculateArea(Shape shape) {
return switch (shape) {
case Circle(var radius) -> Math.PI * radius * radius;
case Rectangle(var width, var height) -> width * height;
case Triangle t -> 10.0; // Simplified calculation
// No default needed - compiler knows all possibilities!
};
}
💡 Learning Tip: Sealed = “Exclusive club” - only VIP classes (permits list) can join. Compiler knows all possibilities, enabling exhaustive pattern matching.
🃏 Records (Java 21 Features)
Basic record syntax:
// Compact record declaration
public record Person(String name, int age) {
// Automatically generates:
// - Constructor: Person(String name, int age)
// - Accessors: name(), age()
// - equals(), hashCode(), toString()
}
// Usage:
Person person = new Person("Alice", 25);
System.out.println(person.name()); // Alice
System.out.println(person.age()); // 25
Record with validation and custom methods:
public record BankAccount(String accountNumber, double balance) {
// Compact constructor for validation
public BankAccount {
if (balance < 0) {
throw new IllegalArgumentException("Balance cannot be negative");
}
if (accountNumber == null || accountNumber.isBlank()) {
throw new IllegalArgumentException("Account number required");
}
}
// Custom methods allowed
public boolean isOverdrawn() {
return balance < 0;
}
public BankAccount withdraw(double amount) {
return new BankAccount(accountNumber, balance - amount);
}
}
Records with pattern matching:
record Point(int x, int y) {}
record ColoredPoint(Point point, String color) {}
static String describe(Object obj) {
return switch (obj) {
case Point(int x, int y) -> "Point at (" + x + ", " + y + ")";
case ColoredPoint(Point(int x, int y), String color) ->
color + " point at (" + x + ", " + y + ")";
default -> "Unknown";
};
}
💡 Learning Tip: Records = “Data class on autopilot” - automatic constructor, accessors, equals/hashCode/toString. Perfect for immutable data carriers.
🃏 Text Blocks (Java 21)
Multi-line strings with preserved formatting:
// Traditional string concatenation:
String html1 = "<html>\n" +
" <body>\n" +
" <h1>Hello World</h1>\n" +
" </body>\n" +
"</html>";
// Text block (Java 15+):
String html2 = """
<html>
<body>
<h1>Hello World</h1>
</body>
</html>
""";
// JSON example:
String json = """
{
"name": "John Doe",
"age": 30,
"city": "New York"
}
""";
// SQL example:
String query = """
SELECT users.name, users.email, orders.total
FROM users
JOIN orders ON users.id = orders.user_id
WHERE orders.date >= ?
ORDER BY orders.total DESC
""";
Text block processing methods:
String textBlock = """
Line 1
Line 2
Line 3
""";
// String methods work normally:
String[] lines = textBlock.lines().toArray(String[]::new);
String trimmed = textBlock.strip();
boolean contains = textBlock.contains("Line 2");
// Formatted text blocks:
String template = """
Hello %s,
Your balance is $%.2f
Account: %s
""";
String message = template.formatted("Alice", 1234.56, "ACC-123");
💡 Learning Tip: Text blocks = “What you see is what you get” - preserves formatting, perfect for HTML, JSON, SQL. Triple quotes mark the boundaries.
🃏 Java I/O - File Reading and Writing
Rule: Java I/O provides multiple ways to read/write files with different performance characteristics.
- Files.readString()/writeString(): Simple text file operations (Java 11+)
- BufferedReader/Writer: Efficient line-by-line processing
- FileInputStream/OutputStream: Byte-level operations
import java.nio.file.*;
import java.io.*;
import java.util.List;
// Simple file operations (Java 11+)
Path textFile = Path.of("data.txt");
// Write string to file
String content = "Hello\nWorld\nJava";
Files.writeString(textFile, content);
// Read entire file as string
String fileContent = Files.readString(textFile);
System.out.println(fileContent);
// Read all lines into List
List<String> lines = Files.readAllLines(textFile);
lines.forEach(System.out::println);
// Write lines to file
List<String> outputLines = List.of("Line 1", "Line 2", "Line 3");
Files.write(textFile, outputLines);
Buffered I/O for large files:
// Efficient reading with BufferedReader
try (BufferedReader reader = Files.newBufferedReader(Path.of("large.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
// Efficient writing with BufferedWriter
try (BufferedWriter writer = Files.newBufferedWriter(Path.of("output.txt"))) {
writer.write("First line");
writer.newLine();
writer.write("Second line");
writer.newLine();
}
// Stream processing for very large files
try (Stream<String> lines = Files.lines(Path.of("huge.txt"))) {
lines.filter(line -> line.contains("important"))
.map(String::toUpperCase)
.forEach(System.out::println);
}
Byte-level operations:
// Copy file using byte arrays
try (FileInputStream in = new FileInputStream("source.dat");
FileOutputStream out = new FileOutputStream("dest.dat")) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
}
// Files utility for copying
Files.copy(Path.of("source.txt"), Path.of("destination.txt"),
StandardCopyOption.REPLACE_EXISTING);
💡 Learning Tip: Remember “FILES = SIMPLE, STREAMS = CONTROL” - Files class for simple operations, streams for fine-grained control and large files.
Q: When should you use Files.readString() vs BufferedReader?
A: Use Files.readString() for small files when you need the entire content. Use BufferedReader for large files or when processing line-by-line to avoid memory issues.