✦ 113 modern patterns · Java 8 → Java 25
Java has evolved.
Your code can too.
A collection of modern Java code snippets. Every old Java pattern next to its clean, modern replacement — side by side.
✕ Old
public class Point {
private final int x, y;
public Point(int x, int y) { ... }
public int getX() { return x; }
public int getY() { return y; }
// equals, hashCode, toString
}
✓ Modern
public record Point(int x, int y) {}
Modernize your Java codebase with GitHub Copilot.
Let Copilot help you migrate legacy patterns to modern Java — automatically.
All comparisons
113 snippets- All
- Language
- Collections
- Strings
- Streams
- Concurrency
- I/O
- Errors
- Date/Time
- Security
- Tooling
- Enterprise
- All
- Java 11
- Java 17
- Java 21
- Java 25
Language
Calling out to C code from Java
Old
public class CallCFromJava {
static { System.loadLibrary("strlen-jni"); }
public static native long strlen(String s);
public static void main(String[] args) {
long ret = strlen("Bambi");
System.out.println("Return value " + ret); // 5
}
}
// Run javac -h to generate the .h file, then write C:
// #include "CallCFromJava.h"
// #include <string.h>
// JNIEXPORT jlong JNICALL Java_CallCFromJava_strlen(
// JNIEnv *env, jclass clazz, jstring str) {
// const char* s = (*env)->GetStringUTFChars(env, str, NULL);
// jlong len = (jlong) strlen(s);
// (*env)->ReleaseStringUTFChars(env, str, s);
// return len;
// }
Modern
void main() throws Throwable {
try (var arena = Arena.ofConfined()) {
// Use any system library directly — no C wrapper needed
var stdlib = Linker.nativeLinker().defaultLookup();
var foreignFuncAddr = stdlib.find("strlen").orElseThrow();
var strlenSig = FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS);
var strlenMethod = Linker.nativeLinker() .downcallHandle(foreignFuncAddr, strlenSig);
var ret = (long) strlenMethod.invokeExact(arena.allocateFrom("Bambi"));
System.out.println("Return value " + ret); // 5
}
}
// Your own C library needs no special Java annotations:
// long greet(char* name) {
// printf("Hello %s\n", name);
// return 0;
// }
hover to see modern →
Language
Compact canonical constructor
Old
public record Person(String name,
List<String> pets) {
// Full canonical constructor
public Person(String name,
List<String> pets) {
Objects.requireNonNull(name);
this.name = name;
this.pets = List.copyOf(pets);
}
}
Modern
public record Person(String name,
List<String> pets) {
// Compact constructor
public Person {
Objects.requireNonNull(name);
pets = List.copyOf(pets);
}
}
hover to see modern →
Language
Compact source files
Old
public class HelloWorld {
public static void main(String[] args) {
System.out.println(
"Hello, World!");
}
}
Modern
void main() {
IO.println("Hello, World!");
}
hover to see modern →
Language
Default interface methods
Old
// Need abstract class to share behavior
public abstract class AbstractLogger {
public void log(String msg) {
System.out.println(
timestamp() + ": " + msg);
}
abstract String timestamp();
}
// Single inheritance only
public class FileLogger
extends AbstractLogger { ... }
Modern
public interface Logger {
default void log(String msg) {
IO.println(
timestamp() + ": " + msg);
}
String timestamp();
}
// Multiple interfaces allowed
public class FileLogger
implements Logger, Closeable { ... }
hover to see modern →
Language
Diamond with anonymous classes
Old
Map<String, List<String>> map =
new HashMap<String, List<String>>();
// anonymous class: no diamond
Predicate<String> p =
new Predicate<String>() {
public boolean test(String s) {..}
};
Modern
Map<String, List<String>> map =
new HashMap<>();
// Java 9: diamond with anonymous classes
Predicate<String> p =
new Predicate<>() {
public boolean test(String s) {..}
};
hover to see modern →
Language
Exhaustive switch without default
Old
// Must add default even though
// all cases are covered
double area(Shape s) {
if (s instanceof Circle c)
return Math.PI * c.r() * c.r();
else if (s instanceof Rect r)
return r.w() * r.h();
else throw new IAE();
}
Modern
// sealed Shape permits Circle, Rect
double area(Shape s) {
return switch (s) {
case Circle c ->
Math.PI * c.r() * c.r();
case Rect r ->
r.w() * r.h();
}; // no default needed!
}
hover to see modern →
Language
Flexible constructor bodies
Old
class Square extends Shape {
Square(double side) {
super(side, side);
// can't validate BEFORE super!
if (side <= 0)
throw new IAE("bad");
}
}
Modern
class Square extends Shape {
Square(double side) {
if (side <= 0)
throw new IAE("bad");
super(side, side);
}
}
hover to see modern →
Language
Guarded patterns with when
Old
if (shape instanceof Circle) {
Circle c = (Circle) shape;
if (c.radius() > 10) {
return "large circle";
} else {
return "small circle";
}
} else {
return "not a circle";
}
Modern
return switch (shape) {
case Circle c
when c.radius() > 10
-> "large circle";
case Circle c
-> "small circle";
default -> "not a circle";
};
hover to see modern →
Language
Markdown in Javadoc comments
Old
/**
* Returns the {@code User} with
* the given ID.
*
* <p>Example:
* <pre>{@code
* var user = findUser(123);
* }</pre>
*
* @param id the user ID
* @return the user
*/
public User findUser(int id) { ... }
Modern
/// Returns the `User` with
/// the given ID.
///
/// Example:
/// ```java
/// var user = findUser(123);
/// ```
///
/// @param id the user ID
/// @return the user
public User findUser(int id) { ... }
hover to see modern →
Language
Module import declarations
Old
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
Modern
import module java.base;
// All of java.util, java.io, java.nio
// etc. available in one line
hover to see modern →
Language
Pattern matching for instanceof
Old
if (obj instanceof String) {
String s = (String) obj;
System.out.println(s.length());
}
Modern
if (obj instanceof String s) {
IO.println(s.length());
}
hover to see modern →
Language
Pattern matching in switch
Old
String format(Object obj) {
if (obj instanceof Integer i)
return "int: " + i;
else if (obj instanceof Double d)
return "double: " + d;
else if (obj instanceof String s)
return "str: " + s;
return "unknown";
}
Modern
String format(Object obj) {
return switch (obj) {
case Integer i -> "int: " + i;
case Double d -> "double: " + d;
case String s -> "str: " + s;
default -> "unknown";
};
}
hover to see modern →
Language
Primitive types in patterns
Old
String classify(int code) {
if (code >= 200 && code < 300)
return "success";
else if (code >= 400 && code < 500)
return "client error";
else
return "other";
}
Modern
String classify(int code) {
return switch (code) {
case int c when c >= 200
&& c < 300 -> "success";
case int c when c >= 400
&& c < 500 -> "client error";
default -> "other";
};
}
hover to see modern →
Language
Private interface methods
Old
interface Logger {
default void logInfo(String msg) {
System.out.println(
"[INFO] " + timestamp() + msg);
}
default void logWarn(String msg) {
System.out.println(
"[WARN] " + timestamp() + msg);
}
}
Modern
interface Logger {
private String format(String lvl, String msg) {
return "[" + lvl + "] " + timestamp() + msg;
}
default void logInfo(String msg) {
IO.println(format("INFO", msg));
}
default void logWarn(String msg) {
IO.println(format("WARN", msg));
}
}
hover to see modern →
Language
Record patterns (destructuring)
Old
if (obj instanceof Point) {
Point p = (Point) obj;
int x = p.getX();
int y = p.getY();
System.out.println(x + y);
}
Modern
if (obj instanceof Point(int x, int y)) {
IO.println(x + y);
}
hover to see modern →
Language
Records for data classes
Old
public class Point {
private final int x, y;
public Point(int x, int y) { ... }
public int getX() { return x; }
public int getY() { return y; }
// equals, hashCode, toString
}
Modern
public record Point(int x, int y) {}
hover to see modern →
Language
Sealed classes for type hierarchies
Old
// Anyone can extend Shape
public abstract class Shape { }
public class Circle extends Shape { }
public class Rect extends Shape { }
// unknown subclasses possible
Modern
public sealed interface Shape
permits Circle, Rect {}
public record Circle(double r)
implements Shape {}
public record Rect(double w, double h)
implements Shape {}
hover to see modern →
Language
Static members in inner classes
Old
class Library {
// Must be static nested class
static class Book {
static int globalBookCount;
Book() {
globalBookCount++;
}
}
}
// Usage
var book = new Library.Book();
Modern
class Library {
// Can be inner class with statics
class Book {
static int globalBookCount;
Book() {
Book.globalBookCount++;
}
}
}
// Usage
var lib = new Library();
var book = lib.new Book();
hover to see modern →
Language
Static methods in interfaces
Old
// Separate utility class needed
public class ValidatorUtils {
public static boolean isBlank(
String s) {
return s == null ||
s.trim().isEmpty();
}
}
// Usage
if (ValidatorUtils.isBlank(input)) { ... }
Modern
public interface Validator {
boolean validate(String s);
static boolean isBlank(String s) {
return s == null ||
s.trim().isEmpty();
}
}
// Usage
if (Validator.isBlank(input)) { ... }
hover to see modern →
Language
Switch expressions
Old
String msg;
switch (day) {
case MONDAY:
msg = "Start";
break;
case FRIDAY:
msg = "End";
break;
default:
msg = "Mid";
}
Modern
String msg = switch (day) {
case MONDAY -> "Start";
case FRIDAY -> "End";
default -> "Mid";
};
hover to see modern →
Language
Text blocks for multiline strings
Old
String json = "{\n" +
" \"name\": \"Duke\",\n" +
" \"age\": 30\n" +
"}";
Modern
String json = """
{
"name": "Duke",
"age": 30
}""";
hover to see modern →
Language
Type inference with var
Old
Map<String, List<Integer>> map =
new HashMap<String, List<Integer>>();
for (Map.Entry<String, List<Integer>> e
: map.entrySet()) {
// verbose type noise
}
Modern
var map = new HashMap<String, List<Integer>>();
for (var entry : map.entrySet()) {
// clean and readable
}
hover to see modern →
Language
Unnamed variables with _
Old
try {
parse(input);
} catch (Exception ignored) {
log("parse failed");
}
map.forEach((key, value) -> {
process(value); // key unused
});
Modern
try {
parse(input);
} catch (Exception _) {
log("parse failed");
}
map.forEach((_, value) -> {
process(value);
});
hover to see modern →
Collections
Collectors.teeing()
Old
long count = items.stream().count();
double sum = items.stream()
.mapToDouble(Item::price)
.sum();
var result = new Stats(count, sum);
Modern
var result = items.stream().collect(
Collectors.teeing(
Collectors.counting(),
Collectors.summingDouble(Item::price),
Stats::new
)
);
hover to see modern →
Collections
Copying collections immutably
Old
List<String> copy =
Collections.unmodifiableList(
new ArrayList<>(original)
);
Modern
List<String> copy =
List.copyOf(original);
hover to see modern →
Collections
Immutable list creation
Old
List<String> list =
Collections.unmodifiableList(
new ArrayList<>(
Arrays.asList("a", "b", "c")
)
);
Modern
List<String> list =
List.of("a", "b", "c");
hover to see modern →
Collections
Immutable map creation
Old
Map<String, Integer> map = new HashMap<>();
map.put("a", 1);
map.put("b", 2);
map.put("c", 3);
map = Collections.unmodifiableMap(map);
Modern
Map<String, Integer> map =
Map.of("a", 1, "b", 2, "c", 3);
hover to see modern →
Collections
Immutable set creation
Old
Set<String> set =
Collections.unmodifiableSet(
new HashSet<>(
Arrays.asList("a", "b", "c")
)
);
Modern
Set<String> set =
Set.of("a", "b", "c");
hover to see modern →
Collections
Map.entry() factory
Old
Map.Entry<String, Integer> e =
new AbstractMap.SimpleEntry<>(
"key", 42
);
Modern
var e = Map.entry("key", 42);
hover to see modern →
Collections
Reverse list iteration
Old
for (ListIterator<String> it =
list.listIterator(list.size());
it.hasPrevious(); ) {
String element = it.previous();
System.out.println(element);
}
Modern
for (String element : list.reversed()) {
IO.println(element);
}
hover to see modern →
Collections
Sequenced collections
Old
// Get last element
Object last = list.get(list.size() - 1);
// Get first
Object first = list.get(0);
// Reverse iteration: manual
Modern
var last = list.getLast();
var first = list.getFirst();
var reversed = list.reversed();
hover to see modern →
Collections
Typed stream toArray
Old
List<String> list = getNames();
List<String> filtered = new ArrayList<>();
for (String n : list) {
if (n.length() > 3) {
filtered.add(n);
}
}
String[] arr = filtered.toArray(new String[0]);
Modern
String[] arr = getNames().stream()
.filter(n -> n.length() > 3)
.toArray(String[]::new);
hover to see modern →
Collections
Unmodifiable collectors
Old
List<String> list = stream.collect(
Collectors.collectingAndThen(
Collectors.toList(),
Collections::unmodifiableList
)
);
Modern
List<String> list = stream.toList();
hover to see modern →
Strings
String chars as stream
Old
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
if (Character.isDigit(c)) {
process(c);
}
}
Modern
str.chars()
.filter(Character::isDigit)
.forEach(c -> process((char) c));
hover to see modern →
Strings
String.formatted()
Old
String msg = String.format(
"Hello %s, you are %d",
name, age
);
Modern
String msg =
"Hello %s, you are %d"
.formatted(name, age);
hover to see modern →
Strings
String.indent() and transform()
Old
String[] lines = text.split("\n");
StringBuilder sb = new StringBuilder();
for (String line : lines) {
sb.append(" ").append(line)
.append("\n");
}
String indented = sb.toString();
Modern
String indented = text.indent(4);
String result = text
.transform(String::strip)
.transform(s -> s.replace(" ", "-"));
hover to see modern →
Strings
String.isBlank()
Old
boolean blank =
str.trim().isEmpty();
// or: str.trim().length() == 0
Modern
boolean blank = str.isBlank();
// handles Unicode whitespace too
hover to see modern →
Strings
String.lines() for line splitting
Old
String text = "one\ntwo\nthree";
String[] lines = text.split("\n");
for (String line : lines) {
System.out.println(line);
}
Modern
String text = "one\ntwo\nthree";
text.lines().forEach(IO::println);
hover to see modern →
Strings
String.repeat()
Old
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 3; i++) {
sb.append("abc");
}
String result = sb.toString();
Modern
String result = "abc".repeat(3);
// "abcabcabc"
hover to see modern →
Strings
String.strip() vs trim()
Old
// trim() only removes ASCII whitespace
// (chars <= U+0020)
String clean = str.trim();
Modern
// strip() removes all Unicode whitespace
String clean = str.strip();
String left = str.stripLeading();
String right = str.stripTrailing();
hover to see modern →
Streams
Collectors.flatMapping()
Old
// Flatten within a grouping collector
// Required complex custom collector
Map<String, Set<String>> tagsByDept =
// no clean way in Java 8
Modern
var tagsByDept = employees.stream()
.collect(groupingBy(
Emp::dept,
flatMapping(
e -> e.tags().stream(),
toSet()
)
));
hover to see modern →
Streams
Optional.ifPresentOrElse()
Old
Optional<User> user = findUser(id);
if (user.isPresent()) {
greet(user.get());
} else {
handleMissing();
}
Modern
findUser(id).ifPresentOrElse(
this::greet,
this::handleMissing
);
hover to see modern →
Streams
Optional.or() fallback
Old
Optional<Config> cfg = primary();
if (!cfg.isPresent()) {
cfg = secondary();
}
if (!cfg.isPresent()) {
cfg = defaults();
}
Modern
Optional<Config> cfg = primary()
.or(this::secondary)
.or(this::defaults);
hover to see modern →
Streams
Predicate.not() for negation
Old
List<String> nonEmpty = list.stream()
.filter(s -> !s.isBlank())
.collect(Collectors.toList());
Modern
List<String> nonEmpty = list.stream()
.filter(Predicate.not(String::isBlank))
.toList();
hover to see modern →
Streams
Stream gatherers
Old
// Sliding window: manual implementation
List<List<T>> windows = new ArrayList<>();
for (int i = 0; i <= list.size()-3; i++) {
windows.add(
list.subList(i, i + 3));
}
Modern
var windows = stream
.gather(
Gatherers.windowSliding(3)
)
.toList();
hover to see modern →
Streams
Stream.iterate() with predicate
Old
Stream.iterate(1, n -> n * 2)
.limit(10)
.forEach(System.out::println);
// can't stop at a condition
Modern
Stream.iterate(
1,
n -> n < 1000,
n -> n * 2
).forEach(IO::println);
// stops when n >= 1000
hover to see modern →
Streams
Stream.mapMulti()
Old
stream.flatMap(order ->
order.items().stream()
.map(item -> new OrderItem(
order.id(), item)
)
);
Modern
stream.<OrderItem>mapMulti(
(order, downstream) -> {
for (var item : order.items())
downstream.accept(
new OrderItem(order.id(), item));
}
);
hover to see modern →
Streams
Stream.ofNullable()
Old
Stream<String> s = val != null
? Stream.of(val)
: Stream.empty();
Modern
Stream<String> s =
Stream.ofNullable(val);
hover to see modern →
Streams
Stream takeWhile / dropWhile
Old
List<Integer> result = new ArrayList<>();
for (int n : sorted) {
if (n >= 100) break;
result.add(n);
}
// no stream equivalent in Java 8
Modern
var result = sorted.stream()
.takeWhile(n -> n < 100)
.toList();
// or: .dropWhile(n -> n < 10)
hover to see modern →
Streams
Stream.toList()
Old
List<String> result = stream
.filter(s -> s.length() > 3)
.collect(Collectors.toList());
Modern
List<String> result = stream
.filter(s -> s.length() > 3)
.toList();
hover to see modern →
Streams
Virtual thread executor
Old
ExecutorService exec =
Executors.newFixedThreadPool(10);
try {
futures = tasks.stream()
.map(t -> exec.submit(t))
.toList();
} finally {
exec.shutdown();
}
Modern
try (var exec = Executors
.newVirtualThreadPerTaskExecutor()) {
var futures = tasks.stream()
.map(exec::submit)
.toList();
}
hover to see modern →
Concurrency
CompletableFuture chaining
Old
Future<String> future =
executor.submit(this::fetchData);
String data = future.get(); // blocks
String result = transform(data);
Modern
CompletableFuture.supplyAsync(
this::fetchData
)
.thenApply(this::transform)
.thenAccept(IO::println);
hover to see modern →
Concurrency
Concurrent HTTP with virtual threads
Old
ExecutorService pool =
Executors.newFixedThreadPool(10);
List<Future<String>> futures =
urls.stream()
.map(u -> pool.submit(
() -> fetchUrl(u)))
.toList();
// manual shutdown, blocking get()
Modern
try (var exec = Executors
.newVirtualThreadPerTaskExecutor()) {
var results = urls.stream()
.map(u -> exec.submit(
() -> client.send(req(u),
ofString()).body()))
.toList().stream()
.map(Future::join).toList();
}
hover to see modern →
Concurrency
ExecutorService auto-close
Old
ExecutorService exec =
Executors.newCachedThreadPool();
try {
exec.submit(task);
} finally {
exec.shutdown();
exec.awaitTermination(
1, TimeUnit.MINUTES);
}
Modern
try (var exec =
Executors.newCachedThreadPool()) {
exec.submit(task);
}
// auto shutdown + await on close
hover to see modern →
Concurrency
Lock-free lazy initialization
Old
class Config {
private static volatile Config inst;
static Config get() {
if (inst == null) {
synchronized (Config.class) {
if (inst == null)
inst = load();
}
}
return inst;
}
}
Modern
class Config {
private static final
StableValue<Config> INST =
StableValue.of(Config::load);
static Config get() {
return INST.get();
}
}
hover to see modern →
Concurrency
Modern Process API
Old
Process p = Runtime.getRuntime()
.exec("ls -la");
int code = p.waitFor();
// no way to get PID
// no easy process info
Modern
ProcessHandle ph =
ProcessHandle.current();
long pid = ph.pid();
ph.info().command()
.ifPresent(IO::println);
ph.children().forEach(
c -> IO.println(c.pid()));
hover to see modern →
Concurrency
Scoped values
Old
static final ThreadLocal<User> CURRENT =
new ThreadLocal<>();
void handle(Request req) {
CURRENT.set(authenticate(req));
try { process(); }
finally { CURRENT.remove(); }
}
Modern
static final ScopedValue<User> CURRENT =
ScopedValue.newInstance();
void handle(Request req) {
ScopedValue.where(CURRENT,
authenticate(req)
).run(this::process);
}
hover to see modern →
Concurrency
Stable values
Old
private volatile Logger logger;
Logger getLogger() {
if (logger == null) {
synchronized (this) {
if (logger == null)
logger = createLogger();
}
}
return logger;
}
Modern
private final StableValue<Logger> logger =
StableValue.of(this::createLogger);
Logger getLogger() {
return logger.get();
}
hover to see modern →
Concurrency
Structured concurrency
Old
ExecutorService exec =
Executors.newFixedThreadPool(2);
Future<User> u = exec.submit(this::fetchUser);
Future<Order> o = exec.submit(this::fetchOrder);
try {
return combine(u.get(), o.get());
} finally { exec.shutdown(); }
Modern
try (var scope = new StructuredTaskScope
.ShutdownOnFailure()) {
var u = scope.fork(this::fetchUser);
var o = scope.fork(this::fetchOrder);
scope.join().throwIfFailed();
return combine(u.get(), o.get());
}
hover to see modern →
Concurrency
Thread.sleep with Duration
Old
// What unit is 5000? ms? us?
Thread.sleep(5000);
// 2.5 seconds: math required
Thread.sleep(2500);
Modern
Thread.sleep(
Duration.ofSeconds(5)
);
Thread.sleep(
Duration.ofMillis(2500)
);
hover to see modern →
Concurrency
Virtual threads
Old
Thread thread = new Thread(() -> {
System.out.println("hello");
});
thread.start();
thread.join();
Modern
Thread.startVirtualThread(() -> {
IO.println("hello");
}).join();
hover to see modern →
I/O
Deserialization filters
Old
// Dangerous: accepts any class
ObjectInputStream ois =
new ObjectInputStream(input);
Object obj = ois.readObject();
// deserialization attacks possible!
Modern
ObjectInputFilter filter =
ObjectInputFilter.Config
.createFilter(
"com.myapp.*;!*"
);
ois.setObjectInputFilter(filter);
Object obj = ois.readObject();
hover to see modern →
I/O
File memory mapping
Old
try (FileChannel channel =
FileChannel.open(path,
StandardOpenOption.READ,
StandardOpenOption.WRITE)) {
MappedByteBuffer buffer =
channel.map(
FileChannel.MapMode.READ_WRITE,
0, (int) channel.size());
// Limited to 2GB
// Freed by GC, no control
}
Modern
FileChannel channel =
FileChannel.open(path,
StandardOpenOption.READ,
StandardOpenOption.WRITE);
try (Arena arena = Arena.ofShared()) {
MemorySegment segment =
channel.map(
FileChannel.MapMode.READ_WRITE,
0, channel.size(), arena);
// No size limit
// ...
} // Deterministic cleanup
hover to see modern →
I/O
Files.mismatch()
Old
// Compare two files byte by byte
byte[] f1 = Files.readAllBytes(path1);
byte[] f2 = Files.readAllBytes(path2);
boolean equal = Arrays.equals(f1, f2);
// loads both files entirely into memory
Modern
long pos = Files.mismatch(path1, path2);
// -1 if identical
// otherwise: position of first difference
hover to see modern →
I/O
Modern HTTP client
Old
URL url = new URL("https://api.com/data");
HttpURLConnection con =
(HttpURLConnection) url.openConnection();
con.setRequestMethod("GET");
BufferedReader in = new BufferedReader(
new InputStreamReader(con.getInputStream()));
// read lines, close streams...
Modern
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder()
.uri(URI.create("https://api.com/data"))
.build();
var response = client.send(
request, BodyHandlers.ofString());
String body = response.body();
hover to see modern →
I/O
InputStream.transferTo()
Old
byte[] buf = new byte[8192];
int n;
while ((n = input.read(buf)) != -1) {
output.write(buf, 0, n);
}
Modern
input.transferTo(output);
hover to see modern →
I/O
IO class for console I/O
Old
import java.util.Scanner;
Scanner sc = new Scanner(System.in);
System.out.print("Name: ");
String name = sc.nextLine();
System.out.println("Hello, " + name);
sc.close();
Modern
String name = IO.readln("Name: ");
IO.println("Hello, " + name);
hover to see modern →
I/O
Path.of() factory
Old
Path path = Paths.get("src", "main",
"java", "App.java");
Modern
var path = Path.of("src", "main",
"java", "App.java");
hover to see modern →
I/O
Reading files
Old
StringBuilder sb = new StringBuilder();
try (BufferedReader br =
new BufferedReader(
new FileReader("data.txt"))) {
String line;
while ((line = br.readLine()) != null)
sb.append(line).append("\n");
}
String content = sb.toString();
Modern
String content =
Files.readString(Path.of("data.txt"));
hover to see modern →
I/O
Try-with-resources improvement
Old
Connection conn = getConnection();
// Must re-declare in try
try (Connection c = conn) {
use(c);
}
Modern
Connection conn = getConnection();
// Use existing variable directly
try (conn) {
use(conn);
}
hover to see modern →
I/O
Writing files
Old
try (FileWriter fw =
new FileWriter("out.txt");
BufferedWriter bw =
new BufferedWriter(fw)) {
bw.write(content);
}
Modern
Files.writeString(
Path.of("out.txt"),
content
);
hover to see modern →
Errors
Helpful NullPointerExceptions
Old
// Old NPE message:
// "NullPointerException"
// at MyApp.main(MyApp.java:42)
// Which variable was null?!
Modern
// Modern NPE message:
// Cannot invoke "String.length()"
// because "user.address().city()"
// is null
// Exact variable identified!
hover to see modern →
Errors
Multi-catch exception handling
Old
try {
process();
} catch (IOException e) {
log(e);
} catch (SQLException e) {
log(e);
} catch (ParseException e) {
log(e);
}
Modern
try {
process();
} catch (IOException
| SQLException
| ParseException e) {
log(e);
}
hover to see modern →
Errors
Null case in switch
Old
// Must check before switch
if (status == null) {
return "unknown";
}
return switch (status) {
case ACTIVE -> "active";
case PAUSED -> "paused";
default -> "other";
};
Modern
return switch (status) {
case null -> "unknown";
case ACTIVE -> "active";
case PAUSED -> "paused";
default -> "other";
};
hover to see modern →
Errors
Optional chaining
Old
String city = null;
if (user != null) {
Address addr = user.getAddress();
if (addr != null) {
city = addr.getCity();
}
}
if (city == null) city = "Unknown";
Modern
String city = Optional.ofNullable(user)
.map(User::address)
.map(Address::city)
.orElse("Unknown");
hover to see modern →
Errors
Optional.orElseThrow() without supplier
Old
// Risky: get() throws if empty, no clear intent
String value = optional.get();
// Verbose: supplier just for NoSuchElementException
String value = optional
.orElseThrow(NoSuchElementException::new);
Modern
// Clear intent: throws NoSuchElementException if empty
String value = optional.orElseThrow();
hover to see modern →
Errors
Record-based error responses
Old
// Verbose error class
public class ErrorResponse {
private final int code;
private final String message;
// constructor, getters, equals,
// hashCode, toString...
}
Modern
public record ApiError(
int code,
String message,
Instant timestamp
) {
public ApiError(int code, String msg) {
this(code, msg, Instant.now());
}
}
hover to see modern →
Errors
Objects.requireNonNullElse()
Old
String name = input != null
? input
: "default";
// easy to get the order wrong
Modern
String name = Objects
.requireNonNullElse(
input, "default"
);
hover to see modern →
Date/Time
Date formatting
Old
// Not thread-safe!
SimpleDateFormat sdf =
new SimpleDateFormat("yyyy-MM-dd");
String formatted = sdf.format(date);
// Must synchronize for concurrent use
Modern
DateTimeFormatter fmt =
DateTimeFormatter.ofPattern(
"uuuu-MM-dd");
String formatted =
LocalDate.now().format(fmt);
// Thread-safe, immutable
hover to see modern →
Date/Time
Duration and Period
Old
// How many days between two dates?
long diff = date2.getTime()
- date1.getTime();
long days = diff
/ (1000 * 60 * 60 * 24);
// ignores DST, leap seconds
Modern
long days = ChronoUnit.DAYS
.between(date1, date2);
Period period = Period.between(
date1, date2);
Duration elapsed = Duration.between(
time1, time2);
hover to see modern →
Date/Time
HexFormat
Old
// Pad to 2 digits, uppercase
String hex = String.format(
"%02X", byteValue);
// Parse hex string
int val = Integer.parseInt(
"FF", 16);
Modern
var hex = HexFormat.of()
.withUpperCase();
String s = hex.toHexDigits(
byteValue);
byte[] bytes =
hex.parseHex("48656C6C6F");
hover to see modern →
Date/Time
Instant with nanosecond precision
Old
// Millisecond precision only
long millis =
System.currentTimeMillis();
// 1708012345678
Modern
// Microsecond/nanosecond precision
Instant now = Instant.now();
// 2025-02-15T20:12:25.678901234Z
long nanos = now.getNano();
hover to see modern →
Date/Time
java.time API basics
Old
// Mutable, confusing, zero-indexed months
Calendar cal = Calendar.getInstance();
cal.set(2025, 0, 15); // January = 0!
Date date = cal.getTime();
// not thread-safe
Modern
LocalDate date = LocalDate.of(
2025, Month.JANUARY, 15);
LocalTime time = LocalTime.of(14, 30);
Instant now = Instant.now();
// immutable, thread-safe
hover to see modern →
Date/Time
Math.clamp()
Old
// Clamp value between min and max
int clamped =
Math.min(Math.max(value, 0), 100);
// or: min and max order confusion
Modern
int clamped =
Math.clamp(value, 0, 100);
// value constrained to [0, 100]
hover to see modern →
Security
Key Derivation Functions
Old
SecretKeyFactory factory =
SecretKeyFactory.getInstance(
"PBKDF2WithHmacSHA256");
KeySpec spec = new PBEKeySpec(
password, salt, 10000, 256);
SecretKey key =
factory.generateSecret(spec);
Modern
var kdf = KDF.getInstance("HKDF-SHA256");
SecretKey key = kdf.deriveKey(
"AES",
KDF.HKDFParameterSpec
.ofExtract()
.addIKM(inputKey)
.addSalt(salt)
.thenExpand(info, 32)
.build()
);
hover to see modern →
Security
PEM encoding/decoding
Old
String pem = "-----BEGIN CERTIFICATE-----\n"
+ Base64.getMimeEncoder()
.encodeToString(
cert.getEncoded())
+ "\n-----END CERTIFICATE-----";
Modern
// Encode to PEM
String pem = PEMEncoder.of()
.encodeToString(cert);
// Decode from PEM
var cert = PEMDecoder.of()
.decode(pemString);
hover to see modern →
Security
RandomGenerator interface
Old
// Hard-coded to one algorithm
Random rng = new Random();
int value = rng.nextInt(100);
// Or thread-local, but still locked in
int value = ThreadLocalRandom.current()
.nextInt(100);
Modern
// Algorithm-agnostic via factory
var rng = RandomGenerator.of("L64X128MixRandom");
int value = rng.nextInt(100);
// Or get a splittable generator
var rng = RandomGeneratorFactory
.of("L64X128MixRandom").create();
hover to see modern →
Security
Strong random generation
Old
// Default algorithm — may not be
// the strongest available
SecureRandom random =
new SecureRandom();
byte[] bytes = new byte[32];
random.nextBytes(bytes);
Modern
// Platform's strongest algorithm
SecureRandom random =
SecureRandom.getInstanceStrong();
byte[] bytes = new byte[32];
random.nextBytes(bytes);
hover to see modern →
Security
TLS 1.3 by default
Old
SSLContext ctx =
SSLContext.getInstance("TLSv1.2");
ctx.init(null, trustManagers, null);
SSLSocketFactory factory =
ctx.getSocketFactory();
// Must specify protocol version
Modern
// TLS 1.3 is the default!
var client = HttpClient.newBuilder()
.sslContext(SSLContext.getDefault())
.build();
// Already using TLS 1.3
hover to see modern →
Tooling
AOT class preloading
Old
// Every startup:
// - Load 10,000+ classes
// - Verify bytecode
// - JIT compile hot paths
// Startup: 2-5 seconds
Modern
// Training run:
$ java -XX:AOTCacheOutput=app.aot \
-cp app.jar com.App
// Production:
$ java -XX:AOTCache=app.aot \
-cp app.jar com.App
hover to see modern →
Tooling
Built-in HTTP server
Old
// Install and configure a web server
// (Apache, Nginx, or embedded Jetty)
// Or write boilerplate with com.sun.net.httpserver
HttpServer server = HttpServer.create(
new InetSocketAddress(8080), 0);
server.createContext("/", exchange -> { ... });
server.start();
Modern
// Terminal: serve current directory
$ jwebserver
// Or use the API (JDK 18+)
var server = SimpleFileServer.createFileServer(
new InetSocketAddress(8080),
Path.of("."),
OutputLevel.VERBOSE);
server.start();
hover to see modern →
Tooling
Compact object headers
Old
// Default: 128-bit object header
// = 16 bytes overhead per object
// A boolean field object = 32 bytes!
// Mark word (64) + Klass pointer (64)
Modern
// -XX:+UseCompactObjectHeaders
// 64-bit object header
// = 8 bytes overhead per object
// 50% less header memory
// More objects fit in cache
hover to see modern →
Tooling
JFR for profiling
Old
// Install VisualVM / YourKit / JProfiler
// Attach to running process
// Configure sampling
// Export and analyze
// External tool required
Modern
// Start with profiling enabled
$ java -XX:StartFlightRecording=
filename=rec.jfr MyApp
// Or attach to running app:
$ jcmd <pid> JFR.start
hover to see modern →
Tooling
JShell for prototyping
Old
// 1. Create Test.java
// 2. javac Test.java
// 3. java Test
// Just to test one expression!
Modern
$ jshell
jshell> "hello".chars().count()
$1 ==> 5
jshell> List.of(1,2,3).reversed()
$2 ==> [3, 2, 1]
hover to see modern →
Tooling
JUnit 6 with JSpecify null safety
Old
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class UserServiceTest {
// JUnit 5: no null contracts on the API
// Can assertEquals() accept null? Check source...
// Does fail(String) allow null message? Unknown.
@Test
void findUser_found() {
// Is result nullable? API doesn't say
User result = service.findById("u1");
assertNotNull(result);
assertEquals("Alice", result.name());
}
@Test
void findUser_notFound() {
// Hope this returns null, not throws...
assertNull(service.findById("missing"));
}
}
Modern
import org.junit.jupiter.api.Test;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
import static org.junit.jupiter.api.Assertions.*;
@NullMarked // all refs non-null unless @Nullable
class UserServiceTest {
// JUnit 6 API is @NullMarked:
// assertNull(@Nullable Object actual)
// assertEquals(@Nullable Object, @Nullable Object)
// fail(@Nullable String message)
@Test
void findUser_found() {
// IDE warns: findById returns @Nullable User
@Nullable User result = service.findById("u1");
assertNotNull(result); // narrows type to non-null
assertEquals("Alice", result.name()); // safe
}
@Test
void findUser_notFound() {
@Nullable User result = service.findById("missing");
assertNull(result); // IDE confirms null expectation
}
}
hover to see modern →
Tooling
Multi-file source launcher
Old
$ javac *.java
$ java Main
// Must compile all files first
// Need a build tool for dependencies
Modern
$ java Main.java
// Automatically finds and compiles
// other source files referenced
// by Main.java
hover to see modern →
Tooling
Single-file execution
Old
$ javac HelloWorld.java
$ java HelloWorld
// Two steps every time
Modern
$ java HelloWorld.java
// Compiles and runs in one step
// Also works with shebangs:
#!/usr/bin/java --source 25
hover to see modern →
Enterprise
EJB Timer vs Jakarta Scheduler
Old
@Stateless
public class ReportGenerator {
@Resource
TimerService timerService;
@PostConstruct
public void init() {
timerService.createCalendarTimer(
new ScheduleExpression()
.hour("2").minute("0"));
}
@Timeout
public void generateReport(Timer timer) {
// runs every day at 02:00
buildDailyReport();
}
}
Modern
@ApplicationScoped
public class ReportGenerator {
@Resource
ManagedScheduledExecutorService scheduler;
@PostConstruct
public void init() {
scheduler.scheduleAtFixedRate(
this::generateReport,
0, 24, TimeUnit.HOURS);
}
public void generateReport() {
buildDailyReport();
}
}
hover to see modern →
Enterprise
EJB versus CDI
Old
@Stateless
public class OrderEJB {
@EJB
private InventoryEJB inventory;
public void placeOrder(Order order) {
// container-managed transaction
inventory.reserve(order.getItem());
}
}
Modern
@ApplicationScoped
public class OrderService {
@Inject
private InventoryService inventory;
@Transactional
public void placeOrder(Order order) {
inventory.reserve(order.getItem());
}
}
hover to see modern →
Enterprise
JDBC ResultSet Mapping vs JPA Criteria API
Old
String sql = "SELECT * FROM users"
+ " WHERE status = ? AND age > ?";
try (Connection con = ds.getConnection();
PreparedStatement ps =
con.prepareStatement(sql)) {
ps.setString(1, status);
ps.setInt(2, minAge);
ResultSet rs = ps.executeQuery();
List<User> users = new ArrayList<>();
while (rs.next()) {
User u = new User();
u.setId(rs.getLong("id"));
u.setName(rs.getString("name"));
u.setAge(rs.getInt("age"));
users.add(u);
}
}
Modern
@PersistenceContext
EntityManager em;
public List<User> findActiveAboveAge(
String status, int minAge) {
var cb = em.getCriteriaBuilder();
var cq =
cb.createQuery(User.class);
var root = cq.from(User.class);
cq.select(root).where(
cb.equal(root.get("status"), status),
cb.greaterThan(root.get("age"), minAge));
return em.createQuery(cq).getResultList();
}
hover to see modern →
Enterprise
JDBC versus jOOQ
Old
String sql = "SELECT id, name, email FROM users "
+ "WHERE department = ? AND salary > ?";
try (Connection con = ds.getConnection();
PreparedStatement ps =
con.prepareStatement(sql)) {
ps.setString(1, department);
ps.setBigDecimal(2, minSalary);
ResultSet rs = ps.executeQuery();
List<User> result = new ArrayList<>();
while (rs.next()) {
result.add(new User(
rs.getLong("id"),
rs.getString("name"),
rs.getString("email")));
}
return result;
}
Modern
DSLContext dsl = DSL.using(ds, SQLDialect.POSTGRES);
return dsl
.select(USERS.ID, USERS.NAME, USERS.EMAIL)
.from(USERS)
.where(USERS.DEPARTMENT.eq(department)
.and(USERS.SALARY.gt(minSalary)))
.fetchInto(User.class);
hover to see modern →
Enterprise
JDBC versus JPA
Old
String sql = "SELECT * FROM users WHERE id = ?";
try (Connection con = dataSource.getConnection();
PreparedStatement ps =
con.prepareStatement(sql)) {
ps.setLong(1, id);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
User u = new User();
u.setId(rs.getLong("id"));
u.setName(rs.getString("name"));
}
}
Modern
@PersistenceContext
EntityManager em;
public User findUser(Long id) {
return em.find(User.class, id);
}
public List<User> findByName(String name) {
return em.createQuery(
"SELECT u FROM User u WHERE u.name = :name",
User.class)
.setParameter("name", name)
.getResultList();
}
hover to see modern →
Enterprise
JNDI Lookup vs CDI Injection
Old
public class OrderService {
private DataSource ds;
public void init() throws NamingException {
InitialContext ctx = new InitialContext();
ds = (DataSource) ctx.lookup(
"java:comp/env/jdbc/OrderDB");
}
public List<Order> findAll()
throws SQLException {
try (Connection con = ds.getConnection()) {
// query orders
}
}
}
Modern
@ApplicationScoped
public class OrderService {
@Inject
@Resource(name = "jdbc/OrderDB")
DataSource ds;
public List<Order> findAll()
throws SQLException {
try (Connection con = ds.getConnection()) {
// query orders
}
}
}
hover to see modern →
Enterprise
JPA versus Jakarta Data
Old
@PersistenceContext
EntityManager em;
public User findById(Long id) {
return em.find(User.class, id);
}
public List<User> findByName(String name) {
return em.createQuery(
"SELECT u FROM User u WHERE u.name = :name",
User.class)
.setParameter("name", name)
.getResultList();
}
public void save(User user) {
em.persist(user);
}
Modern
@Repository
public interface Users extends CrudRepository<User, Long> {
List<User> findByName(String name);
}
hover to see modern →
Enterprise
JSF Managed Bean vs CDI Named Bean
Old
@ManagedBean
@SessionScoped
public class UserBean implements Serializable {
@ManagedProperty("#{userService}")
private UserService userService;
private String name;
public String getName() { return name; }
public void setName(String name) {
this.name = name;
}
public void setUserService(UserService svc) {
this.userService = svc;
}
}
Modern
@Named
@SessionScoped
public class UserBean implements Serializable {
@Inject
private UserService userService;
private String name;
public String getName() { return name; }
public void setName(String name) {
this.name = name;
}
}
hover to see modern →
Enterprise
Manual JPA Transaction vs Declarative @Transactional
Old
@PersistenceContext
EntityManager em;
public void transferFunds(Long from, Long to,
BigDecimal amount) {
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Account src = em.find(Account.class, from);
Account dst = em.find(Account.class, to);
src.debit(amount);
dst.credit(amount);
tx.commit();
} catch (Exception e) {
tx.rollback();
throw e;
}
}
Modern
@ApplicationScoped
public class AccountService {
@PersistenceContext
EntityManager em;
@Transactional
public void transferFunds(Long from, Long to,
BigDecimal amount) {
var src = em.find(Account.class, from);
var dst = em.find(Account.class, to);
src.debit(amount);
dst.credit(amount);
}
}
hover to see modern →
Enterprise
Message-Driven Bean vs Reactive Messaging
Old
@MessageDriven(activationConfig = {
@ActivationConfigProperty(
propertyName = "destinationType",
propertyValue = "jakarta.jms.Queue"),
@ActivationConfigProperty(
propertyName = "destination",
propertyValue = "java:/jms/OrderQueue")
})
public class OrderMDB implements MessageListener {
@Override
public void onMessage(Message message) {
TextMessage txt = (TextMessage) message;
processOrder(txt.getText());
}
}
Modern
@ApplicationScoped
public class OrderProcessor {
@Incoming("orders")
public void process(Order order) {
// automatically deserialized from
// the "orders" channel
fulfillOrder(order);
}
}
hover to see modern →
Enterprise
Servlet versus JAX-RS
Old
@WebServlet("/users")
public class UserServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req,
HttpServletResponse res)
throws ServletException, IOException {
String id = req.getParameter("id");
res.setContentType("application/json");
res.getWriter().write("{\"id\":\"" + id + "\"}");
}
}
Modern
@Path("/users")
public class UserResource {
@GET
@Produces(MediaType.APPLICATION_JSON)
public Response getUser(
@QueryParam("id") String id) {
return Response.ok(new User(id)).build();
}
}
hover to see modern →
Enterprise
Singleton EJB vs CDI @ApplicationScoped
Old
@Singleton
@Startup
@ConcurrencyManagement(
ConcurrencyManagementType.CONTAINER)
public class ConfigCache {
private Map<String, String> cache;
@PostConstruct
public void load() {
cache = loadFromDatabase();
}
@Lock(LockType.READ)
public String get(String key) {
return cache.get(key);
}
@Lock(LockType.WRITE)
public void refresh() {
cache = loadFromDatabase();
}
}
Modern
@ApplicationScoped
public class ConfigCache {
private volatile Map<String, String> cache;
@PostConstruct
public void load() {
cache = loadFromDatabase();
}
public String get(String key) {
return cache.get(key);
}
public void refresh() {
cache = loadFromDatabase();
}
}
hover to see modern →
Enterprise
SOAP Web Services vs Jakarta REST
Old
@WebService
public class UserWebService {
@WebMethod
public UserResponse getUser(
@WebParam(name = "id") String id) {
User user = findUser(id);
UserResponse res = new UserResponse();
res.setId(user.getId());
res.setName(user.getName());
return res;
}
}
Modern
@Path("/users")
@Produces(MediaType.APPLICATION_JSON)
public class UserResource {
@Inject
UserService userService;
@GET
@Path("/{id}")
public User getUser(@PathParam("id") String id) {
return userService.findById(id);
}
}
hover to see modern →
Enterprise
Spring Framework 7 API Versioning
Old
// Version 1 controller
@RestController
@RequestMapping("/api/v1/products")
public class ProductControllerV1 {
@GetMapping("/{id}")
public ProductDtoV1 getProduct(
@PathVariable Long id) {
return service.getV1(id);
}
}
// Version 2 — duplicated structure
@RestController
@RequestMapping("/api/v2/products")
public class ProductControllerV2 {
@GetMapping("/{id}")
public ProductDtoV2 getProduct(
@PathVariable Long id) {
return service.getV2(id);
}
}
Modern
// Configure versioning once
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureApiVersioning(
ApiVersionConfigurer config) {
config.useRequestHeader("X-API-Version");
}
}
// Single controller, version per method
@RestController
@RequestMapping("/api/products")
public class ProductController {
@GetMapping(value = "/{id}", version = "1")
public ProductDtoV1 getV1(@PathVariable Long id) {
return service.getV1(id);
}
@GetMapping(value = "/{id}", version = "2")
public ProductDtoV2 getV2(@PathVariable Long id) {
return service.getV2(id);
}
}
hover to see modern →
Enterprise
Spring Null Safety with JSpecify
Old
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
public class UserService {
@Nullable
public User findById(@NonNull String id) {
return repository.findById(id).orElse(null);
}
@NonNull
public List<User> findAll() {
return repository.findAll();
}
@NonNull
public User save(@NonNull User user) {
return repository.save(user);
}
}
Modern
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@NullMarked
public class UserService {
public @Nullable User findById(String id) {
return repository.findById(id).orElse(null);
}
public List<User> findAll() {
return repository.findAll();
}
public User save(User user) {
return repository.save(user);
}
}
hover to see modern →
Enterprise
Spring XML Bean Config vs Annotation-Driven
Old
<!-- applicationContext.xml -->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="userRepository"
class="com.example.UserRepository">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="userService"
class="com.example.UserService">
<property name="repository" ref="userRepository"/>
</bean>
</beans>
Modern
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@Repository
public class UserRepository {
private final JdbcTemplate jdbc;
public UserRepository(JdbcTemplate jdbc) {
this.jdbc = jdbc;
}
}
@Service
public class UserService {
private final UserRepository repository;
public UserService(UserRepository repository) {
this.repository = repository;
}
}
hover to see modern →
