✦ 86 modern patterns · Java 8 → Java 25

Java has evolved.
Your code can too.

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) {}
Share 𝕏 🦋 in
🤖
Modernize your Java codebase with GitHub Copilot. Let Copilot help you migrate legacy patterns to modern Java — automatically.

All comparisons

86 snippets
Filter:
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

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

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

Pattern matching for instanceof

Old if (obj instanceof String) { String s = (String) obj; System.out.println(s.length()); }
Modern if (obj instanceof String s) { System.out.println(s.length()); }
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

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)) { System.out.println(x + y); }
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 →
Language

Compact source files

Old public class HelloWorld { public static void main( String[] args) { System.out.println( "Hello, World!"); } }
Modern void main() { println("Hello, World!"); }
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

Enhanced for with var

Old List<Map.Entry<String, Integer>> list = ...; for (Map.Entry<String, Integer> entry : list) { process(entry); }
Modern var list = getEntries(); for (var entry : list) { process(entry); }
hover to see modern →
Language

Diamond operator

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

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) { System.out.println(format("INFO", msg)); } default void logWarn(String msg) { System.out.println(format("WARN", msg)); } }
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

Guarded patterns with when

Old if (shape instanceof Circle c) { 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

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

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

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 →
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

Copying collections immutably

Old List<String> copy = Collections.unmodifiableList( new ArrayList<>(original) );
Modern List<String> copy = List.copyOf(original);
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

Sequenced collections

Old // Get last element var last = list.get(list.size() - 1); // Get first var 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

Stream toList() shorthand

Old List<String> names = people.stream() .map(Person::name) .collect(Collectors.toList());
Modern List<String> names = people.stream() .map(Person::name) .toList();
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

Typed stream toArray

Old List<String> list = getNames(); String[] arr = new String[list.size()]; for (int i = 0; i < list.size(); i++) { arr[i] = list.get(i); }
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.collect( Collectors.toUnmodifiableList() );
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.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 →
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.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.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

Multiline JSON/SQL/HTML

Old String sql = "SELECT u.name, u.email\n" + "FROM users u\n" + "WHERE u.active = true\n" + "ORDER BY u.name";
Modern String sql = """ SELECT u.name, u.email FROM users u WHERE u.active = true ORDER BY u.name """;
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 →
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.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(System.out::println); // stops when n >= 1000
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

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

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

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 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

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 →
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 →
Concurrency

Virtual threads

Old Thread thread = new Thread(() -> { System.out.println("hello"); }); thread.start(); thread.join();
Modern Thread.startVirtualThread(() -> { System.out.println("hello"); }).join();
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

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

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(System.out::println);
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

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

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(System.out::println); ph.children().forEach( c -> System.out.println(c.pid()));
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

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 →
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

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

Writing files

Old try (BufferedWriter bw = new BufferedWriter( new FileWriter("out.txt"))) { bw.write(content); }
Modern Files.writeString( Path.of("out.txt"), content );
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

Path.of() factory

Old Path path = Paths.get("src", "main", "java", "App.java");
Modern Path path = Path.of("src", "main", "java", "App.java");
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

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

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 →
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

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

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 →
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

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 →
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

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

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( "yyyy-MM-dd"); String formatted = LocalDate.now().format(fmt); // Thread-safe, immutable
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

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 →
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 HexFormat hex = HexFormat.of() .withUpperCase(); String s = hex.toHexDigits( byteValue); byte[] bytes = hex.parseHex("48656C6C6F");
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

Key Derivation Functions

Old SecretKeyFactory factory = SecretKeyFactory.getInstance( "PBKDF2WithHmacSHA256"); KeySpec spec = new PBEKeySpec( password, salt, 10000, 256); SecretKey key = factory.generateSecret(spec);
Modern KDF 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

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

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

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 →
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

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

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

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:AOTMode=record \ -XX:AOTConfiguration=app.aotconf \ -cp app.jar com.App // Production: $ java -XX:AOTMode=on \ -XX:AOTCache=app.aot \ -cp app.jar com.App
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) { System.out.println( timestamp() + ": " + msg); } String timestamp(); } // Multiple interfaces allowed public class FileLogger implements Logger, Closeable { ... }
hover to see modern →
86
Modern Patterns
17
JDK Versions Covered
10
Categories
0
Lines of JS Required
ESC