This project targets Java 21 (LTS) and actively uses the modern features introduced since Java 8. This guide explains each feature, why it exists, and where you'll see it in the code.
What it is: The compiler infers the type of a local variable from the right-hand side.
You write var instead of repeating the full type name.
// Old way — type written twice
DiscountService service = new DiscountService();
String discountLine = service.getDiscountLine(customer);
// Java 10+ — type inferred, less noise
var service = new DiscountService();
var discountLine = service.getDiscountLine(customer);- Only works for local variables (inside methods). Not fields, not parameters.
- The type is still statically checked at compile time — this is NOT dynamic typing.
- Don't overuse it when the type isn't obvious from context:
var x = getValue(); // ❌ What type is x? Unclear without checking getValue()
var service = new DiscountService(); // ✅ Type is obvious from the right-hand sideIn this project: DiscountServiceTest.java, OptionalScenariosDemoTest.java, AdvancedOptionalScenariosTest.java, Main.java, Person.java
What it is: switch can now be used as an expression that produces a value.
Uses -> arrow syntax — no fall-through, no break needed.
// Old switch statement — verbose, fall-through prone
int discount;
if (points >= 100) {
discount = 10;
} else if (points >= 50) {
discount = 5;
} else {
discount = 0;
}
// Java 14+ switch expression — concise, exhaustive
int discount = switch (points / 50) {
case 0 -> 0; // < 50 points
case 1 -> 5; // 50–99 points
default -> 10; // 100+ points
};- No fall-through bugs — each arm is independent
- Exhaustiveness — the compiler warns if cases are missing
- Returns a value — can be used inline in assignments or return statements
- Cleaner than if/else chains for multi-branch logic
In this project: DiscountService.java — getDiscountPercentage() uses a switch expression to map card points to a discount tier.
What it is: An instance method on String equivalent to String.format() but called on the template string itself.
// Old way
String msg = String.format("Person{name='%s', age=%d}", name, age);
// Java 15+
String msg = "Person{name='%s', age=%d}".formatted(name, age);Reads more naturally — the template string comes first, then .formatted() fills it in.
In this project: Person.toString() in Person.java
What it is: A terminal operation on Stream<T> that collects elements into an unmodifiable List<T>.
No import needed. Returns a list that cannot be mutated after creation.
// Old way (Java 8) — requires import, verbose
import static java.util.stream.Collectors.toList;
List<Integer> ages = stream.collect(toList());
// Java 16+ — concise, no import, signals immutability
List<Integer> ages = stream.toList();collect(Collectors.toList()) |
.toList() |
|
|---|---|---|
| Mutability | Mutable list | Unmodifiable list |
| Import | Required | None |
| Available since | Java 8 | Java 16 |
💡 Prefer
.toList()when you don't need to mutate the result — it communicates intent (this list won't change) and is more concise.
In this project: OptionalScenariosDemoTest.java, AdvancedOptionalScenariosTest.java
Converts an Optional<T> into a Stream<T> with 0 or 1 elements.
Used with flatMap to cleanly drop absent values from a Stream<Optional<T>>:
List<Integer> ages = names.stream()
.map(Person::findByName) // Stream<Optional<Person>>
.flatMap(Optional::stream) // absent Optionals become empty streams → vanish
.map(Person::getAge)
.toList();Fallback to another Optional when the first is empty:
Optional<User> user = findInCache(id)
.or(() -> findInDatabase(id))
.or(() -> findInArchive(id));In this project: OptionalScenariosDemoTest.java, AdvancedOptionalScenariosTest.java
What it is: Factory methods for creating compact, unmodifiable collections.
// Old way — mutable, verbose
List<String> names = new ArrayList<>();
names.add("Jack");
names.add("Sara");
names = Collections.unmodifiableList(names); // extra step to make immutable
// Java 9+ — concise and immutable by default
var names = List.of("Jack", "Sara");
⚠️ List.of()throwsNullPointerExceptionif you passnullas an element.
UseArrays.asList()if you specifically need to allow null elements.
In this project: OptionalScenariosDemoTest.java, Person.java, AdvancedOptionalScenariosTest.java
Handles both the present and absent case in a single, readable call:
// Old way
if (optional.isPresent()) {
sendRewardEmail(optional.get());
} else {
sendPromo();
}
// Java 9+
optional.ifPresentOrElse(
card -> sendRewardEmail(card),
() -> sendPromo()
);In this project: AdvancedOptionalScenariosTest.java
The positive way to check for absence — reads better than !isPresent():
// Java 8
if (!customer.getMemberCard().isPresent()) { ... }
// Java 11+
if (customer.getMemberCard().isEmpty()) { ... }In this project: AdvancedOptionalScenariosTest.java
The project runs on Java 21 LTS. These features are available to explore:
Immutable data carriers with auto-generated constructors, accessors, equals, hashCode, and toString:
// Instead of a full POJO class with boilerplate
record Person(String name, int age) {}
// Usage
var p = new Person("Jack", 15);
System.out.println(p.name()); // accessor is name(), not getName()
System.out.println(p); // Person[name=Jack, age=15]💡 Great for DTOs, value objects, and data transfer classes in real projects.
Eliminates the cast after instanceof:
// Old way
if (shape instanceof Circle) {
Circle c = (Circle) shape; // redundant cast
return c.radius() * 2;
}
// Java 16+
if (shape instanceof Circle c) {
return c.radius() * 2; // c is already typed as Circle
}Restrict which classes can extend a class — makes hierarchies explicit and exhaustive:
sealed interface Shape permits Circle, Rectangle, Triangle {}Combined with switch expressions and pattern matching, this enables exhaustive type-safe dispatch — the compiler knows all possible subtypes.
Multi-line strings without escape characters:
// Old way
String json = "{\n \"name\": \"Jack\",\n \"age\": 15\n}";
// Java 15+
String json = """
{
"name": "Jack",
"age": 15
}
""";| Feature | Since | Used in this project |
|---|---|---|
Optional, lambdas, Function<T,R> |
Java 8 | ✅ Throughout |
List.of(), Optional::stream, or(), ifPresentOrElse() |
Java 9 | ✅ Tests |
var (local type inference) |
Java 10 | ✅ All files |
Optional.isEmpty() |
Java 11 | ✅ AdvancedOptionalScenariosTest |
| Switch expressions | Java 14 | ✅ DiscountService |
String.formatted() |
Java 15 | ✅ Person.toString() |
Stream.toList() |
Java 16 | ✅ Test files |
| Records | Java 16 | Available — not yet used |
Pattern matching instanceof |
Java 16 | Available — not yet used |
| Sealed classes | Java 17 | Available — not yet used |
| Virtual threads | Java 21 | Available — not yet used |