100 Java Programming Questions — Java 8, 11, 17 & 21
A comprehensive hands-on collection covering Stream API, Optional, Records, Sealed Classes, Pattern Matching, Virtual Threads and Sequenced Collections. Every question has complete, runnable solutions — often with multiple approaches compared.
How to use this guide
Questions are organized by Java version and topic. Click any question card to expand the solution. Use the filter buttons to focus on a specific version, or the search box to find a keyword. All 100 questions are answered in depth — most include the "naive" approach, the idiomatic Stream/functional approach, and performance notes where relevant.
Employee model & sample data Reference
All Java 8 Stream API questions below use this Employee class and sample list. Bookmark this section — we'll reference it throughout.
java — Employee.javapublic class Employee {
private int id;
private String name;
private int age;
private long salary;
private String gender;
private String deptName;
private String city;
private int yearOfJoining;
// standard constructor, getters, equals, hashCode, toString
}
java — sample list used in every Stream questionList<Employee> employees = List.of(
new Employee(101, "Arjun", 32, 120000, "M", "Engineering", "Bangalore", 2016),
new Employee(102, "Priya", 28, 95000, "F", "Engineering", "Bangalore", 2019),
new Employee(103, "Rohit", 45, 185000, "M", "Engineering", "Hyderabad", 2012),
new Employee(104, "Sneha", 35, 140000, "F", "Finance", "Mumbai", 2015),
new Employee(105, "Vikram", 29, 78000, "M", "Finance", "Mumbai", 2020),
new Employee(106, "Anita", 41, 160000, "F", "HR", "Bangalore", 2014),
new Employee(107, "Karthik",26, 65000, "M", "HR", "Bangalore", 2021),
new Employee(108, "Divya", 38, 155000, "F", "Sales", "Delhi", 2013),
new Employee(109, "Manoj", 33, 110000, "M", "Sales", "Delhi", 2017),
new Employee(110, "Kavya", 30, 125000, "F", "Sales", "Mumbai", 2018),
new Employee(111, "Ravi", 52, 220000, "M", "Engineering", "Hyderabad", 2010),
new Employee(112, "Meera", 27, 88000, "F", "Engineering", "Bangalore", 2022)
);
Filtering & mapping Java 8 · Q1-15
Basic Stream operations — filter, map, sort, distinct, count. Foundations that every question builds on.
Engineering department.Java 8▶Solution
Straightforward filter on deptName.
javaList<Employee> engg = employees.stream()
.filter(e -> "Engineering".equals(e.getDeptName()))
.collect(Collectors.toList());
Gotcha
Note "Engineering".equals(e.getDeptName()) — not e.getDeptName().equals("Engineering"). If deptName is null, the second form throws NPE. Always put the literal on the left.
A.Java 8▶Solution
javaList<Employee> aEmps = employees.stream()
.filter(e -> e.getName().startsWith("A"))
.collect(Collectors.toList());
Case-insensitive variant
java.filter(e -> e.getName().toUpperCase().startsWith("A"))names from the full list.Java 8▶Solution — use map to transform
javaList<String> names = employees.stream()
.map(Employee::getName)
.collect(Collectors.toList());
In Java 16+, prefer .toList()
javaList<String> names = employees.stream()
.map(Employee::getName)
.toList(); // returns an unmodifiable listSolution
javalong count = employees.stream()
.filter(e -> e.getAge() > 35)
.count();
Note
count() returns long, not int — for datasets with potentially millions of records. Casting to int risks overflow.
Solution
javaList<String> depts = employees.stream()
.map(Employee::getDeptName)
.distinct()
.collect(Collectors.toList());
Alternative — use a Set
javaSet<String> depts = employees.stream()
.map(Employee::getDeptName)
.collect(Collectors.toSet());
distinct() preserves insertion order; toSet() returns a HashSet with no order guarantee.
Solution
javaList<Employee> sorted = employees.stream()
.sorted(Comparator.comparingLong(Employee::getSalary))
.collect(Collectors.toList());
Descending
java.sorted(Comparator.comparingLong(Employee::getSalary).reversed())
Prefer typed comparators
Use comparingLong for long salary, comparingInt for int age, comparingDouble for double. These avoid auto-boxing and are measurably faster for large lists.
Solution — chained comparator
javaList<Employee> sorted = employees.stream()
.sorted(Comparator.comparing(Employee::getDeptName)
.thenComparing(Comparator.comparingLong(Employee::getSalary).reversed()))
.collect(Collectors.toList());
Gotcha with .reversed()
thenComparing(...).reversed() reverses the entire combined comparator, not just the tail. Always apply .reversed() inside the thenComparing argument.
Solution — sorted then limit
javaList<Employee> top3 = employees.stream()
.sorted(Comparator.comparingLong(Employee::getSalary).reversed())
.limit(3)
.collect(Collectors.toList());
Note that limit is a short-circuit operation — the JIT can optimize, though sorted is a stateful intermediate op that fully materializes the stream.
Solution
javaList<Employee> rest = employees.stream()
.sorted(Comparator.comparingLong(Employee::getSalary).reversed())
.skip(2)
.collect(Collectors.toList());Solution — anyMatch (short-circuits on first hit)
javaboolean exists = employees.stream()
.anyMatch(e -> e.getSalary() > 200000);
Related short-circuit ops
allMatch— are ALL salaries above X?noneMatch— is there NO employee above X?findFirst— return the first matching elementfindAny— return any match (faster on parallel streams)
Solution
javaboolean allAbove = employees.stream()
.allMatch(e -> e.getSalary() >= 50000);
Edge case — empty stream
allMatch on an empty stream returns true (vacuously true), and anyMatch returns false. This matches the math convention but can surprise developers.
Solution
javaList<String> upperNames = employees.stream()
.map(e -> e.getName().toUpperCase())
.sorted()
.collect(Collectors.toList());List<List<Integer>>, flatten it into a single List<Integer>.Java 8▶Solution — flatMap
javaList<List<Integer>> nested = List.of(List.of(1,2), List.of(3,4), List.of(5));
List<Integer> flat = nested.stream()
.flatMap(List::stream)
.collect(Collectors.toList());
// [1, 2, 3, 4, 5]
map vs flatMap — the mental model
map: one-to-one transformation (T -> R).
flatMap: one-to-many that unwraps (T -> Stream<R>, then concatenates).
Solution
javaList<String> words = List.of("hello", "world", "java");
List<Character> chars = words.stream()
.flatMap(w -> w.chars().mapToObj(c -> (char) c))
.distinct()
.sorted()
.collect(Collectors.toList());
Note String.chars() returns IntStream, so we cast back to char via mapToObj.
Solution — nullsLast
javaList<Employee> sorted = employees.stream()
.sorted(Comparator.comparing(Employee::getName,
Comparator.nullsLast(Comparator.naturalOrder())))
.collect(Collectors.toList());
Why this matters
Default natural ordering throws NPE on null. nullsFirst/nullsLast wraps a comparator to handle nulls gracefully — critical when dealing with data from external sources.
Grouping & aggregation Java 8 · Q16-30
Collectors.groupingBy, counting, partitioningBy, and nested groupings — the most frequently tested area in interviews.
Solution
javaMap<String, List<Employee>> byDept = employees.stream()
.collect(Collectors.groupingBy(Employee::getDeptName));
The default downstream collector is toList(). The returned map is a regular HashMap — iteration order is not guaranteed.
Solution
javaMap<String, Long> countByDept = employees.stream()
.collect(Collectors.groupingBy(Employee::getDeptName, Collectors.counting()));
// { Engineering=5, Finance=2, HR=2, Sales=3 }Solution — mapping downstream
javaMap<String, List<String>> namesByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDeptName,
Collectors.mapping(Employee::getName, Collectors.toList())));
mapping applies a transformation before handing to the downstream collector — keeps the pipeline declarative without a second stream.
Solution
javaMap<String, Double> avgSalByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDeptName,
Collectors.averagingLong(Employee::getSalary)));
Variants
summingLong, averagingLong, averagingDouble all work similarly. For a rich summary with count/sum/min/max/avg in one pass, use summarizingLong.
Solution — maxBy downstream
javaMap<String, Optional<Employee>> topByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDeptName,
Collectors.maxBy(Comparator.comparingLong(Employee::getSalary))));
Unwrap Optional — collectingAndThen
javaMap<String, Employee> topByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDeptName,
Collectors.collectingAndThen(
Collectors.maxBy(Comparator.comparingLong(Employee::getSalary)),
Optional::get)));Solution — partitioningBy
javaMap<Boolean, List<Employee>> partitioned = employees.stream()
.collect(Collectors.partitioningBy(e -> e.getSalary() > 100000));
List<Employee> highEarners = partitioned.get(true);
List<Employee> lowEarners = partitioned.get(false);
partitioningBy vs groupingBy
partitioningBy always produces a map with exactly two keys (true and false), even if one is empty. groupingBy with a boolean key may skip keys entirely if no elements match — subtle but real difference.
partitioningBy.Java 8▶Solution
javaMap<Boolean, Long> genderCount = employees.stream()
.collect(Collectors.partitioningBy(
e -> "M".equals(e.getGender()),
Collectors.counting()));Solution
javaMap<String, Map<String, List<Employee>>> nested = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDeptName,
Collectors.groupingBy(Employee::getGender)));
The composable nature of Collectors is the power of the Streams API — any collector can be a downstream of any grouping.
Solution
javaMap<String, Map<String, Long>> deptGenderCount = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDeptName,
Collectors.groupingBy(
Employee::getGender,
Collectors.counting())));
// { Engineering={M=3, F=2}, HR={M=1, F=1}, ... }Solution
javaString largestDept = employees.stream()
.collect(Collectors.groupingBy(Employee::getDeptName, Collectors.counting()))
.entrySet().stream()
.max(Map.Entry.comparingByValue())
.map(Map.Entry::getKey)
.orElse(null);
Note the two-pass structure
Stream the collection → group → produce intermediate map → stream that map → reduce to max. This pattern (group-then-reduce) is extremely common for "top by X" queries.
Solution — summarizingLong
javaMap<String, LongSummaryStatistics> stats = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDeptName,
Collectors.summarizingLong(Employee::getSalary)));
stats.forEach((dept, s) -> System.out.printf(
"%s: count=%d, avg=%.2f, min=%d, max=%d, sum=%d%n",
dept, s.getCount(), s.getAverage(), s.getMin(), s.getMax(), s.getSum()));
Why this matters: one stream pass computes five statistics. Doing it with five separate stream operations would be 5x the work.
Solution
javaMap<String, Set<String>> deptsByCity = employees.stream()
.collect(Collectors.groupingBy(
Employee::getCity,
Collectors.mapping(Employee::getDeptName, Collectors.toSet())));Solution
javaLinkedHashMap<String, Long> sortedTotals = employees.stream()
.collect(Collectors.groupingBy(Employee::getDeptName,
Collectors.summingLong(Employee::getSalary)))
.entrySet().stream()
.sorted(Map.Entry.<String, Long>comparingByValue().reversed())
.collect(Collectors.toMap(
Map.Entry::getKey, Map.Entry::getValue,
(a, b) -> a,
LinkedHashMap::new));
Why the merge function matters
toMap needs a merge function to handle duplicate keys — since there are none here, (a,b) -> a is safe. The LinkedHashMap supplier preserves the sorted order.
Solution
javaList<String> busyDepts = employees.stream()
.collect(Collectors.groupingBy(Employee::getDeptName, Collectors.counting()))
.entrySet().stream()
.filter(e -> e.getValue() > 2)
.map(Map.Entry::getKey)
.collect(Collectors.toList());Solution
javaMap<String, Double> avgAgeByGender = employees.stream()
.collect(Collectors.groupingBy(
Employee::getGender,
Collectors.averagingInt(Employee::getAge)));
// { M=36.17, F=33.17 }Salary & ranking problems Java 8 · Q31-40
The classic "second highest salary", Nth largest, and ranking problems — favorites in technical screens.
Solution — max with a comparator
javaOptional<Employee> highest = employees.stream()
.max(Comparator.comparingLong(Employee::getSalary));
highest.ifPresent(e -> System.out.println(e.getName() + ": " + e.getSalary()));
Why Optional?
max returns Optional because the stream could be empty. Never call .get() without a presence check — use ifPresent, orElse, or orElseThrow.
Solution — sort, skip, findFirst
javaOptional<Long> secondHighest = employees.stream()
.map(Employee::getSalary)
.distinct()
.sorted(Comparator.reverseOrder())
.skip(1)
.findFirst();
Critical — use distinct()
Without distinct, if two employees earn the same highest salary, the "second highest" would return the same value. Distinct on the salary stream guarantees distinct salary values, not distinct employees.
Solution — reusable method
javapublic static Optional<Long> nthHighestSalary(List<Employee> emps, int n) {
if (n < 1) throw new IllegalArgumentException("n must be >= 1");
return emps.stream()
.map(Employee::getSalary)
.distinct()
.sorted(Comparator.reverseOrder())
.skip(n - 1)
.findFirst();
}
Better for large N — use PriorityQueue
Sorting is O(N log N). For very large datasets or frequent queries, a min-heap of size N is O(N log k) — but for interview problems, the stream approach is idiomatic and fine.
Solution
javaMap<String, Optional<Employee>> secondByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDeptName,
Collectors.collectingAndThen(
Collectors.toList(),
list -> list.stream()
.sorted(Comparator.comparingLong(Employee::getSalary).reversed())
.skip(1)
.findFirst())));
This shows the power of collectingAndThen — group by dept, then run arbitrary logic on each group.
Solution A — mapToLong + sum
javalong total = employees.stream()
.mapToLong(Employee::getSalary)
.sum();
Solution B — reduce
javalong total = employees.stream()
.map(Employee::getSalary)
.reduce(0L, Long::sum);
Solution C — summingLong
javalong total = employees.stream()
.collect(Collectors.summingLong(Employee::getSalary));
Prefer A — mapToLong avoids boxing to Long on every element.
Solution
javaOptionalDouble avg = employees.stream()
.mapToLong(Employee::getSalary)
.average();
avg.ifPresent(a -> System.out.printf("Average: %.2f%n", a));
Returns OptionalDouble because the average of an empty stream is undefined.
Solution
javaOptional<Employee> topFemale = employees.stream()
.filter(e -> "F".equals(e.getGender()))
.max(Comparator.comparingLong(Employee::getSalary));Solution — index via a counter
javaList<Map.Entry<Integer, Employee>> ranked = new ArrayList<>();
int[] rank = {0};
employees.stream()
.sorted(Comparator.comparingLong(Employee::getSalary).reversed())
.forEach(e -> ranked.add(Map.entry(++rank[0], e)));
Purer functional approach using IntStream
javaList<Employee> sorted = employees.stream()
.sorted(Comparator.comparingLong(Employee::getSalary).reversed())
.toList();
Map<Integer, Employee> rankMap = IntStream.range(0, sorted.size())
.boxed()
.collect(Collectors.toMap(i -> i + 1, sorted::get));
Stream pipelines don't expose an index by design — when you need one, IntStream.range paired with a materialized list is the idiomatic workaround.
Solution
javadouble avg = employees.stream()
.mapToLong(Employee::getSalary)
.average()
.orElse(0);
List<Employee> aboveAvg = employees.stream()
.filter(e -> e.getSalary() > avg)
.collect(Collectors.toList());
Why two passes are needed
Stream operations cannot reference aggregate results of the same stream (no "self-reference"). Computing the average and then filtering requires two passes — that's just how streams work.
Solution — pre-compute department averages
javaMap<String, Double> avgByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDeptName,
Collectors.averagingLong(Employee::getSalary)));
List<Employee> aboveDeptAvg = employees.stream()
.filter(e -> e.getSalary() > avgByDept.get(e.getDeptName()))
.collect(Collectors.toList());
This is a classic "group-relative filter" pattern
Same pattern applies to "above dept median", "top 10% per dept", etc. Always compute the group aggregate first, then filter with a lookup.
Collectors & reduce Java 8 · Q41-50
Advanced collectors, custom reduction, and joining strings.
Solution — joining
javaString names = employees.stream()
.map(Employee::getName)
.collect(Collectors.joining(", "));
// "Arjun, Priya, Rohit, ..."
With prefix and suffix
javaString names = employees.stream()
.map(Employee::getName)
.collect(Collectors.joining(", ", "[", "]"));
// "[Arjun, Priya, Rohit, ...]"Map<Integer, String> of employee ID to name.Java 8▶Solution — toMap
javaMap<Integer, String> idToName = employees.stream()
.collect(Collectors.toMap(Employee::getId, Employee::getName));
Handling duplicate keys — merge function
javaMap<String, Long> sumByDept = employees.stream()
.collect(Collectors.toMap(
Employee::getDeptName,
Employee::getSalary,
Long::sum)); // merges when dept name repeats
Without a merge function, duplicate keys throw IllegalStateException.
Solution
javaOptional<String> longest = employees.stream()
.map(Employee::getName)
.max(Comparator.comparingInt(String::length));Solution
Streams don't have a built-in reverse. Options:
java// Option 1: collect, reverse, stream again
List<Integer> nums = List.of(1, 2, 3, 4);
List<Integer> reversed = nums.stream()
.collect(Collectors.collectingAndThen(
Collectors.toList(),
l -> { Collections.reverse(l); return l; }));
// Option 2: sorted with reverse comparator (if natural ordering is meaningful)
nums.stream().sorted(Comparator.reverseOrder()).toList();
// Option 3: use an IntStream with decrementing index
IntStream.iterate(nums.size() - 1, i -> i >= 0, i -> i - 1)
.mapToObj(nums::get)
.toList();Solution
javaint sumOfSquares = List.of(1, 2, 3, 4, 5).stream()
.mapToInt(Integer::intValue)
.map(n -> n * n)
.sum();
// 55Solution
javaString input = "programming";
Map<Character, Long> freq = input.chars()
.mapToObj(c -> (char) c)
.collect(Collectors.groupingBy(
Function.identity(),
Collectors.counting()));
// {p=1, r=2, o=1, g=2, a=1, m=2, i=1, n=1}
Preserve insertion order
java.collect(Collectors.groupingBy(
Function.identity(),
LinkedHashMap::new,
Collectors.counting()));Solution — count, then find first with count 1
javaString s = "swiss";
Character firstUnique = s.chars()
.mapToObj(c -> (char) c)
.collect(Collectors.groupingBy(
Function.identity(),
LinkedHashMap::new, // preserve order
Collectors.counting()))
.entrySet().stream()
.filter(e -> e.getValue() == 1)
.map(Map.Entry::getKey)
.findFirst()
.orElse(null);
// 'w'
The LinkedHashMap supplier is critical — otherwise iteration order is undefined and "first" becomes meaningless.
Solution — tracked via HashSet
javaList<Integer> nums = List.of(1, 2, 3, 2, 4, 5, 3, 1);
Set<Integer> seen = new HashSet<>();
Set<Integer> duplicates = nums.stream()
.filter(n -> !seen.add(n))
.collect(Collectors.toSet());
// [1, 2, 3]
Caveat — mutating state in stream operations
This works but is not parallel-safe. For sequential streams it's fine; if you need parallelism, use the grouping approach instead:
javaList<Integer> duplicates = nums.stream()
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
.entrySet().stream()
.filter(e -> e.getValue() > 1)
.map(Map.Entry::getKey)
.collect(Collectors.toList());Solution
javaList<String> words = List.of("apple", "banana", "apple", "cherry", "apple", "banana");
String mostFrequent = words.stream()
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
.entrySet().stream()
.max(Map.Entry.comparingByValue())
.map(Map.Entry::getKey)
.orElse(null);
// "apple"reduce to compute the factorial of N.Java 8▶Solution — IntStream.rangeClosed + reduce
javaint n = 10;
long factorial = IntStream.rangeClosed(1, n)
.asLongStream()
.reduce(1, (a, b) -> a * b);
// 3628800
For N > 20 use BigInteger
javaBigInteger factorial = IntStream.rangeClosed(1, 100)
.mapToObj(BigInteger::valueOf)
.reduce(BigInteger.ONE, BigInteger::multiply);
20! = 2,432,902,008,176,640,000 fits in long; 21! overflows. Always consider overflow when reducing products or sums.
Optional & functional interfaces Java 8 · Q51-60
Optional correctly, functional interfaces, method references, default methods.
The four foundational interfaces in java.util.function
| Interface | Signature | Use |
|---|---|---|
Function<T,R> | R apply(T) | Transform input to output (used by map) |
Predicate<T> | boolean test(T) | Test a condition (used by filter, anyMatch) |
Consumer<T> | void accept(T) | Side-effect only (used by forEach) |
Supplier<T> | T get() | Provide a value on demand (used by orElseGet, Stream.generate) |
Plus the specializations
BiFunction<T,U,R>, UnaryOperator<T> (= Function<T,T>), BinaryOperator<T> (= BiFunction<T,T,T>), and primitive variants IntPredicate, ToLongFunction, IntSupplier etc. to avoid boxing.
Optional.get() without a check?Java 8▶The problem
Optional.get() throws NoSuchElementException if the Optional is empty — the exact NPE-style bug Optional was meant to prevent.
Proper alternatives
java// BAD — can throw
String name = optEmployee.get().getName();
// GOOD — explicit default
String name = optEmployee.map(Employee::getName).orElse("Unknown");
// GOOD — throw with a meaningful message
String name = optEmployee.map(Employee::getName)
.orElseThrow(() -> new EmployeeNotFoundException("id 101"));
// GOOD — side effect only if present
optEmployee.ifPresent(e -> System.out.println(e.getName()));
// JAVA 9+ — branch present vs absent
optEmployee.ifPresentOrElse(
e -> System.out.println("Found: " + e.getName()),
() -> System.out.println("Not found"));Optional as a field type or a method parameter? Why or why not?Java 8▶Short answer — No to both.
Optional was designed specifically for return types where absence is semantically meaningful. It should not be used as a field, parameter, or collection element.
Why not as a field
Optionalis notSerializablein the traditional sense.- Adds boxing overhead on every access.
- Breaks frameworks that use reflection on fields (Jackson, JPA, etc.).
- Just use a nullable field with clear documentation.
Why not as a parameter
- Callers have to wrap plain values:
doStuff(Optional.of(x))is ugly. - Overload methods instead: one with the param, one without.
Sanctioned use
java// GOOD — return type communicates "may not find one"
Optional<Employee> findById(int id);
// BAD — field
private Optional<String> nickname;
// BAD — parameter
void setAddress(Optional<Address> addr);Optional<Employee>, chain calls to get the employee's department's city safely.Java 8▶Solution — map / flatMap chaining
javaOptional<Employee> opt = findById(101);
// If getDepartment() itself returns Optional, use flatMap
String city = opt
.flatMap(Employee::getDepartment) // Optional<Department>
.map(Department::getCity) // Optional<String>
.orElse("Unknown");
map vs flatMap on Optional
map: applies function, wraps result in Optional.
flatMap: applies function that already returns Optional — avoids Optional<Optional<T>>.
Same mental model as streams: flatMap unwraps; map wraps.
Predicate that combines "age > 30" AND "salary > 100000" using predicate composition.Java 8▶Solution
javaPredicate<Employee> old = e -> e.getAge() > 30;
Predicate<Employee> highPaid = e -> e.getSalary() > 100000;
Predicate<Employee> combined = old.and(highPaid);
List<Employee> result = employees.stream()
.filter(combined)
.collect(Collectors.toList());
Composition methods on Predicate
.and(other)— logical AND.or(other)— logical OR.negate()— NOTPredicate.not(existing)(Java 11+) — static form, cleaner when using method references
Function instances using andThen and compose.Java 8▶Solution
javaFunction<Integer, Integer> add5 = x -> x + 5;
Function<Integer, Integer> times2 = x -> x * 2;
// andThen: f.andThen(g) = g(f(x))
Function<Integer, Integer> addThenDouble = add5.andThen(times2);
addThenDouble.apply(3); // (3 + 5) * 2 = 16
// compose: f.compose(g) = f(g(x))
Function<Integer, Integer> doubleThenAdd = add5.compose(times2);
doubleThenAdd.apply(3); // (3 * 2) + 5 = 11
Mnemonic: andThen reads left-to-right; compose reads right-to-left like math.
Four forms of method reference
java// 1. Static method reference
Function<String, Integer> f1 = Integer::parseInt;
// equivalent to: s -> Integer.parseInt(s)
// 2. Instance method of a particular object
Consumer<String> printer = System.out::println;
// equivalent to: s -> System.out.println(s)
// 3. Instance method of an arbitrary object of a class
Function<String, String> upper = String::toUpperCase;
// equivalent to: s -> s.toUpperCase()
// 4. Constructor reference
Supplier<ArrayList<String>> listFactory = ArrayList::new;
// equivalent to: () -> new ArrayList<>()
Function<String, Employee> byName = Employee::new;
// equivalent to: name -> new Employee(name)Default methods
Introduced in Java 8 to add new methods to interfaces without breaking existing implementations.
javapublic interface Vehicle {
void start();
default void stop() {
System.out.println("Vehicle stopping");
}
}
public class Car implements Vehicle {
@Override
public void start() { System.out.println("Car starting"); }
// stop() inherited from Vehicle
}
The diamond problem
If a class implements two interfaces with the same default method, the compiler forces you to override and explicitly choose which one to call:
java@Override
public void greet() {
InterfaceA.super.greet(); // explicit choice
}Static interface methods
Also new in Java 8. Called on the interface type, not on instances. Used for utility/factory methods that belong with the interface.
javapublic interface Validator<T> {
boolean validate(T t);
static <T> Validator<T> alwaysTrue() {
return t -> true;
}
static <T> Validator<T> notNull() {
return Objects::nonNull;
}
}
// Usage
Validator<String> v = Validator.notNull();
Before Java 8, the pattern required a companion utility class (Validators). Static interface methods collapse this into one place.
Solution — Stream.iterate with limit
javaList<Integer> first10Even = Stream.iterate(0, n -> n + 2)
.limit(10)
.collect(Collectors.toList());
// [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
Java 9+ — three-arg iterate with a predicate
javaList<Integer> first10Even = Stream.iterate(0, n -> n < 20, n -> n + 2)
.collect(Collectors.toList());
Alternative — Stream.generate
Stream.generate(Supplier) produces an unbounded unordered stream — good for random values but can't reference previous values, so iterate is better for sequences.
Java 11 — String methods, var, HTTP client Java 11 · Q61-70
Local variable type inference, new String utility methods, the standardized HTTP Client. LTS since 2018.
isBlank() method do and how is it different from isEmpty()?Java 11▶Comparison
java"".isEmpty(); // true
" ".isEmpty(); // false
" ".isBlank(); // true — also checks for whitespace-only
" \t\n ".isBlank(); // true — handles all whitespace
isBlank() uses Character.isWhitespace() internally, covering spaces, tabs, newlines, and other Unicode whitespace characters.
Use case — form validation
javaif (input == null || input.isBlank()) {
throw new ValidationException("Name required");
}strip() instead of trim() — what's the difference?Java 11▶The key difference — Unicode awareness
javaString s = "\u2003hello\u2003"; // em-space padding
s.trim(); // returns "\u2003hello\u2003" — trim only removes chars <= U+0020
s.strip(); // returns "hello" — strip uses Character.isWhitespace()
Variants
strip()— both sidesstripLeading()— only leftstripTrailing()— only right
Always prefer strip() over trim() for user-facing text. trim() is a legacy that pre-dates proper Unicode support.
Solution — String.repeat(int)
javaString line = "-".repeat(30);
// "------------------------------"
String indent = " ".repeat(level);
// for pretty-printing
Internally uses Arrays.copyOf which is highly optimized — far faster than a StringBuilder loop.
Solution — lines()
javaString text = "line 1\nline 2\nline 3";
List<String> lines = text.lines()
.filter(l -> !l.isBlank())
.map(String::strip)
.collect(Collectors.toList());
lines() returns a Stream<String> and is platform-independent (handles \n, \r, \r\n all correctly). Prefer this over split("\n").
var for local variable type inference.Java 11▶Good uses
java// RHS type is obvious from the call
var list = new ArrayList<String>();
// Avoids painful generic redundancy
var map = new HashMap<String, List<Employee>>();
// Type inference in enhanced for
for (var e : employees) {
System.out.println(e.getName());
}
Bad uses
java// BAD — type unclear from call
var result = process(data); // What is result? Reader wastes time figuring it out.
// BAD — can't use with null
var x = null; // Compile error — no type to infer
// BAD — can't use on fields, method params, or return types
private var field; // Compile error
Rule of thumb: var is useful when the RHS is verbose and the type is obvious. If it hurts readability, spell out the type.
var in a lambda parameter (Java 11 specific feature).Java 11▶Solution
java// Works in Java 11+, not Java 10
employees.stream()
.filter((@NotNull var e) -> e.getSalary() > 100000)
.forEach(System.out::println);
Why use var in lambdas?
Without var, you cannot add annotations or modifiers to inferred-type lambda parameters. With var, you can annotate them.
java// NOT ALLOWED — can't annotate an implicit-typed param
(@NotNull e) -> e.getSalary()
// ALLOWED with var
(@NotNull var e) -> e.getSalary()HttpClient.Java 11▶Synchronous call
javaHttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.github.com/users/octocat"))
.header("Accept", "application/json")
.GET()
.build();
HttpResponse<String> response = client.send(
request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode());
System.out.println(response.body());
Asynchronous
javaCompletableFuture<String> future = client.sendAsync(request,
HttpResponse.BodyHandlers.ofString())
.thenApply(HttpResponse::body);
future.thenAccept(System.out::println).join();
This replaces the legacy HttpURLConnection and supports HTTP/2 natively. For production, consider connection pooling and timeout configuration.
toArray(IntFunction).Java 11▶Java 11 shortcut
javaList<String> names = List.of("Arjun", "Priya", "Rohit");
// Pre-Java 11
String[] arr1 = names.toArray(new String[0]);
// Java 11+ — cleaner
String[] arr2 = names.toArray(String[]::new);
The method reference form is slightly more idiomatic; both produce an array of the correct size.
Single-file source launcher
bash# HelloWorld.java — single file, no project needed
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, " + args[0]);
}
}
# Just run — no javac step
$ java HelloWorld.java Uddula
Hello, Uddula
Why this matters
Great for scripts, CI tools, and teaching. You can put #!/usr/bin/java --source 11 as a shebang line and use Java as a scripting language.
Predicate.not(...) to filter out elements matching a predicate.Java 11▶Solution — cleaner than .negate() or lambda negation
javaList<String> nonBlank = lines.stream()
.filter(Predicate.not(String::isBlank))
.collect(Collectors.toList());
Compare to alternatives
java// With lambda — fine but verbose
.filter(s -> !s.isBlank())
// With .negate() — only works after assigning to a variable
Predicate<String> blank = String::isBlank;
.filter(blank.negate())
// Cleanest — Java 11 Predicate.not
.filter(Predicate.not(String::isBlank))Java 17 — Records Java 17 · Q71-78
Immutable data carriers with auto-generated constructor, accessors, equals, hashCode, and toString.
Point record. What does the compiler generate?Java 17▶Declaration
javapublic record Point(int x, int y) {}
What the compiler generates (conceptually)
javapublic final class Point extends Record {
private final int x;
private final int y;
public Point(int x, int y) { this.x = x; this.y = y; }
public int x() { return x; }
public int y() { return y; }
public boolean equals(Object o) { /* compares x,y */ }
public int hashCode() { /* based on x,y */ }
public String toString() { return "Point[x=..., y=...]"; }
}
Key properties
- Implicitly
final— cannot be extended - Extends
java.lang.Record - All components are
private final - Accessors are not
getXbut justx()— the component name
Solution — compact constructor
javapublic record Employee(int id, String name, long salary) {
// Compact constructor — no parameter list, no explicit assignments
public Employee {
if (id < 0) throw new IllegalArgumentException("id must be >= 0");
Objects.requireNonNull(name, "name");
if (salary < 0) throw new IllegalArgumentException("salary must be >= 0");
name = name.strip(); // normalization allowed
}
}
Compact vs canonical constructor
- Compact — no param list, no
this.x = x. Assignments happen implicitly at the end. - Canonical — full param list. You do assignments yourself. More verbose but more flexible.
Yes to all — with nuance
javapublic record Employee(int id, String name, long salary) {
// Static fields — allowed
public static final long MIN_WAGE = 15000;
// Static factory methods — encouraged
public static Employee intern(int id, String name) {
return new Employee(id, name, MIN_WAGE);
}
// Instance methods — allowed
public boolean isHighPaid() {
return salary > 200000;
}
// Instance FIELDS — NOT ALLOWED
// private long tempField; // compile error
}
What records cannot do
- Cannot declare instance fields (only components).
- Cannot extend another class (they implicitly extend
Record). - Cannot be abstract.
Records can implement interfaces
javapublic interface Identifiable {
int id();
default String displayId() { return "#" + id(); }
}
public record Employee(int id, String name) implements Identifiable {}
// Usage
Employee e = new Employee(101, "Arjun");
e.displayId(); // "#101"
The id() accessor auto-generated by the record satisfies the Identifiable.id() contract directly.
Before records — verbose DTO
javapublic class CreateUserRequest {
private String name;
private String email;
private int age;
// constructor, getters, setters, equals, hashCode, toString
// ~40 lines
}
With records — one line
javapublic record CreateUserRequest(
@NotBlank String name,
@Email String email,
@Min(18) int age
) {}
@PostMapping("/users")
public ResponseEntity<User> create(@Valid @RequestBody CreateUserRequest req) {
return ResponseEntity.ok(userService.create(req.name(), req.email(), req.age()));
}
Best practice for REST APIs: Records for request/response DTOs. Entities (JPA) should stay as regular classes because JPA requires mutability and no-arg constructors.
Records make great map keys
javapublic record DeptCity(String dept, String city) {}
Map<DeptCity, List<Employee>> groupedByDeptCity = employees.stream()
.collect(Collectors.groupingBy(
e -> new DeptCity(e.getDeptName(), e.getCity())));
Why this works perfectly
Records auto-generate equals and hashCode based on all components. No manual implementation needed — the compound key "just works" in a HashMap.
Before records, you'd need a dedicated DeptCityKey class with manually-written boilerplate. Records collapse this pattern.
toString() for custom formatting.Java 17▶Solution — override normally
javapublic record Money(long amount, String currency) {
@Override
public String toString() {
return String.format("%s %.2f", currency, amount / 100.0);
}
}
Money m = new Money(12550, "USD");
System.out.println(m); // "USD 125.50"
You can also override equals/hashCode — but only do so if the default component-based behavior is genuinely wrong. Custom equals on a record is almost always a code smell.
Java 21 — record patterns with pattern matching
javapublic record Point(int x, int y) {}
List<Point> points = List.of(new Point(1, 2), new Point(3, 4));
// Java 21 — record pattern destructuring
points.forEach(p -> {
if (p instanceof Point(int x, int y)) {
System.out.println(x + ", " + y);
}
});
In a switch (Java 21)
javaString describe(Object o) {
return switch (o) {
case Point(int x, int y) when x == y -> "on diagonal";
case Point(int x, int y) -> "at (" + x + ", " + y + ")";
default -> "unknown";
};
}Java 17 — Sealed classes Java 17 · Q79-84
Restrict inheritance to a known set of subclasses. Enables exhaustive pattern matching.
Shape with permitted subclasses Circle and Rectangle.Java 17▶Solution
javapublic sealed interface Shape
permits Circle, Rectangle {}
public record Circle(double radius) implements Shape {}
public record Rectangle(double width, double height) implements Shape {}
Permitted subclass constraints
Each permitted subclass must be one of:
final— cannot itself be extendedsealed— itself restricts further subclassingnon-sealed— explicitly opens up hierarchy (allows unrestricted subclasses)
Records are implicitly final, which is why they're commonly paired with sealed interfaces.
1. Exhaustive switch without default
javadouble area(Shape s) {
return switch (s) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.width() * r.height();
// no default needed — compiler knows all subtypes!
};
}
2. API modelling — closed domain
Represents "there are exactly these N kinds of X, no more." Example: payment types, event types, error classes. Callers can handle every case confidently.
3. Safer refactoring
Add a new permitted subtype → every exhaustive switch across the codebase becomes a compile error until you handle it. This is the opposite of runtime surprises.
permits clause of a sealed class?Java 17▶Yes — and it's a common idiom
javapublic sealed interface Result<T>
permits Result.Success, Result.Failure {
record Success<T>(T value) implements Result<T> {}
record Failure<T>(String error) implements Result<T> {}
}
Records are implicitly final, which satisfies the sealed class requirement that subtypes be closed. This combination is so idiomatic it's considered the canonical way to model sum types (algebraic data types) in Java.
sealed, non-sealed, and final?Java 17▶The three-tier hierarchy
javapublic sealed class Vehicle permits Car, Truck, Motorcycle {}
// Closed — can't be extended further
public final class Car extends Vehicle {}
// Sealed — further restricted subclasses
public sealed class Truck extends Vehicle permits PickupTruck, SemiTruck {}
public final class PickupTruck extends Truck {}
public final class SemiTruck extends Truck {}
// Non-sealed — explicitly opens the hierarchy
public non-sealed class Motorcycle extends Vehicle {}
// Anyone can now subclass Motorcycle freely
Use cases
final— "I'm a concrete leaf, don't extend me"sealed— "I have a known set of children"non-sealed— "I appear in a sealed parent, but I'm open"
Solution
javapublic sealed interface Payment
permits CreditCard, UPI, Cash, Wallet {}
public record CreditCard(String number, String cvv, YearMonth expiry)
implements Payment {}
public record UPI(String vpa) implements Payment {}
public record Cash(long amount) implements Payment {}
public record Wallet(String provider, String walletId) implements Payment {}
public class PaymentProcessor {
public Result process(Payment p) {
return switch (p) {
case CreditCard(var num, var cvv, var exp) -> chargeCreditCard(num, cvv, exp);
case UPI(var vpa) -> chargeUPI(vpa);
case Cash(var amount) -> recordCash(amount);
case Wallet(var provider, var id) -> chargeWallet(provider, id);
};
}
}
Adding a new payment method (say, CryptoPayment) — every switch in the codebase that handles Payment becomes a compile error until updated. Exactly the refactoring safety you want.
The rules
- Same module if the sealed class is in a named module.
- Same package if the sealed class is in the unnamed module (classpath).
Why the constraint
Sealed classes work via the PermittedSubclasses attribute in the class file. The JVM verifies this attribute at class loading — but across module boundaries, class loading order becomes complex. Keeping permitted subclasses in the same module/package guarantees they're loaded together.
What this means practically
- You can't have a public sealed interface and let arbitrary clients add subclasses — that would defeat the entire point of "sealed".
- For a library defining a sealed hierarchy, expose a non-sealed base class if extension is an intended feature.
Java 17 — Pattern matching Java 17 · Q85-90
Pattern matching for instanceof, switch expressions, and the type-safety improvements that come with them.
instanceof+cast using pattern matching.Java 17▶Traditional — verbose
javaif (obj instanceof String) {
String s = (String) obj;
System.out.println(s.length());
}
Pattern matching — Java 17
javaif (obj instanceof String s) {
System.out.println(s.length());
}
Scope rules — the pattern variable
sis in scope inside theifblock- Also in scope in
&&chains:if (o instanceof String s && s.length() > 5) ... - Also in the
elseof negated:if (!(o instanceof String s)) return; s.length(); // works!
Solution — Java 21 pattern matching for switch
javapublic String describe(Object obj) {
return switch (obj) {
case Integer i when i < 0 -> "negative int: " + i;
case Integer i -> "positive int: " + i;
case String s when s.isEmpty() -> "empty string";
case String s -> "string of length " + s.length();
case int[] arr -> "int array of size " + arr.length;
case null -> "null";
default -> "unknown: " + obj.getClass();
};
}
Note — when guards
when clauses add conditional matching — matches both the type AND the condition. Before when, you had to nest if inside the case body, which was ugly.
Note — case null
You can now match null explicitly in a switch. Before Java 21, switching on null always threw NPE.
Record pattern example
javapublic sealed interface Shape
permits Circle, Rectangle, Triangle {}
public record Circle(double radius) implements Shape {}
public record Rectangle(double w, double h) implements Shape {}
public record Triangle(double base, double height) implements Shape {}
public static double area(Shape s) {
return switch (s) {
case Circle(double r) -> Math.PI * r * r;
case Rectangle(double w, double h) -> w * h;
case Triangle(double b, double h) -> 0.5 * b * h;
};
}
Nested deconstruction
javacase Rectangle(Point(int x1, int y1), Point(int x2, int y2)) ->
Math.abs(x2 - x1) * Math.abs(y2 - y1);
Record patterns make Java competitive with Scala/Kotlin for algebraic data type handling. Combined with sealed interfaces, you get compile-time exhaustiveness checking.
when guards for range checks.Java 17▶Solution
javaString classifyTemperature(Object o) {
return switch (o) {
case Integer t when t < 0 -> "freezing";
case Integer t when t < 10 -> "cold";
case Integer t when t < 25 -> "mild";
case Integer t when t <= 40 -> "warm";
case Integer t -> "hot";
case null -> "no reading";
default -> "invalid type";
};
}
Case order matters — matches proceed top-down. The compiler warns on unreachable cases (e.g. if an earlier case subsumes a later one).
JsonNode-style polymorphic JSON using pattern matching.Java 17▶Real-world use case
javapublic sealed interface JsonValue
permits JsonString, JsonNumber, JsonBool, JsonNull, JsonArray, JsonObject {}
public record JsonString(String value) implements JsonValue {}
public record JsonNumber(double value) implements JsonValue {}
public record JsonBool(boolean value) implements JsonValue {}
public record JsonNull() implements JsonValue {}
public record JsonArray(List<JsonValue> items) implements JsonValue {}
public record JsonObject(Map<String, JsonValue> fields) implements JsonValue {}
public static String render(JsonValue v) {
return switch (v) {
case JsonString(var s) -> "\"" + s + "\"";
case JsonNumber(var n) -> String.valueOf(n);
case JsonBool(var b) -> String.valueOf(b);
case JsonNull() -> "null";
case JsonArray(var items) -> items.stream().map(Main::render)
.collect(Collectors.joining(",", "[", "]"));
case JsonObject(var f) -> f.entrySet().stream()
.map(e -> "\"" + e.getKey() + "\":" + render(e.getValue()))
.collect(Collectors.joining(",", "{", "}"));
};
}
This pattern — sealed interface + records + exhaustive switch with deconstruction — is the most powerful new capability in modern Java for type-safe polymorphic handling.
Before — visitor pattern
javainterface ShapeVisitor<R> {
R visitCircle(Circle c);
R visitRectangle(Rectangle r);
}
interface Shape {
<R> R accept(ShapeVisitor<R> v);
}
class Circle implements Shape {
double radius;
@Override
public <R> R accept(ShapeVisitor<R> v) { return v.visitCircle(this); }
}
// ... same for Rectangle, 50+ lines of boilerplate
ShapeVisitor<Double> areaVisitor = new ShapeVisitor<>() {
public Double visitCircle(Circle c) { return Math.PI * c.radius * c.radius; }
public Double visitRectangle(Rectangle r) { return r.width * r.height; }
};
double a = shape.accept(areaVisitor);
After — sealed + switch
javasealed interface Shape permits Circle, Rectangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double w, double h) implements Shape {}
double area(Shape s) {
return switch (s) {
case Circle(var r) -> Math.PI * r * r;
case Rectangle(var w, var h) -> w * h;
};
}
Same compile-time safety, no boilerplate, new operations added without modifying the data classes. This is why functional-style ADT handling is displacing the visitor pattern in modern Java codebases.
Java 21 — Virtual threads Java 21 · Q91-95
Lightweight threads managed by the JVM. Run millions concurrently where platform threads would run thousands.
Definitions
- Platform thread — a 1:1 wrapper over an OS thread. Stack size ~1MB. Max ~5000 per JVM typically.
- Virtual thread — managed entirely by the JVM, mounted onto carrier platform threads only when running. ~200 bytes per idle thread. Can run millions concurrently.
Creation
java// Platform thread (old way)
Thread platform = new Thread(() -> doWork());
platform.start();
// Virtual thread (Java 21)
Thread virtual = Thread.ofVirtual().start(() -> doWork());
// Executor with virtual threads
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 10_000).forEach(i ->
executor.submit(() -> processRequest(i)));
}
The killer use case — I/O-bound servers
A web server handling 50k concurrent connections with platform threads needs a complex async framework (Netty, Vert.x). With virtual threads, "thread per request" becomes viable again — one virtual thread per connection, blocking I/O that yields the carrier thread during waits.
Solution
javapublic class VirtualThreadDemo {
public static void main(String[] args) throws Exception {
Instant start = Instant.now();
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
List<Future<String>> futures = IntStream.range(0, 10_000)
.mapToObj(i -> executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1));
return "Task " + i;
}))
.toList();
for (var f : futures) f.get();
}
System.out.println("Elapsed: " + Duration.between(start, Instant.now()));
}
}
// Elapsed: ~1-2 seconds (not 10,000 seconds!)
Why this works
Thread.sleep inside a virtual thread does not block the carrier platform thread. The JVM unmounts the virtual thread from its carrier during the sleep, freeing the carrier to run other virtual threads. When the sleep completes, the virtual thread is scheduled onto any available carrier.
synchronized?Java 21▶The pinning problem
When a virtual thread enters a synchronized block, it gets pinned to its carrier platform thread. The carrier cannot service other virtual threads until the synchronized block exits.
Why this matters
If 10,000 virtual threads all try to enter the same synchronized block, you effectively lose virtual threads' scalability advantage. Throughput drops to platform-thread levels.
Better — use ReentrantLock
java// BAD — pins the carrier thread during lock wait
synchronized (this) {
// blocking I/O here pins the carrier
doBlockingIO();
}
// GOOD — ReentrantLock releases the carrier during wait
private final ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
doBlockingIO(); // yields carrier properly
} finally {
lock.unlock();
}
Detect pinning
Run with -Djdk.tracePinnedThreads=full to log stack traces when pinning happens. Audit your code for hot synchronized blocks before adopting virtual threads at scale.
Note: Future Java versions aim to fix this — synchronized and java.util.concurrent.locks will eventually behave the same way.
StructuredTaskScope (preview in Java 21).Java 21▶The problem it solves
Parent task spawns multiple child tasks. Traditional executors leak tasks — parent can return before children complete, or children leak if parent fails. Structured concurrency enforces parent-child lifetime coupling.
Solution
javapublic Response fetchDashboardData(UserId id) throws Exception {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Subtask<User> userFuture = scope.fork(() -> loadUser(id));
Subtask<Orders> ordersFuture = scope.fork(() -> loadOrders(id));
Subtask<Preferences> prefsFuture = scope.fork(() -> loadPreferences(id));
scope.join(); // wait for all
scope.throwIfFailed(); // propagate any failure
return new Response(userFuture.get(), ordersFuture.get(), prefsFuture.get());
}
}
Semantics
- If any subtask fails,
ShutdownOnFailurecancels the others. - Parent cannot return until all subtasks complete (success or cancellation).
- Virtual threads are cheap — you can spawn freely without worrying about thread pool sizes.
This is the concurrency model functional languages (Erlang, Akka) have had for decades, finally available natively in Java.
Not a silver bullet — three scenarios where platform threads win
1. CPU-bound workloads
Virtual threads add no value when work is purely computational. One thread per core on a fixed pool is already optimal. Using virtual threads just adds mounting/unmounting overhead.
java// WRONG for CPU-bound — Fibonacci, image processing, numerical sim
Executors.newVirtualThreadPerTaskExecutor()
// RIGHT for CPU-bound
Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors())
2. Long-lived CPU-hungry threads
A single long-running CPU-bound virtual thread will keep its carrier busy, preventing other virtual threads from making progress. The unmount-on-block design only helps when threads actually block.
3. ThreadLocal-heavy code
Virtual threads can number in the millions. Each ThreadLocal occupies per-thread memory. Libraries that store significant data in ThreadLocal (e.g. per-thread connection caches) will consume far more memory than expected.
Use ScopedValue (Java 21 preview) as a lighter alternative for request-scoped data.
4. Synchronized-heavy code
See Q93 — pinning negates the benefits until the JDK fixes synchronized.
Java 21 — Sequenced collections & more Java 21 · Q96-100
The new SequencedCollection interface, HashMap improvements, and other Java 21 niceties.
SequencedCollection and what problem does it solve?Java 21▶The problem — no unified API for "first" and "last"
Before Java 21, getting the first/last element of an ordered collection required different APIs per collection type:
java// LinkedList had getFirst() / getLast()
// ArrayList required list.get(0) / list.get(list.size() - 1)
// LinkedHashSet required iteration just to peek at first
// TreeSet had first() / last() but different semantics
Java 21 — unified interface
javaList<String> list = new ArrayList<>(List.of("a", "b", "c"));
list.getFirst(); // "a"
list.getLast(); // "c"
list.addFirst("z"); // list becomes [z, a, b, c]
list.addLast("d"); // list becomes [z, a, b, c, d]
list.reversed(); // returns a reversed VIEW (not a copy)
Interface hierarchy
SequencedCollection<E>— addsaddFirst/Last,getFirst/Last,removeFirst/Last,reversed()SequencedSet<E>— extends the above + SetSequencedMap<K,V>— analogous for maps
ArrayList, LinkedList, LinkedHashSet, TreeSet, LinkedHashMap, and TreeMap all now implement these interfaces.
LinkedHashMap using the new API.Java 21▶Solution
javavar map = new LinkedHashMap<String, Integer>();
map.put("a", 1);
map.put("b", 2);
map.put("c", 3);
map.firstEntry(); // a=1
map.lastEntry(); // c=3
// Reversed VIEW
map.reversed(); // { c=3, b=2, a=1 }
// Put a new first / last
map.putFirst("z", 0); // inserts at the beginning
map.putLast("d", 4); // moves/inserts at end
Important — reversed() is a view, not a copy
Modifications to the reversed view affect the original map. This is consistent with Collections.unmodifiableList, Map.keySet, etc.
Stream.toList() vs Collectors.toList().Java 21▶Comparison
java// Pre-Java 16 — verbose
List<String> list1 = employees.stream()
.map(Employee::getName)
.collect(Collectors.toList());
// Returns ArrayList (mutable)
// Java 16+ — cleaner
List<String> list2 = employees.stream()
.map(Employee::getName)
.toList();
// Returns an UNMODIFIABLE list
Three differences
| Aspect | Collectors.toList() | .toList() |
|---|---|---|
| Mutability | Mutable (ArrayList) | Unmodifiable |
| Null handling | Allows nulls | Allows nulls |
| Performance | Baseline | Slightly faster (pre-sized) |
Prefer .toList() unless you need to mutate the result. Immutability is a feature, not a limitation.
HashMap methods putIfAbsent, computeIfAbsent, and merge.Java 21▶Three powerful methods (actually Java 8+, but heavily used in modern code)
javaMap<String, List<Employee>> byDept = new HashMap<>();
// BAD — verbose, handles null manually
for (Employee e : employees) {
List<Employee> list = byDept.get(e.getDeptName());
if (list == null) {
list = new ArrayList<>();
byDept.put(e.getDeptName(), list);
}
list.add(e);
}
// GOOD — computeIfAbsent
for (Employee e : employees) {
byDept.computeIfAbsent(e.getDeptName(), k -> new ArrayList<>()).add(e);
}
merge — atomic update-or-insert
javaMap<String, Integer> wordCount = new HashMap<>();
for (String word : words) {
wordCount.merge(word, 1, Integer::sum);
// If absent: put(word, 1)
// If present: put(word, oldValue + 1)
}
putIfAbsent — only if not already present
javaMap<String, String> config = new HashMap<>();
config.putIfAbsent("timeout", "5000"); // sets
config.putIfAbsent("timeout", "9999"); // no-op — key existsString.stripIndent() and text blocks (Java 15+).Java 21▶Text blocks solve multi-line string pain
java// Pre-Java 15 — escape hell
String json = "{\n" +
" \"name\": \"Arjun\",\n" +
" \"age\": 32\n" +
"}";
// Java 15+ — text block
String json = """
{
"name": "Arjun",
"age": 32
}
""";
Indentation handling — stripIndent()
Text blocks automatically strip common leading whitespace based on the indentation of the closing """. String.stripIndent() does this explicitly on any string.
javaString raw = """
hello
world
""";
// raw becomes:
// "hello\n world\n" -- 8 spaces of common indent removed
Interpolation — formatted()
javaString html = """
<div>Hello, %s! You have %d messages.</div>
""".formatted(name, count);
Ideal for
- JSON / XML literals in tests
- SQL queries in code
- HTML templates
- Multi-line log messages
Congratulations — you've reached the end of all 100 questions! 🎉