Java 8 · 11 · 17 · 21 Senior level 100 programming questions ~2 hours

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.

#1Get all employees in the 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.

#2Find employees whose name starts with 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"))
#3Get a list of just the employee 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 list
#4Count employees older than 35.Java 8

Solution

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.

#5Get all distinct department names.Java 8

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.

#6Sort employees by salary ascending.Java 8

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.

#7Sort employees by department ascending, then by salary descending.Java 8

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.

#8Get the first 3 employees ordered by salary descending.Java 8

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.

#9Skip the first 2 highest-salary employees and return the rest (ordered by salary desc).Java 8

Solution

javaList<Employee> rest = employees.stream()
    .sorted(Comparator.comparingLong(Employee::getSalary).reversed())
    .skip(2)
    .collect(Collectors.toList());
#10Check whether any employee earns more than 200,000.Java 8

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 element
  • findAny — return any match (faster on parallel streams)
#11Verify that all employees earn at least 50,000.Java 8

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.

#12Get a list of all names in UPPERCASE, sorted alphabetically.Java 8

Solution

javaList<String> upperNames = employees.stream()
    .map(e -> e.getName().toUpperCase())
    .sorted()
    .collect(Collectors.toList());
#13Given a 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).

#14Extract all unique characters from a list of words, sorted.Java 8

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.

#15Sort employees by age, but put null names at the end.Java 8

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.

#16Group employees by department.Java 8

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.

#17Count employees per department.Java 8

Solution

javaMap<String, Long> countByDept = employees.stream()
    .collect(Collectors.groupingBy(Employee::getDeptName, Collectors.counting()));
// { Engineering=5, Finance=2, HR=2, Sales=3 }
#18Group employees by department, getting only a list of names (not full Employee objects).Java 8

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.

#19Group employees by department and get the average salary per department.Java 8

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.

#20Get the highest-paid employee per department.Java 8

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)));
#21Partition employees into two groups: those earning above 100,000 and those below.Java 8

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.

#22Count male and female employees using partitioningBy.Java 8

Solution

javaMap<Boolean, Long> genderCount = employees.stream()
    .collect(Collectors.partitioningBy(
        e -> "M".equals(e.getGender()),
        Collectors.counting()));
#23Group employees first by department, then by gender — nested grouping.Java 8

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.

#24For each department, get the count of male and female employees.Java 8

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}, ... }
#25Which department has the most employees?Java 8

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.

#26Get full salary statistics per department (count, sum, min, max, average) in a single pass.Java 8

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.

#27Group employees by city and collect only the set of distinct department names per city.Java 8

Solution

javaMap<String, Set<String>> deptsByCity = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getCity,
        Collectors.mapping(Employee::getDeptName, Collectors.toSet())));
#28Compute the total salary paid out per department and sort the result by that total descending.Java 8

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.

#29List down the department names that have more than 2 employees.Java 8

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());
#30Get the average age of employees per gender.Java 8

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.

#31Find the employee with the highest salary.Java 8

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.

#32Find the second highest salary in the organization.Java 8

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.

#33Find the Nth highest salary generically — parameterized solution.Java 8

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.

#34Find the second highest-paid employee per department.Java 8

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.

#35Calculate the total salary expense of the organization.Java 8

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 AmapToLong avoids boxing to Long on every element.

#36Find the average salary across the organization.Java 8

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.

#37Find the highest-paid female employee.Java 8

Solution

javaOptional<Employee> topFemale = employees.stream()
    .filter(e -> "F".equals(e.getGender()))
    .max(Comparator.comparingLong(Employee::getSalary));
#38Rank employees by salary — assign rank 1 to the highest, 2 to the next, and so on.Java 8

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.

#39Find employees whose salary is above the organization-wide average.Java 8

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.

#40For each department, find employees earning above that department's average salary.Java 8

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.

#41Join all employee names into a single comma-separated string.Java 8

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, ...]"
#42Build a 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.

#43Find the longest employee name in the list.Java 8

Solution

javaOptional<String> longest = employees.stream()
    .map(Employee::getName)
    .max(Comparator.comparingInt(String::length));
#44Reverse the order of a stream.Java 8

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();
#45Given a list of integers, find the sum of squares.Java 8

Solution

javaint sumOfSquares = List.of(1, 2, 3, 4, 5).stream()
    .mapToInt(Integer::intValue)
    .map(n -> n * n)
    .sum();
// 55
#46Count the frequency of each character in a string.Java 8

Solution

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()));
#47Find the first non-repeating character in a string.Java 8

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.

#48Given a list of integers, find duplicates.Java 8

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());
#49Find the most frequent element in a list.Java 8

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"
#50Implement a generic 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.

#51What are the four canonical functional interfaces and when is each used?Java 8

The four foundational interfaces in java.util.function

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

#52Why should you never use 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"));
#53Should you use 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

  • Optional is not Serializable in 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);
#54Given 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.

#55Write a 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() — NOT
  • Predicate.not(existing) (Java 11+) — static form, cleaner when using method references
#56Compose two 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.

#57Explain method references with all four variants.Java 8

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)
#58What is a default method in an interface? Write an example.Java 8

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
}
#59What is a static method in an interface? When would you use one?Java 8

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.

#60Create an infinite stream of even numbers and take the first 10.Java 8

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.

#61What does the new 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");
}
#62Use 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 sides
  • stripLeading() — only left
  • stripTrailing() — only right

Always prefer strip() over trim() for user-facing text. trim() is a legacy that pre-dates proper Unicode support.

#63Repeat a string N times without a loop.Java 11

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.

#64Split a multi-line string into a stream of lines.Java 11

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").

#65Demonstrate correct usage of 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.

#66Use 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()
#67Make an HTTP GET request using the Java 11 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.

#68Convert a collection to an array using the new 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.

#69Launch a single-file Java program without compiling first (JEP 330).Java 11

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.

#70Use 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.

#71Declare a 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 getX but just x() — the component name
#72Add validation to a record using a compact constructor.Java 17

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.
#73Can a record have static fields and methods? Instance methods?Java 17

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.
#74Implement an interface with a record.Java 17

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.

#75Use records to replace DTOs/POJOs in a Spring Boot REST API.Java 17

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.

#76Create a nested record used as a grouping key.Java 17

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.

#77Override a record's 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.

#78Deconstruct a record in a Stream pipeline (Java 21+ feature, but ties in here).Java 17

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.

#79Declare a sealed interface 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 extended
  • sealed — itself restricts further subclassing
  • non-sealed — explicitly opens up hierarchy (allows unrestricted subclasses)

Records are implicitly final, which is why they're commonly paired with sealed interfaces.

#80Why use sealed classes — give three concrete benefits.Java 17

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.

#81Can a record be in the 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.

#82What's the difference between 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"
#83Model a payment processing system using sealed interfaces.Java 17

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.

#84Can permitted subclasses be in a different package or module?Java 17

The rules

  1. Same module if the sealed class is in a named module.
  2. 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.

#85Rewrite a traditional 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

  • s is in scope inside the if block
  • Also in scope in && chains: if (o instanceof String s && s.length() > 5) ...
  • Also in the else of negated: if (!(o instanceof String s)) return; s.length(); // works!
#86Write a method that returns a description based on the runtime type using a switch expression with pattern matching.Java 17

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.

#87Use record patterns to deconstruct a record in a switch (Java 21).Java 17

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.

#88Use pattern matching with 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).

#89Handle 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.

#90Convert a visitor pattern implementation to sealed classes + pattern matching.Java 17

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.

#91What is a virtual thread and how does it differ from a platform thread?Java 21

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.

#92Spawn 10,000 virtual threads, each doing a simulated blocking call. Show the code.Java 21

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.

#93What is "pinning" in virtual threads and why should you avoid 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.

#94Demonstrate structured concurrency using 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, ShutdownOnFailure cancels 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.

#95When should you NOT use virtual threads?Java 21

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.

#96What is 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> — adds addFirst/Last, getFirst/Last, removeFirst/Last, reversed()
  • SequencedSet<E> — extends the above + Set
  • SequencedMap<K,V> — analogous for maps

ArrayList, LinkedList, LinkedHashSet, TreeSet, LinkedHashMap, and TreeMap all now implement these interfaces.

#97Get the first and last entries of a 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.

#98Demonstrate the new 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

AspectCollectors.toList().toList()
MutabilityMutable (ArrayList)Unmodifiable
Null handlingAllows nullsAllows nulls
PerformanceBaselineSlightly faster (pre-sized)

Prefer .toList() unless you need to mutate the result. Immutability is a feature, not a limitation.

#99Use the new 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 exists
#100Final question — explain String.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! 🎉