Modern Java: A Version Feature Guide
What changed in each release since Java 8, which old habits it replaces, and what to reach for in new code today.
-
Java Basics
- Introduction to Java
- Setting Up Java and Writing Your First Program
- Variables, Data Types and Literals in Java
- Type Casting and Type Conversion in Java
- Operators and Expressions in Java
- Input and Output in Java
- Comments, Keywords and Naming Conventions in Java
- Control Flow in Java: if, else and switch
- Loops in Java: for, while and do-while
- Methods
- Arrays and Strings
-
OOP
- Classes and Objects in Java
- Constructors in Java
- The this Keyword in Java
- The static Keyword in Java
- Encapsulation in Java
- Access Modifiers in Java
- Inheritance in Java
- Method Overriding and super in Java
- Polymorphism in Java
- Abstraction, Abstract Classes and Interfaces in Java
- Composition, Aggregation and Association in Java
- The Object Lifecycle in Java
- Core Java
- Exception Handling
-
Collections
- The Java Collections Framework
- List in Java: ArrayList, LinkedList, Vector and Stack
- Set in Java: HashSet, LinkedHashSet and TreeSet
- Map in Java: HashMap, LinkedHashMap and TreeMap
- How HashMap Works Internally in Java
- Queue and Deque in Java: ArrayDeque and PriorityQueue
- Iterators in Java
- Comparable and Comparator in Java
- Collections Utilities and Choosing the Right Collection
- Generics
- Java 8+
- Stream API
- Date and Time
- File and I/O
-
Multithreading
- Threads in Java: Processes, Runnable and Thread
- Thread Lifecycle in Java
- Synchronization in Java: synchronized and volatile
- Locks and Atomic Classes in Java
- Race Conditions and Deadlocks in Java
- The Executor Framework and Thread Pools in Java
- Future and CompletableFuture in Java
- Concurrent Collections in Java
- The Java Memory Model
- JVM and Memory
- Advanced Java
- Networking
- JDBC
- Testing
Why this matters
A great deal of Java material still teaches patterns that were correct in 2010 and are not the best answer now. This note lists what changed and what it replaced, so older examples can be recognised for what they are.
Java 8: the turning point
// Lambdas and functional interfaces
Runnable task = () -> System.out.println("run");
// Streams
List<String> titles = notes.stream().filter(Note::published).map(Note::title).toList();
// Optional
Optional<Note> found = repository.findById(42);
// The modern date and time API
LocalDate today = LocalDate.now();
// Default and static interface methods
interface Validator { default Validator and(Validator other) { return null; } }| Replaces | With |
|---|---|
| Anonymous classes for one method | Lambdas |
| Loops that filter and collect | Streams |
Returning null | Optional |
Date, Calendar, SimpleDateFormat | java.time |
Java 9 to 11
// 9: immutable collection factories
List<String> roles = List.of("admin", "editor");
Map<String, Integer> limits = Map.of("free", 10, "paid", 100);
// 9: the module system, and private interface methods
// 9: takeWhile, dropWhile, Optional.stream, Stream.iterate with a condition
// 10: local variable type inference
var counts = new HashMap<String, Integer>();
// 11: String and Files convenience
" text ".strip();
"".isBlank();
"ab".repeat(3);
String content = Files.readString(path);
// 11: the standard HTTP client, and single file source launchJava 12 to 17
// 14: switch expressions
String type = switch (day) {
case 1, 7 -> "weekend";
default -> "weekday";
};
// 14: helpful NullPointerException messages, on by default from 15
// 15: text blocks
String query = """
SELECT id, title
FROM notes
""";
// 16: pattern matching for instanceof
if (value instanceof String text && !text.isBlank()) { }
// 16: records
public record Money(String currency, long minorUnits) { }
// 16: Stream.toList()
List<String> titles = stream.toList();
// 17: sealed classes
public sealed interface Shape permits Circle, Square { }Java 18 to 21
// 18: UTF-8 becomes the default charset everywhere
// 19 onwards: ExecutorService is AutoCloseable
try (var executor = Executors.newFixedThreadPool(4)) { }
// 21: virtual threads
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> handle(request));
}
// 21: pattern matching for switch, with guards
String result = switch (value) {
case Integer i when i < 0 -> "negative";
case Integer i -> "number " + i;
case String s -> "text";
default -> "other";
};
// 21: record patterns
if (shape instanceof Rectangle(double width, double height)) { }
// 21: sequenced collections
List<String> list = new ArrayList<>(List.of("a", "b", "c"));
System.out.println(list.getFirst());
System.out.println(list.getLast());
System.out.println(list.reversed());Java 22 to 25
// 25: compact source files and instance main methods
void main() {
IO.println("A very small program.");
}Java 25 is the current long term support release. Beyond the finalised features, several large efforts, structured concurrency and scoped values among them, have been moving through preview; check what is final in the version you target rather than assuming.
Old habit against modern equivalent
| Old | Modern |
|---|---|
new Date(), SimpleDateFormat | LocalDate, DateTimeFormatter |
Returning null for "not found" | Optional |
| Anonymous class with one method | Lambda or method reference |
A class of getters, equals and hashCode | record |
if (x instanceof T) { T t = (T) x; } | if (x instanceof T t) |
A switch statement with break | A switch expression with arrows |
| Concatenated multi line strings | Text blocks |
Arrays.asList for a constant | List.of |
Collections.unmodifiableList at a boundary | List.copyOf |
Vector, Hashtable, Stack | ArrayList, HashMap, ArrayDeque |
Manual close() in finally | try with resources |
| A thread pool for blocking I/O | Virtual threads, on 21 and later |
chain of instanceof | Sealed types with a pattern switch |
new File(...) | Path.of(...) and Files |
| Third party HTTP clients for simple calls | java.net.http.HttpClient |
Still correct, and still worth knowing
- Plain loops, when they read better than a stream.
synchronized, for simple mutual exclusion.- Interfaces and polymorphism, which pattern matching supplements rather than replaces.
- Checked exceptions, used sparingly.
- Arrays, for primitives and performance sensitive code.
Choosing a target version
| Version | Consider it when |
|---|---|
| 8 | Only a legacy constraint requires it |
| 11 | An older system that cannot move yet |
| 17 | Records and sealed types, widely supported |
| 21 | Virtual threads and pattern matching for switch |
| 25 | New projects, the current LTS |
Target the newest long term support release your environment allows. The compatibility record is exceptionally strong, so upgrading is usually far less work than expected.
How to keep current
- Read the release notes of each LTS rather than every intermediate version.
- Check the version a tutorial targets before copying its patterns.
- Let your editor suggest modern replacements, but understand each one before accepting it.
- Prefer the standard library; several once essential third party utilities are now built in.
Practice
- Take an older class of yours and modernise it: records,
var, text blocks and pattern matching. - Replace every
DateandSimpleDateFormatin a piece of code withjava.time. - Convert an
instanceofchain into a sealed hierarchy with a pattern switch. - Rewrite a fixed thread pool handling I/O using virtual threads and state the difference.
- List three features you would gain by moving a Java 8 project to the current LTS.
Conclusion
Java has changed considerably since 8, and most of the change removes ceremony rather than adding complexity. Target the newest LTS you can, learn the replacements above, and treat older examples as history rather than instruction.