Java's evolution: Less verbose, more expressive, still type-safe
Records eliminate boilerplate for immutable data classes. Perfect for DTOs, value objects, and API responses.
// 50+ lines of boilerplate for a simple data class public final class User { private final String id; private final String name; private final String email; public User(String id, String name, String email) { this.id = id; this.name = name; this.email = email; } public String getId() { return id; } public String getName() { return name; } public String getEmail() { return email; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; User user = (User) o; return Objects.equals(id, user.id) && Objects.equals(name, user.name) && Objects.equals(email, user.email); } @Override public int hashCode() { return Objects.hash(id, name, email); } @Override public String toString() { return "User[id=" + id + ", name=" + name + ", email=" + email + "]"; } }
// One line! Gets: constructor, getters, equals, hashCode, toString public record User(String id, String name, String email) { } // Usage User user = new User("1", "Alice", "alice@example.com"); user.name(); // "Alice" (not getName()!) user.email(); // "alice@example.com" // With validation public record User(String id, String name, String email) { public User { // Compact constructor Objects.requireNonNull(id, "id required"); Objects.requireNonNull(name, "name required"); if (!email.contains("@")) { throw new IllegalArgumentException("Invalid email"); } } } // With additional methods public record Point(double x, double y) { public double distanceFromOrigin() { return Math.sqrt(x * x + y * y); } public Point translate(double dx, double dy) { return new Point(x + dx, y + dy); // Records are immutable } }
# Python: always inferred names = ["Alice", "Bob"] # list user = get_user(123) # User result = process(data) # unknown until runtime
// Java 10+: var for local variables (still type-safe!) // Before: redundant type on both sides List<String> names = new ArrayList<String>(); Map<String, List<User>> usersByDept = new HashMap<String, List<User>>(); // After: var infers the type var names = new ArrayList<String>(); // type: ArrayList<String> var usersByDept = new HashMap<String, List<User>>(); // Great for complex types var stream = list.stream() .filter(x -> x.isActive()) .map(User::getName); // IDE knows exact type // Still type-safe! This is a compile error: var name = "Alice"; name = 42; // COMPILE ERROR: incompatible types // When NOT to use var: var result = getResult(); // Unclear what type result is UserResponse result = getResult(); // Clear! // var is local variables only - not fields, parameters, or return types
// Old way: instanceof + cast if (obj instanceof String) { String s = (String) obj; // Redundant cast System.out.println(s.length()); } // New way: pattern matching for instanceof (Java 16+) if (obj instanceof String s) { System.out.println(s.length()); // s already typed! } // Can use in conditions if (obj instanceof String s && s.length() > 5) { System.out.println("Long string: " + s); } // Guards with && if (obj instanceof Integer i && i > 0) { System.out.println("Positive: " + i); }
// Switch expressions (Java 14+) - return values! // Old switch statement String result; switch (day) { case MONDAY: case FRIDAY: result = "Busy"; break; case SATURDAY: case SUNDAY: result = "Weekend"; break; default: result = "Normal"; } // New switch expression String result = switch (day) { case MONDAY, FRIDAY -> "Busy"; case SATURDAY, SUNDAY -> "Weekend"; default -> "Normal"; }; // Pattern matching in switch (Java 21+) String describe(Object obj) { return switch (obj) { case Integer i -> "Integer: " + i; case Long l -> "Long: " + l; case String s -> "String: " + s; case null -> "null"; default -> "Unknown"; }; } // With guards (Java 21+) String format(Object obj) { return switch (obj) { case String s when s.isEmpty() -> "Empty string"; case String s -> "String: " + s; case Integer i when i < 0 -> "Negative"; case Integer i -> "Positive or zero"; default -> "Other"; }; }
// Record patterns: destructure records in patterns (Java 21+) record Point(int x, int y) {} record Circle(Point center, int radius) {} // Destructure in instanceof if (shape instanceof Circle(Point(var x, var y), var r)) { System.out.println("Circle at (" + x + "," + y + ") with radius " + r); } // Destructure in switch String describe(Object shape) { return switch (shape) { case Circle(Point(var x, var y), var r) when r > 10 -> "Large circle at (" + x + "," + y + ")"; case Circle(Point(var x, var y), var r) -> "Small circle at (" + x + "," + y + ")"; case Point(var x, var y) -> "Point at (" + x + "," + y + ")"; default -> "Unknown shape"; }; }
// Control exactly which classes can extend/implement // Compiler knows ALL possible subtypes public sealed interface Shape permits Circle, Rectangle, Triangle { double area(); } public record Circle(double radius) implements Shape { @Override public double area() { return Math.PI * radius * radius; } } public record Rectangle(double width, double height) implements Shape { @Override public double area() { return width * height; } } public record Triangle(double base, double height) implements Shape { @Override public double area() { return 0.5 * base * height; } } // Exhaustive switch - compiler knows all cases! String describe(Shape shape) { return switch (shape) { case Circle c -> "Circle with radius " + c.radius(); case Rectangle r -> "Rectangle " + r.width() + "x" + r.height(); case Triangle t -> "Triangle with base " + t.base(); // No default needed! Compiler knows these are ALL cases }; } // If you add a new Shape subtype, ALL switches must be updated // This is caught at COMPILE TIME, not runtime!
# Python triple quotes json_template = """ { "name": "%s", "email": "%s" } """ sql = """ SELECT u.id, u.name FROM users u WHERE u.active = true """
// Old way: escape hell String json = "{\n" + " \"name\": \"" + name + "\",\n" + " \"email\": \"" + email + "\"\n" + "}"; // Text blocks (Java 15+) String json = """ { "name": "%s", "email": "%s" } """.formatted(name, email); // Great for SQL String sql = """ SELECT u.id, u.name, u.email FROM users u JOIN orders o ON u.id = o.user_id WHERE u.active = true AND o.created_at > :since ORDER BY o.created_at DESC """; // Great for HTML/templates String html = """ <html> <body> <h1>Hello, %s!</h1> </body> </html> """.formatted(username); // Trailing backslash continues line without newline String oneLine = """ This is a very long line that I want to \ wrap in source but not in the output""";
// Traditional threads: expensive, limited (thousands) for (int i = 0; i < 10000; i++) { new Thread(() -> doWork()).start(); // May exhaust OS threads } // Virtual threads: cheap, unlimited (millions!) for (int i = 0; i < 1_000_000; i++) { Thread.startVirtualThread(() -> doWork()); // No problem! } // Using ExecutorService try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { for (int i = 0; i < 10000; i++) { executor.submit(() -> { // Each task gets its own virtual thread String result = fetchFromApi(); // Blocking IO is fine! processResult(result); }); } } // Waits for all tasks // Why this matters: // - Write simple blocking code (like Python) // - Get async performance automatically // - No callback hell, no reactive complexity // - One virtual thread per request at Twilio scale = trivial // Spring Boot 3.2+ supports virtual threads out of the box // spring.threads.virtual.enabled=true
Virtual threads change everything for high-concurrency systems. You can now:
This is likely a hot topic for a Twilio Distinguished Architect interview.
// Old NPE (before Java 14) // Exception in thread "main" java.lang.NullPointerException // at Example.main(Example.java:15) // Which part was null?! // New NPE (Java 14+) // Exception in thread "main" java.lang.NullPointerException: // Cannot invoke "String.toUpperCase()" because the return value // of "User.getName()" is null String city = user.getAddress().getCity().toUpperCase(); // Now tells you EXACTLY which part was null: // - user was null? // - getAddress() returned null? // - getCity() returned null?
// toList() instead of collect(Collectors.toList()) - Java 16+ List<String> names = users.stream() .map(User::getName) .toList(); // Returns immutable list // mapMulti() - flatMap alternative (Java 16+) List<Integer> numbers = Stream.of(1, 2, 3) .<Integer>mapMulti((n, consumer) -> { consumer.accept(n); consumer.accept(n * 10); }) .toList(); // [1, 10, 2, 20, 3, 30] // takeWhile/dropWhile (Java 9+) List<Integer> nums = List.of(1, 2, 3, 4, 5); nums.stream() .takeWhile(n -> n < 4) .toList(); // [1, 2, 3] nums.stream() .dropWhile(n -> n < 4) .toList(); // [4, 5] // Collectors.teeing() - two collectors at once (Java 12+) var result = Stream.of(1, 2, 3, 4, 5).collect( Collectors.teeing( Collectors.summingInt(Integer::intValue), // sum Collectors.counting(), // count (sum, count) -> sum / count // average ) ); // 3
| Version | Key Features | LTS? |
|---|---|---|
| Java 8 (2014) | Lambdas, Streams, Optional, new Date/Time API | Yes |
| Java 9 | Modules (JPMS), JShell, Stream improvements | No |
| Java 10 | var (local type inference) | No |
| Java 11 (2018) | HTTP Client, String methods | Yes |
| Java 14 | Records (preview), Switch expressions | No |
| Java 15 | Text blocks, Sealed classes (preview) | No |
| Java 16 | Records (final), Pattern matching instanceof | No |
| Java 17 (2021) | Sealed classes (final), Pattern matching switch (preview) | Yes |
| Java 21 (2023) | Virtual threads, Record patterns, Pattern matching (final) | Yes |
For a Distinguished Architect role, you should know: