Java course menu
Modern Java Features (Java 10 to 21): var, Records, Text Blocks, Switch Expressions and Pattern Matching
A tour of modern Java: var, enhanced switch expressions, text blocks, records, pattern matching for instanceof and switch, record patterns, sealed classes, Optional, the java.time API, new String and collection methods, and sequenced collections in Java 21.
On this page
Java has changed a lot since Java 8. Code written with modern features is shorter, safer and easier to read than the style found in many older tutorials. This lesson gathers the most useful additions up to the Java 21 LTS release, several of which you met earlier in the course.
var#
var infers a local variable's type from its initializer. It removes noise from long generic types:
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class Main {
public static void main(String[] args) {
var scores = new HashMap<String, List<Integer>>(); // instead of repeating the type
scores.put("Asha", new ArrayList<>(List.of(92, 88)));
for (var entry : scores.entrySet()) {
System.out.println(entry.getKey() + " " + entry.getValue());
}
}
}Asha [92, 88]Use var when the type is obvious from the right-hand side; write the type when it helps readers.
Switch expressions#
public class Main {
public static void main(String[] args) {
int month = 4;
String season = switch (month) {
case 12, 1, 2 -> "winter";
case 3, 4, 5 -> "summer";
case 6, 7, 8, 9 -> "monsoon";
default -> "autumn";
};
System.out.println(season);
}
}summerNo fallthrough, no break, several labels per case, and the compiler checks exhaustiveness. See switch.
Text blocks#
public class Main {
public static void main(String[] args) {
String name = "Asha";
String html = """
<p>
Hello, %s!
</p>
""".formatted(name);
System.out.print(html);
}
}<p>
Hello, Asha!
</p>Records#
record Money(String currency, long amount) {
Money {
if (amount < 0) throw new IllegalArgumentException("negative amount");
}
Money plus(Money other) {
return new Money(currency, amount + other.amount);
}
}
public class Main {
public static void main(String[] args) {
Money a = new Money("INR", 500);
Money b = new Money("INR", 250);
System.out.println(a.plus(b));
System.out.println(a.equals(new Money("INR", 500)));
}
}Money[currency=INR, amount=750]
trueSee Records and Enums.
Pattern matching#
instanceof#
public class Main {
static String describe(Object o) {
if (o instanceof String s && !s.isBlank()) {
return "text of length " + s.length();
} else if (o instanceof Integer i) {
return "number " + (i * 2) + " when doubled";
}
return "something else";
}
public static void main(String[] args) {
System.out.println(describe("hello"));
System.out.println(describe(21));
System.out.println(describe(3.5));
}
}text of length 5
number 42 when doubled
something elseSealed types, switch patterns and record patterns#
Together, these let you model data as a closed set of record types and process it safely. The compiler knows every possible case, so no default is needed, and record patterns take records apart in the case itself:
sealed interface Shape permits Circle, Rectangle, Triangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double width, double height) implements Shape {}
record Triangle(double base, double height) implements Shape {}
public class Main {
static double area(Shape shape) {
return switch (shape) {
case Circle(double r) -> Math.PI * r * r;
case Rectangle(double w, double h) -> w * h;
case Triangle(double b, double h) -> 0.5 * b * h;
};
}
public static void main(String[] args) {
Shape[] shapes = {new Circle(1), new Rectangle(3, 4), new Triangle(6, 2)};
for (Shape s : shapes) {
System.out.printf("%s -> %.2f%n", s, area(s));
}
}
}Circle[radius=1.0] -> 3.14
Rectangle[width=3.0, height=4.0] -> 12.00
Triangle[base=6.0, height=2.0] -> 6.00If you add a new Shape to the permits list and forget a case, the switch stops compiling.
Optional#
Optional<T> represents a value that may be absent, making "not found" explicit instead of returning null:
import java.util.Map;
import java.util.Optional;
public class Main {
static final Map<String, String> EMAILS = Map.of("asha", "asha@example.com");
static Optional<String> findEmail(String user) {
return Optional.ofNullable(EMAILS.get(user));
}
public static void main(String[] args) {
System.out.println(findEmail("asha").map(String::toUpperCase).orElse("unknown"));
System.out.println(findEmail("john").orElse("unknown"));
findEmail("asha").ifPresent(e -> System.out.println("send to " + e));
System.out.println(findEmail("john").isPresent());
}
}ASHA@EXAMPLE.COM
unknown
send to asha@example.com
falseUse Optional mainly as a return type. Avoid get() without checking; prefer orElse, orElseThrow, map and ifPresent.
Dates and times: java.time#
The java.time API (Java 8) replaced the old, error-prone Date and Calendar classes. Its types are immutable and clearly named:
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.Period;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
public class Main {
public static void main(String[] args) {
LocalDate start = LocalDate.of(2026, 11, 2);
LocalDate end = LocalDate.of(2027, 1, 15);
System.out.println(ChronoUnit.DAYS.between(start, end));
System.out.println(start.plusDays(30));
System.out.println(start.getDayOfWeek() == DayOfWeek.MONDAY);
System.out.println(Period.between(start, end));
System.out.println(start.format(DateTimeFormatter.ofPattern("dd MMM yyyy")));
System.out.println(LocalDate.parse("2026-10-23").isLeapYear());
}
}74
2026-12-02
true
P2M13D
02 Nov 2026
falseLocalDate (a date), LocalTime, LocalDateTime, ZonedDateTime (with a time zone), Instant (a moment in UTC), Duration and Period cover nearly every need. Formatting month names uses your system's language settings unless you pass a Locale.
Handy small additions#
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
System.out.println(" ".isBlank() + " [" + " hi ".strip() + "] " + "ab".repeat(3));
System.out.println("line1\nline2\n".lines().count());
List<String> names = new ArrayList<>(List.of("Asha", "Ravi", "Meera"));
System.out.println(names.getFirst() + " " + names.getLast()); // Java 21
System.out.println(names.reversed()); // Java 21
List<Integer> squares = List.of(1, 2, 3).stream().map(n -> n * n).toList(); // Java 16
System.out.println(squares);
}
}true [hi] ababab
2
Asha Meera
[Meera, Ravi, Asha]
[1, 4, 9]Java 21's sequenced collections add getFirst, getLast, addFirst, addLast and reversed to lists, deques, LinkedHashSet, LinkedHashMap and sorted collections.
Common mistakes#
Practice#
Practice exercise
Define a sealed interface Payment with records Card(String last4, double amount) and Upi(String handle, double amount). Write a method that uses a switch with record patterns to describe each payment, like Card ending 4242: 499.00, and use it on one of each.
Show solution
sealed interface Payment permits Card, Upi {}
record Card(String last4, double amount) implements Payment {}
record Upi(String handle, double amount) implements Payment {}
public class Payments {
static String describe(Payment p) {
return switch (p) {
case Card(String last4, double amount) -> String.format("Card ending %s: %.2f", last4, amount);
case Upi(String handle, double amount) -> String.format("UPI %s: %.2f", handle, amount);
};
}
public static void main(String[] args) {
System.out.println(describe(new Card("4242", 499)));
System.out.println(describe(new Upi("asha@bank", 1250.5)));
}
}Card ending 4242: 499.00
UPI asha@bank: 1250.50Key takeaway#
Finished this lesson?