Stream API Interview Questions | JiQuest

add

#

Stream API

java.util.stream interview preparation

Java Stream API: 200+ interview-ready questions with working code answers.

Practice Stream, IntStream/LongStream/DoubleStream, Collectors, Optional, parallel streams, Files.lines, and custom collectors with real, compilable Java code for every question -- from basics to production pitfalls.

208Questions
14Categories
100%Runnable code
Collection Array / IntStream Files.lines() Intermediate Ops filter · map · sorted flatMap (lazy, chainable) Terminal Op collect · reduce forEach (triggers run) Result List / Map / value nothing runs until a terminal operation is called (laziness)

What makes a strong Stream API answer?

Interviewers are checking whether you understand laziness, statelessness, and when a stream is the wrong tool -- not just whether you remember method names.

Right operationKnow the difference between intermediate (lazy) and terminal (eager) operations.
Right collectorPick the simplest Collector that produces the exact shape you need.
Stateless lambdasAvoid mutating shared state inside map/filter/forEach, especially in parallel.
Know the costExplain boxing, parallel overhead, and when a plain loop is clearer.
ApproachUse whenWatch out for
Sequential streamDefault choice for small-to-medium collections and most business logic.No parallel speedup, but usually fine and easier to reason about.
Parallel streamLarge, CPU-bound, independent-element workloads on multi-core hardware.Shares the common ForkJoinPool; avoid for I/O-bound or small datasets.
Stream<Integer> (boxed)You need Collectors, Optional, or object semantics.Boxing overhead on every element compared to a primitive stream.
IntStream / LongStream / DoubleStreamNumeric aggregation: sum, average, statistics.Must call .boxed() to use with most Collectors.

Categories

Questions and answers

Every question has a real, working code answer. Domain objects (Employee, Order, Transaction, Product, Student, Book, Person, Customer, Department) are assumed to have standard getters.

Stream Basics & Creation

1. Create a Stream from a List of Strings and print each element.

List<String> names = List.of("Ann", "Ben", "Cara");
names.stream().forEach(System.out::println);

2. Create a Stream directly from individual values using Stream.of.

Stream<Integer> numbers = Stream.of(3, 7, 11, 19);
numbers.forEach(System.out::println);

3. Create an empty Stream and show that it produces no elements.

Stream<String> empty = Stream.empty();
long count = empty.count();
System.out.println(count); // 0

4. Create an infinite Stream of even numbers with Stream.iterate and take the first 5.

List<Integer> firstFiveEvens = Stream.iterate(0, n -> n + 2)
    .limit(5)
    .collect(Collectors.toList());

5. Create a Stream of random UUID strings with Stream.generate, limited to 3 values.

List<String> ids = Stream.generate(() -> UUID.randomUUID().toString())
    .limit(3)
    .collect(Collectors.toList());

6. Create a Stream from an array of Product objects.

Product[] productArray = { p1, p2, p3 };
List<String> productNames = Arrays.stream(productArray)
    .map(Product::getName)
    .collect(Collectors.toList());

7. Convert a Map<String, Employee> into a stream of its entries and print each key-value pair.

Map<String, Employee> employeesById = fetchEmployeesById();
employeesById.entrySet().stream()
    .forEach(e -> System.out.println(e.getKey() + " -> " + e.getValue().getName()));

8. Use the Java 9 three-argument Stream.iterate to generate values while a condition holds, without an explicit limit.

List<Integer> powersOfTwoUnder100 = Stream.iterate(1, n -> n < 100, n -> n * 2)
    .collect(Collectors.toList());

9. Create a Stream of Optional values and filter out the empty ones using Java 9's Optional::stream.

List<Optional<String>> maybeNames =
    List.of(Optional.of("Amy"), Optional.empty(), Optional.of("Sam"));

List<String> presentNames = maybeNames.stream()
    .flatMap(Optional::stream)
    .collect(Collectors.toList());

10. Explain and demonstrate why a Stream can only be consumed once.

Stream<String> stream = Stream.of("a", "b", "c");
stream.forEach(System.out::println);
stream.count(); // throws IllegalStateException: stream has already been operated upon or closed
Why A Stream is a one-shot pipeline description, not a reusable data structure. Once a terminal operation runs, the underlying source has already been traversed and closed.

11. Create a Stream from a Collection with Collection.stream() and count how many elements are present.

List<Department> departments = fetchDepartments();
long departmentCount = departments.stream().count();

12. Build a Stream that concatenates two existing streams using Stream.concat.

Stream<String> firstBatch = Stream.of("Alice", "Bob");
Stream<String> secondBatch = Stream.of("Cara", "Drew");

List<String> combined = Stream.concat(firstBatch, secondBatch)
    .collect(Collectors.toList());

13. Create a Stream directly from a String's characters using chars().

String word = "stream";
List<Character> letters = word.chars()
    .mapToObj(c -> (char) c)
    .collect(Collectors.toList());

14. Use Stream.ofNullable (Java 9+) to safely build a single-element stream from a value that might be null.

String maybeNull = fetchNickname();
long resultCount = Stream.ofNullable(maybeNull).count(); // 0 or 1

15. Create a Stream from an Iterable that does not directly expose a stream() method.

Iterable<Book> bookIterable = getLegacyBookIterable();
Stream<Book> bookStream = StreamSupport.stream(bookIterable.spliterator(), false);
long total = bookStream.count();

Filtering & Mapping

16. Given a list of Student objects, filter out students with a GPA below 3.0 and collect their names.

List<String> honorRollNames = students.stream()
    .filter(s -> s.getGpa() >= 3.0)
    .map(Student::getName)
    .collect(Collectors.toList());

17. Given a list of Employee objects, filter employees whose salary is above a threshold and collect them into a Set.

Set<Employee> highEarners = employees.stream()
    .filter(e -> e.getSalary() > threshold)
    .collect(Collectors.toSet());

18. Filter employees older than 30 whose name starts with "A", and map the matches to uppercase names.

List<String> matchingNames = employees.stream()
    .filter(e -> e.getAge() > 30)
    .filter(e -> e.getName().startsWith("A"))
    .map(e -> e.getName().toUpperCase())
    .collect(Collectors.toList());

19. From a list of numbers, skip the first 5 elements and then collect the next 10 into a list.

List<Integer> page = numbers.stream()
    .skip(5)
    .limit(10)
    .collect(Collectors.toList());

20. Map a list of Order objects to their amounts, then filter out amounts below $50.

List<Double> significantAmounts = orders.stream()
    .map(Order::getAmount)
    .filter(amount -> amount >= 50.0)
    .collect(Collectors.toList());

21. Given a list of Customer objects, filter only customers with a verified email and map to their email addresses.

List<String> verifiedEmails = customers.stream()
    .filter(Customer::isEmailVerified)
    .map(Customer::getEmail)
    .collect(Collectors.toList());

22. Use peek() to log products passing through a pipeline while filtering out-of-stock items.

List<Product> inStock = products.stream()
    .peek(p -> System.out.println("Checking " + p.getName()))
    .filter(Product::isAvailable)
    .collect(Collectors.toList());
Caution peek() is meant for debugging observation, not business logic. Its execution is unspecified when the pipeline can be optimized or short-circuited.

23. Filter a list of Product objects to its distinct categories.

List<String> categories = products.stream()
    .map(Product::getCategory)
    .distinct()
    .collect(Collectors.toList());

24. Given a list of Transaction objects, map each to a formatted description combining its type and amount.

List<String> descriptions = transactions.stream()
    .map(t -> t.getType() + ": $" + t.getAmount())
    .collect(Collectors.toList());

25. Filter a list of integers to keep only prime numbers, using a helper predicate method reference.

List<Integer> primes = numbers.stream()
    .filter(NumberUtils::isPrime)
    .collect(Collectors.toList());

26. From a list of Book objects, map to titles only for books published after 2015.

List<String> recentTitles = books.stream()
    .filter(b -> b.getYear() > 2015)
    .map(Book::getTitle)
    .collect(Collectors.toList());

27. Given a list of Strings, filter out blank entries and map the rest to a trimmed, lowercase form.

List<String> cleaned = rawInputs.stream()
    .filter(s -> s != null && !s.isBlank())
    .map(s -> s.trim().toLowerCase())
    .collect(Collectors.toList());

28. Given a list of Department objects, filter departments with more than 10 employees and map to their names.

List<String> largeDepartments = departments.stream()
    .filter(d -> d.getEmployees().size() > 10)
    .map(Department::getName)
    .collect(Collectors.toList());

29. Use takeWhile (Java 9+) to take numbers from a sorted list while they remain below 100.

List<Integer> underLimit = sortedNumbers.stream()
    .takeWhile(n -> n < 100)
    .collect(Collectors.toList());

30. Use dropWhile (Java 9+) to skip leading zero-amount transactions and keep the rest.

List<Transaction> nonZeroFromFirstReal = transactions.stream()
    .dropWhile(t -> t.getAmount() == 0.0)
    .collect(Collectors.toList());

Sorting & Comparators

31. Sort a list of Employee objects by salary in descending order.

List<Employee> sortedBySalaryDesc = employees.stream()
    .sorted(Comparator.comparing(Employee::getSalary).reversed())
    .collect(Collectors.toList());

32. Find the top 3 Student objects by GPA.

List<Student> topThreeByGpa = students.stream()
    .sorted(Comparator.comparing(Student::getGpa).reversed())
    .limit(3)
    .collect(Collectors.toList());

33. Sort Book objects by author name, then by price descending for ties.

List<Book> sortedBooks = books.stream()
    .sorted(Comparator.comparing(Book::getAuthor)
        .thenComparing(Comparator.comparing(Book::getPrice).reversed()))
    .collect(Collectors.toList());

34. Find the name of the employee with the second-highest salary.

Optional<String> secondHighestPaid = employees.stream()
    .sorted(Comparator.comparing(Employee::getSalary).reversed())
    .skip(1)
    .map(Employee::getName)
    .findFirst();

35. Sort the ages extracted from a list of Person objects using natural ordering.

List<Integer> ages = people.stream()
    .map(Person::getAge)
    .sorted(Comparator.naturalOrder())
    .collect(Collectors.toList());

36. Sort a list of Strings by length, then alphabetically for ties.

List<String> sorted = words.stream()
    .sorted(Comparator.comparingInt(String::length).thenComparing(Comparator.naturalOrder()))
    .collect(Collectors.toList());

37. Sort a list of Product objects by a discount field that can be null, pushing nulls to the end.

List<Product> sortedByDiscount = products.stream()
    .sorted(Comparator.comparing(Product::getDiscount,
        Comparator.nullsLast(Comparator.naturalOrder())))
    .collect(Collectors.toList());

38. Sort Customer objects in reverse alphabetical order by name.

List<Customer> sortedDesc = customers.stream()
    .sorted(Comparator.comparing(Customer::getName).reversed())
    .collect(Collectors.toList());

39. Sort Order objects by order date (most recent first), then by amount descending.

List<Order> sortedOrders = orders.stream()
    .sorted(Comparator.comparing(Order::getOrderDate).reversed()
        .thenComparing(Comparator.comparing(Order::getAmount).reversed()))
    .collect(Collectors.toList());

40. Collect unique Person names into a TreeSet ordered by name length.

Set<String> namesByLength = people.stream()
    .map(Person::getName)
    .collect(Collectors.toCollection(
        () -> new TreeSet<>(Comparator.comparingInt(String::length))));

41. Sort a list of Transaction objects using a standalone Comparator class instead of a lambda.

class AmountThenDateComparator implements Comparator<Transaction> {
    @Override
    public int compare(Transaction a, Transaction b) {
        int byAmount = Double.compare(b.getAmount(), a.getAmount());
        return byAmount != 0 ? byAmount : a.getDate().compareTo(b.getDate());
    }
}

List<Transaction> sorted = transactions.stream()
    .sorted(new AmountThenDateComparator())
    .collect(Collectors.toList());

42. Sort Employee objects by department, then by salary descending within each department.

List<Employee> sorted = employees.stream()
    .sorted(Comparator.comparing(Employee::getDepartment)
        .thenComparing(Employee::getSalary, Comparator.reverseOrder()))
    .collect(Collectors.toList());

sorted.forEach(e ->
    System.out.println(e.getDepartment() + " | " + e.getName() + " | " + e.getSalary()));

Reduction & Aggregation

43. Given a list of Order objects, extract the order amounts and calculate the total sum.

double totalOrderAmount = orders.stream()
    .mapToDouble(Order::getAmount)
    .sum();

44. Given a list of integers, find the product of all non-zero elements using reduce with an identity.

int product = numbers.stream()
    .filter(n -> n != 0)
    .reduce(1, (a, b) -> a * b);

45. Compute the product of non-zero integers using reduce without an identity, returning Optional.empty() when none exist.

Optional<Integer> product = integers.stream()
    .filter(n -> n != 0)
    .reduce((a, b) -> a * b);

46. Find the length of the longest String in a list.

int maxLength = strings.stream()
    .mapToInt(String::length)
    .max()
    .orElse(0);

47. Find the average age of people younger than 40.

double averageAge = people.stream()
    .filter(p -> p.getAge() < 40)
    .mapToInt(Person::getAge)
    .average()
    .orElse(0.0);

48. Find the total amount of only the transactions marked "COMPLETED".

double totalCompletedAmount = transactions.stream()
    .filter(t -> "COMPLETED".equals(t.getStatus()))
    .mapToDouble(Transaction::getAmount)
    .sum();

49. Find the Employee with the longest tenure.

Optional<Employee> longestTenure = employees.stream()
    .max(Comparator.comparing(Employee::getTenure));

50. Find the Order with the highest amount.

Optional<Order> largestOrder = orders.stream()
    .max(Comparator.comparing(Order::getAmount));

51. Find the most expensive Product, returning Optional.empty() for an empty list.

Optional<Product> mostExpensive = products.stream()
    .max(Comparator.comparing(Product::getPrice));

52. Use the three-argument overload of reduce to sum String lengths in a way that also works safely in parallel.

int totalLength = words.parallelStream()
    .reduce(0,
        (partial, word) -> partial + word.length(),
        Integer::sum);

53. Use reduce with a BinaryOperator to find the shortest String in a list without sorting.

Optional<String> shortest = words.stream()
    .reduce((a, b) -> a.length() <= b.length() ? a : b);

54. Count how many Transaction objects have type "CREDIT" without using Collectors.counting().

long creditCount = transactions.stream()
    .filter(t -> "CREDIT".equals(t.getType()))
    .count();

55. Use reduce to build a running total balance from a list of ledger entries, starting from an opening balance.

BigDecimal closingBalance = ledgerEntries.stream()
    .map(LedgerEntry::getAmount)
    .reduce(openingBalance, BigDecimal::add);

56. Find the minimum salary among employees using a primitive stream's min().

OptionalDouble lowestSalary = employees.stream()
    .mapToDouble(Employee::getSalary)
    .min();

57. Use reduce to join a list of words into one sentence without Collectors.joining().

String sentence = words.stream()
    .reduce("", (a, b) -> a.isEmpty() ? b : a + " " + b);

Collectors -- Basic

58. Collect unique, case-insensitive Strings into a Set.

Set<String> uniqueLower = strings.stream()
    .map(String::toLowerCase)
    .collect(Collectors.toSet());

59. Join a list of Book titles into a single comma-separated string.

String joinedTitles = bookTitles.stream()
    .collect(Collectors.joining(", "));

60. Convert a list of Person objects into a Map of id to name.

Map<Integer, String> idToName = people.stream()
    .collect(Collectors.toMap(Person::getId, Person::getName));

61. Join Product names with a prefix and suffix, e.g. "[A, B, C]", using the three-argument form of Collectors.joining.

String display = products.stream()
    .map(Product::getName)
    .collect(Collectors.joining(", ", "[", "]"));

62. Collect Employee names into an immutable list using Collectors.toUnmodifiableList (Java 10+).

List<String> names = employees.stream()
    .map(Employee::getName)
    .collect(Collectors.toUnmodifiableList());

63. Build a Map from Order id to Order, handling potential duplicate keys with a merge function.

Map<String, Order> ordersById = orders.stream()
    .collect(Collectors.toMap(
        Order::getId,
        Function.identity(),
        (existing, duplicate) -> existing));

64. Collect Customer objects into a specific collection type, a LinkedList, using Collectors.toCollection.

LinkedList<Customer> customerQueue = customers.stream()
    .collect(Collectors.toCollection(LinkedList::new));

65. Compute the average price across all Product objects using Collectors.averagingDouble.

double averagePrice = products.stream()
    .collect(Collectors.averagingDouble(Product::getPrice));

66. Compute the total stock quantity across all Product objects using Collectors.summingInt.

int totalStock = products.stream()
    .collect(Collectors.summingInt(Product::getStock));

67. Count how many Student objects are in a list using Collectors.counting().

long studentCount = students.stream()
    .collect(Collectors.counting());

68. Build summary statistics (min, max, average, sum, count) for a list of Product prices.

DoubleSummaryStatistics priceStats = products.stream()
    .collect(Collectors.summarizingDouble(Product::getPrice));

System.out.println("avg=" + priceStats.getAverage() + " max=" + priceStats.getMax());

69. Collect Employee names into a case-insensitive sorted Set using Collectors.toCollection with a TreeSet.

Set<String> sortedNames = employees.stream()
    .map(Employee::getName)
    .collect(Collectors.toCollection(() -> new TreeSet<>(String.CASE_INSENSITIVE_ORDER)));

70. Convert a list of Transaction ids into a Set to eliminate duplicates using Collectors.toSet().

Set<String> uniqueTransactionIds = transactions.stream()
    .map(Transaction::getId)
    .collect(Collectors.toSet());

71. Use Collectors.reducing to sum Order amounts as an alternative to Collectors.summingDouble.

double total = orders.stream()
    .collect(Collectors.reducing(0.0, Order::getAmount, Double::sum));

72. Collect a stream of Integer values into both a boxed array and a primitive int array.

Integer[] boxedArray = numbers.stream().toArray(Integer[]::new);

int[] primitiveArray = numbers.stream()
    .mapToInt(Integer::intValue)
    .toArray();

Collectors -- Grouping & Partitioning

73. Partition a list of Person objects into two groups: older than 30 and 30 or younger.

Map<Boolean, List<Person>> partitionedByAge = people.stream()
    .collect(Collectors.partitioningBy(p -> p.getAge() > 30));

74. Group Transaction objects by status and count how many fall into each group.

Map<String, Long> countByStatus = transactions.stream()
    .collect(Collectors.groupingBy(Transaction::getStatus, Collectors.counting()));

75. Group Product objects by category, then within each category partition by availability.

Map<String, Map<Boolean, List<Product>>> availabilityByCategory = products.stream()
    .collect(Collectors.groupingBy(
        Product::getCategory,
        Collectors.partitioningBy(Product::isAvailable)));

76. Find the highest-paid Employee in each department.

Map<String, Optional<Employee>> topEarnerByDept = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.maxBy(Comparator.comparing(Employee::getSalary))));

77. Group Transaction objects by type, mapping each group down to just its transaction ids.

Map<String, List<String>> idsByType = transactions.stream()
    .collect(Collectors.groupingBy(
        Transaction::getType,
        Collectors.mapping(Transaction::getId, Collectors.toList())));

78. Group Product objects by category and sum the stock available in each category.

Map<String, Integer> stockByCategory = products.stream()
    .collect(Collectors.groupingBy(
        Product::getCategory,
        Collectors.summingInt(Product::getStock)));

79. Group Order objects by customer and sum the total amount spent by each customer.

Map<String, Double> totalSpentByCustomer = orders.stream()
    .collect(Collectors.groupingBy(
        Order::getCustomerName,
        Collectors.summingDouble(Order::getAmount)));

80. Group a list of Strings by their length.

Map<Integer, List<String>> stringsByLength = words.stream()
    .collect(Collectors.groupingBy(String::length));

81. Group Transaction objects by month, and within each month, group further by status.

Map<Month, Map<String, List<Transaction>>> byMonthThenStatus = transactions.stream()
    .collect(Collectors.groupingBy(
        t -> t.getDate().getMonth(),
        Collectors.groupingBy(Transaction::getStatus)));

82. Find the average salary per department.

Map<String, Double> avgSalaryByDept = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.averagingDouble(Employee::getSalary)));

83. Group Transaction objects by type and find the highest transaction amount per type.

Map<String, Optional<Transaction>> maxAmountByType = transactions.stream()
    .collect(Collectors.groupingBy(
        Transaction::getType,
        Collectors.maxBy(Comparator.comparing(Transaction::getAmount))));

84. Filter employees with more than 5 years of experience, then sum salary by department.

Map<String, Integer> seniorSalaryByDept = employees.stream()
    .filter(e -> e.getYearsOfExperience() > 5)
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.summingInt(Employee::getSalary)));

85. Group Student objects by letter grade.

Map<String, List<Student>> studentsByGrade = students.stream()
    .collect(Collectors.groupingBy(Student::getGrade));

86. Find the department with the highest average salary by chaining a groupingBy result into a second stream.

Optional<Map.Entry<String, Double>> topDept = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.averagingDouble(Employee::getSalary)))
    .entrySet().stream()
    .max(Map.Entry.comparingByValue());

String departmentName = topDept.map(Map.Entry::getKey).orElse("No department");

87. Build a nested report of total transaction amount by type and then by month.

Map<String, Map<Month, Double>> amountByTypeAndMonth = transactions.stream()
    .collect(Collectors.groupingBy(
        Transaction::getType,
        Collectors.groupingBy(
            t -> t.getDate().getMonth(),
            Collectors.summingDouble(Transaction::getAmount))));

88. Group Person objects by city and compute the average age in each city.

Map<String, Double> avgAgeByCity = people.stream()
    .collect(Collectors.groupingBy(
        Person::getCity,
        Collectors.averagingInt(Person::getAge)));

89. Group Transaction objects by type, sum the amount per type, then keep only types whose total exceeds a threshold.

Map<String, Double> largeTypeTotals = transactions.stream()
    .collect(Collectors.groupingBy(
        Transaction::getType,
        Collectors.summingDouble(Transaction::getAmount)))
    .entrySet().stream()
    .filter(entry -> entry.getValue() > threshold)
    .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

90. Count residents per city, keeping only cities with more than 10 residents.

Map<String, Long> largeCities = people.stream()
    .collect(Collectors.groupingBy(Person::getCity, Collectors.counting()))
    .entrySet().stream()
    .filter(entry -> entry.getValue() > 10)
    .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

91. Group Employee objects by department, joining just their names into one readable string per department.

Map<String, String> namesByDept = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.mapping(Employee::getName, Collectors.joining(", "))));

92. Group Order objects by customer, keeping only the maximum order amount per customer as a primitive double instead of an Optional<Order>.

Map<String, Double> maxAmountByCustomer = orders.stream()
    .collect(Collectors.groupingBy(
        Order::getCustomerName,
        Collectors.reducing(0.0, Order::getAmount, Double::max)));

Collectors -- Advanced & Custom

93. Find the highest-paid Employee per department, unwrapped directly to an Employee instead of an Optional.

Map<String, Employee> topEarnerByDept = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.collectingAndThen(
            Collectors.maxBy(Comparator.comparing(Employee::getSalary)),
            Optional::get)));

94. Group Transaction objects by type and, within each type, keep only the top 5 by amount.

Map<String, List<Transaction>> top5ByType = transactions.stream()
    .collect(Collectors.groupingBy(
        Transaction::getType,
        Collectors.collectingAndThen(
            Collectors.toList(),
            list -> list.stream()
                .sorted(Comparator.comparing(Transaction::getAmount).reversed())
                .limit(5)
                .collect(Collectors.toList()))));

95. Group Product objects by category, sorting each group's products by price descending.

Map<String, List<Product>> byCategorySortedByPrice = products.stream()
    .collect(Collectors.groupingBy(
        Product::getCategory,
        Collectors.collectingAndThen(
            Collectors.toList(),
            list -> list.stream()
                .sorted(Comparator.comparing(Product::getPrice).reversed())
                .collect(Collectors.toList()))));

96. Wrap a grouped Employee result into an unmodifiable Map using collectingAndThen.

Map<String, List<Employee>> immutableByDept = employees.stream()
    .collect(Collectors.collectingAndThen(
        Collectors.groupingBy(Employee::getDepartment),
        Collections::unmodifiableMap));

97. Write a custom Collector using Collector.of that joins Strings with a pipe delimiter via a StringBuilder.

Collector<String, StringBuilder, String> customJoiner = Collector.of(
    StringBuilder::new,
    (sb, s) -> sb.append(s).append("|"),
    StringBuilder::append,
    sb -> sb.length() == 0 ? "" : sb.substring(0, sb.length() - 1));

String joined = words.stream().collect(customJoiner);

98. Write a custom Collector that accumulates Order amounts into a BigDecimal total, suitable for parallel use.

Collector<Order, ?, BigDecimal> sumCollector = Collector.of(
    () -> new BigDecimal[]{ BigDecimal.ZERO },
    (acc, order) -> acc[0] = acc[0].add(order.getAmount()),
    (acc1, acc2) -> new BigDecimal[]{ acc1[0].add(acc2[0]) },
    acc -> acc[0]);

BigDecimal total = orders.parallelStream().collect(sumCollector);

99. Use the Java 12 teeing collector to compute both the sum and count of Order amounts in a single pass, combining them into an average.

double averageOrderAmount = orders.stream()
    .collect(Collectors.teeing(
        Collectors.summingDouble(Order::getAmount),
        Collectors.counting(),
        (sum, count) -> count == 0 ? 0.0 : sum / count));

100. Use teeing to find both the cheapest and priciest Product in a single stream traversal.

record PriceRange(Optional<Product> cheapest, Optional<Product> priciest) {}

PriceRange range = products.stream()
    .collect(Collectors.teeing(
        Collectors.minBy(Comparator.comparing(Product::getPrice)),
        Collectors.maxBy(Comparator.comparing(Product::getPrice)),
        PriceRange::new));

101. Use Collectors.filtering (Java 9+) inside groupingBy so each department keeps only employees earning above 50000.

Map<String, List<Employee>> wellPaidByDept = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.filtering(e -> e.getSalary() > 50000, Collectors.toList())));
Note Unlike filtering before groupingBy, this keeps every department key in the resulting map, even when its filtered list ends up empty.

102. Use Collectors.flatMapping (Java 9+) inside groupingBy to collect distinct product names ordered per customer.

Map<String, Set<String>> productNamesByCustomer = customers.stream()
    .collect(Collectors.groupingBy(
        Customer::getName,
        Collectors.flatMapping(
            c -> c.getOrders().stream().flatMap(o -> o.getProducts().stream()).map(Product::getName),
            Collectors.toSet())));

103. Group Employee objects by department into a TreeMap (sorted by department name) using the three-argument groupingBy overload.

Map<String, List<Employee>> sortedByDeptName = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        TreeMap::new,
        Collectors.toList()));

104. Build a Map from Product id to Product, throwing a clear exception on duplicate ids.

Map<String, Product> productsById = products.stream()
    .collect(Collectors.toMap(
        Product::getId,
        Function.identity(),
        (a, b) -> { throw new IllegalStateException("Duplicate product id: " + a.getId()); }));

105. Find the cheapest Book in each category, unwrapped to a plain Book using minBy + collectingAndThen.

Map<String, Book> cheapestByCategory = books.stream()
    .collect(Collectors.groupingBy(
        Book::getCategory,
        Collectors.collectingAndThen(
            Collectors.minBy(Comparator.comparing(Book::getPrice)),
            Optional::get)));

106. Combine groupingBy with Collectors.summarizingInt to get full order-count statistics per customer.

Map<String, IntSummaryStatistics> orderStatsByCustomer = customers.stream()
    .collect(Collectors.groupingBy(
        Customer::getName,
        Collectors.summarizingInt(c -> c.getOrders().size())));

107. Implement a custom Collector that finishes directly into an immutable List using List.copyOf.

Collector<Employee, List<Employee>, List<Employee>> toImmutableList = Collector.of(
    ArrayList::new,
    List::add,
    (left, right) -> { left.addAll(right); return left; },
    List::copyOf);

List<Employee> immutableEmployees = employees.stream().collect(toImmutableList);

flatMap & Nested Structures

108. Given Customer objects each with a list of Order objects that each have a list of Product objects, find all unique products ordered.

Set<Product> allOrderedProducts = customers.stream()
    .flatMap(customer -> customer.getOrders().stream())
    .flatMap(order -> order.getProducts().stream())
    .collect(Collectors.toSet());

109. Find the longest word across a list of sentences by flattening each sentence into its individual words.

Optional<String> longestWord = sentences.stream()
    .flatMap(sentence -> Arrays.stream(sentence.split("\\s+")))
    .max(Comparator.comparingInt(String::length));

110. Merge three separate lists of Order objects from different regions into one distinct list.

List<Order> allRegionsOrders = Stream.of(ordersRegionEast, ordersRegionWest, ordersRegionCentral)
    .flatMap(Collection::stream)
    .distinct()
    .collect(Collectors.toList());

111. Find the highest-paid Employee across every Department by flattening Department into its Employees.

Optional<Employee> highestPaidOverall = departments.stream()
    .flatMap(department -> department.getEmployees().stream())
    .max(Comparator.comparing(Employee::getSalary));

112. Given Student objects each with a list of enrolled Course objects, flatten to a distinct list of all course names taught.

List<String> allCourseNames = students.stream()
    .flatMap(s -> s.getCourses().stream())
    .map(Course::getName)
    .distinct()
    .collect(Collectors.toList());

113. Given Order objects each with multiple OrderLine items, flatten to compute the total quantity ordered across all orders.

int totalQuantity = orders.stream()
    .flatMap(order -> order.getLines().stream())
    .mapToInt(OrderLine::getQuantity)
    .sum();

114. Flatten a List<List<Integer>> (a matrix of rows) into a single List<Integer>.

List<List<Integer>> matrix = List.of(List.of(1, 2), List.of(3, 4, 5), List.of(6));

List<Integer> flattened = matrix.stream()
    .flatMap(List::stream)
    .collect(Collectors.toList());

115. Use flatMapToInt to sum all the digits of every number in a list of numeric Strings.

int digitSum = numericStrings.stream()
    .flatMapToInt(String::chars)
    .map(c -> c - '0')
    .sum();

116. Given Department objects containing Employees who each have a list of Skill objects, find the distinct set of all skills company-wide.

Set<String> allSkills = departments.stream()
    .flatMap(d -> d.getEmployees().stream())
    .flatMap(e -> e.getSkills().stream())
    .map(Skill::getName)
    .collect(Collectors.toSet());

117. Use flatMap to expand each Transaction into its related audit events and count the total number of events.

long totalAuditEvents = transactions.stream()
    .flatMap(t -> t.getAuditEvents().stream())
    .count();

118. Given a Map<String, List<Product>> of products by category, flatten it back into one distinct List<Product>.

List<Product> allProducts = productsByCategory.values().stream()
    .flatMap(List::stream)
    .distinct()
    .collect(Collectors.toList());

119. Show the difference between map and flatMap by first producing a nested Stream<List<String>>, then flattening it correctly.

// map keeps the nested shape: Stream<List<String>>
List<List<String>> nested = sentences.stream()
    .map(s -> Arrays.asList(s.split("\\s+")))
    .collect(Collectors.toList());

// flatMap produces a single flat Stream<String>
List<String> flat = sentences.stream()
    .flatMap(s -> Arrays.stream(s.split("\\s+")))
    .collect(Collectors.toList());

Optional & Null-Safety With Streams

120. Find the first even number greater than 10 in a list, or return an empty Optional.

Optional<Integer> firstLargeEven = numbers.stream()
    .filter(n -> n > 10 && n % 2 == 0)
    .findFirst();

121. Safely unwrap an Optional<Employee> stream result using orElseThrow with a custom exception.

Employee manager = employees.stream()
    .filter(e -> "MANAGER".equals(e.getRole()))
    .findFirst()
    .orElseThrow(() -> new NoSuchElementException("No manager found"));

122. Chain Optional.map after a stream query to transform a possibly-absent result without an explicit null check.

String departmentName = employees.stream()
    .filter(e -> e.getId().equals(targetId))
    .findFirst()
    .map(Employee::getDepartment)
    .orElse("Unassigned");

123. Use findAny() instead of findFirst() on a parallel stream where order does not matter.

Optional<Product> anyOutOfStock = products.parallelStream()
    .filter(p -> !p.isAvailable())
    .findAny();
Difference findFirst() is deterministic even on a parallel stream because it respects encounter order. findAny() may return a different matching element between runs, but can finish faster since any thread's match is acceptable.

124. Use Optional::stream (Java 9+) to filter out empty Optionals produced while mapping over a list of ids.

List<Customer> resolvedCustomers = customerIds.stream()
    .map(this::findCustomerById) // returns Optional<Customer>
    .flatMap(Optional::stream)
    .collect(Collectors.toList());

125. Combine two Optionals from two independent stream lookups into a single combined result without nested if-checks.

Optional<Customer> customerOpt = findCustomerById(id);
Optional<Order> latestOrderOpt = findLatestOrder(id);

Optional<String> summary = customerOpt.flatMap(customer ->
    latestOrderOpt.map(order -> customer.getName() + " last ordered $" + order.getAmount()));

126. Return a safe default average price from a stream reduction when the price list is empty.

double averagePrice = prices.stream()
    .mapToDouble(Double::doubleValue)
    .average()
    .orElse(0.0);

127. Use ifPresentOrElse (Java 9+) after a stream query to either process a found Product or log a fallback message.

products.stream()
    .filter(p -> p.getId().equals(productId))
    .findFirst()
    .ifPresentOrElse(
        p -> System.out.println("Found: " + p.getName()),
        () -> System.out.println("Product not found: " + productId));

128. Show the risky pattern of calling Optional.get() directly on a stream result, and the safer replacement.

// risky: throws NoSuchElementException if absent
Employee risky = employees.stream().findFirst().get();

// safer
Employee safe = employees.stream()
    .findFirst()
    .orElseThrow(() -> new IllegalStateException("No employees available"));

129. Use Optional.filter after a stream lookup to reject a found Employee that does not meet an additional condition.

Optional<Employee> qualifiedManager = employees.stream()
    .filter(e -> "MANAGER".equals(e.getRole()))
    .findFirst()
    .filter(e -> e.getYearsOfExperience() >= 5);

130. Given a list of nullable Strings, use Stream.ofNullable inside flatMap to safely filter out nulls while mapping to uppercase.

List<String> safeUppercase = maybeNullNames.stream()
    .flatMap(Stream::ofNullable)
    .map(String::toUpperCase)
    .collect(Collectors.toList());

131. Reduce a stream of Optional<Double> discount values to a single total, treating empty Optionals as zero.

double totalDiscount = discountOptionals.stream()
    .mapToDouble(opt -> opt.orElse(0.0))
    .sum();

Primitive Streams

132. Sum integers from 1 to 100 (inclusive) using IntStream.rangeClosed.

int sumOneToHundred = IntStream.rangeClosed(1, 100).sum();

133. Generate integers from 0 up to (but excluding) 10 using IntStream.range and print each.

IntStream.range(0, 10).forEach(System.out::println);

134. Convert a List<Integer> to an IntStream to avoid boxing overhead during a sum operation.

int total = integerList.stream()
    .mapToInt(Integer::intValue)
    .sum();

135. Box an IntStream back into a Stream<Integer> to collect into a List.

List<Integer> boxedList = IntStream.rangeClosed(1, 5)
    .boxed()
    .collect(Collectors.toList());

136. Use IntStream.of to create a stream from primitive int literals and find the maximum value.

OptionalInt max = IntStream.of(4, 9, 2, 15, 7).max();

137. Use DoubleStream to compute the average of an array of double prices.

double[] prices = { 19.99, 5.49, 42.00, 3.25 };
OptionalDouble avgPrice = DoubleStream.of(prices).average();

138. Use LongStream to sum a large range of values that would overflow an int, such as 1 to 10,000,000.

long total = LongStream.rangeClosed(1, 10_000_000L).sum();

139. Generate the first 10 Fibonacci numbers using an IntStream over indices.

int[] fib = new int[10];
fib[0] = 0;
fib[1] = 1;
IntStream.range(2, 10).forEach(i -> fib[i] = fib[i - 1] + fib[i - 2]);

List<Integer> fibonacci = Arrays.stream(fib).boxed().collect(Collectors.toList());

140. Compute full IntSummaryStatistics (min, max, sum, average, count) for a stream of Employee ages.

IntSummaryStatistics ageStats = employees.stream()
    .mapToInt(Employee::getAge)
    .summaryStatistics();

System.out.println("min=" + ageStats.getMin() + " max=" + ageStats.getMax() + " avg=" + ageStats.getAverage());

141. Convert an IntStream of character codes back into a String.

String word = IntStream.of(72, 101, 108, 108, 111)
    .mapToObj(c -> String.valueOf((char) c))
    .collect(Collectors.joining());

142. Sum only the even-indexed elements of an int array using IntStream over indices.

int[] values = { 10, 20, 30, 40, 50, 60 };
int sumOfEvenIndexes = IntStream.range(0, values.length)
    .filter(i -> i % 2 == 0)
    .map(i -> values[i])
    .sum();

143. Use IntStream to generate a multiplication table for a given number.

int number = 7;
IntStream.rangeClosed(1, 10)
    .mapToObj(i -> number + " x " + i + " = " + (number * i))
    .forEach(System.out::println);

144. Compute the average of only the passing scores in a primitive int[] array of test scores.

int[] scores = { 45, 78, 92, 60, 88, 33 };
OptionalDouble avgPassingScore = IntStream.of(scores)
    .filter(score -> score >= 50)
    .average();

145. Use IntStream.asLongStream and asDoubleStream to widen a primitive stream for a calculation that needs a wider type.

long totalAsLong = IntStream.rangeClosed(1, 1000)
    .asLongStream()
    .sum();

double totalAsDouble = IntStream.rangeClosed(1, 1000)
    .asDoubleStream()
    .sum();

146. Explain why IntStream is preferred over Stream<Integer> for numeric aggregation, and show the unboxed alternative.

// Stream<Integer> requires unboxing every element inside mapToInt
int boxedSum = numbers.stream()
    .mapToInt(Integer::intValue)
    .sum();

// IntStream.range never boxes at all while generating the values
int unboxedSum = IntStream.range(0, numbers.size())
    .map(numbers::get)
    .sum();

Parallel Streams & Performance

147. Sum all even numbers in a large list using a parallel stream.

int sumOfEvens = largeIntegerList.parallelStream()
    .filter(n -> n % 2 == 0)
    .mapToInt(Integer::intValue)
    .sum();

148. Switch a stream to parallel mid-pipeline with parallel(), then back to sequential() before a final ordered step.

List<String> result = names.stream()
    .parallel()
    .map(String::toUpperCase)
    .sequential()
    .sorted()
    .collect(Collectors.toList());

149. Explain why forEach on a parallel stream does not guarantee encounter order, and show forEachOrdered as the fix.

// order not guaranteed across threads
numbers.parallelStream().forEach(System.out::println);

// order preserved even in parallel
numbers.parallelStream().forEachOrdered(System.out::println);

150. Measure whether a parallel stream is actually faster than a sequential one for a CPU-bound sum.

long start = System.nanoTime();
long sequentialSum = LongStream.rangeClosed(1, 50_000_000L).sum();
long sequentialTime = System.nanoTime() - start;

start = System.nanoTime();
long parallelSum = LongStream.rangeClosed(1, 50_000_000L).parallel().sum();
long parallelTime = System.nanoTime() - start;
Trade-off Parallel streams add task-splitting and thread-coordination overhead. For small collections or I/O-bound work, a sequential stream is often faster.

151. Show why collecting into a shared ArrayList from a parallel stream's forEach is unsafe, and the thread-safe alternative.

// unsafe: ArrayList is not thread-safe under concurrent add() calls
List<String> unsafe = new ArrayList<>();
names.parallelStream().forEach(unsafe::add); // avoid this

// safe: let the Collector handle thread-safe merging
List<String> safe = names.parallelStream()
    .collect(Collectors.toList());

152. Run a parallel stream computation on a custom ForkJoinPool instead of the shared common pool.

ForkJoinPool customPool = new ForkJoinPool(4);
try {
    long total = customPool.submit(() ->
        largeList.parallelStream()
            .mapToLong(Product::getStock)
            .sum()
    ).get();
} finally {
    customPool.shutdown();
}

153. Explain why a parallel stream over a LinkedList splits poorly compared to an ArrayList, and pick the better source structure.

// LinkedList has poor splitting characteristics for parallel streams
List<Employee> linked = new LinkedList<>(employees);
linked.parallelStream().forEach(Employee::recalculateBonus); // splits inefficiently

// ArrayList (or an array) splits cheaply because it supports fast random access
List<Employee> arrayBacked = new ArrayList<>(employees);
arrayBacked.parallelStream().forEach(Employee::recalculateBonus);

154. Use a parallel stream together with Collectors.groupingByConcurrent for a large Transaction dataset.

Map<String, List<Transaction>> byType = transactions.parallelStream()
    .collect(Collectors.groupingByConcurrent(Transaction::getType));

155. Show why a non-atomic shared counter increment inside a parallel stream produces incorrect results, and the atomic fix.

// unsafe: int++ is not atomic, updates get lost under parallel execution
int[] unsafeCounter = { 0 };
items.parallelStream().forEach(item -> unsafeCounter[0]++); // wrong result

// safe: use an atomic accumulator
AtomicInteger safeCounter = new AtomicInteger();
items.parallelStream().forEach(item -> safeCounter.incrementAndGet());

156. Use parallel streams to compute lifetime value per Customer, combining results with a thread-safe Collector.

Map<String, Double> lifetimeValueByCustomer = customers.parallelStream()
    .collect(Collectors.toMap(
        Customer::getName,
        c -> c.getOrders().stream().mapToDouble(Order::getAmount).sum()));

157. Demonstrate that sorted() on a parallel stream still produces a correctly ordered result.

List<Integer> sortedInParallel = largeIntegerList.parallelStream()
    .sorted()
    .collect(Collectors.toList());

158. Compare parallelStream().count() versus stream().count() on an unfiltered source and explain the result.

long sequentialCount = employees.stream().count();
long parallelCount = employees.parallelStream().count();
// both return the same value instantly: count() on an unfiltered,
// sized source is answered directly from the collection's size,
// so parallelism adds only overhead with no benefit here

159. Use IntStream.range(...).parallel() to check primality across a large range of numbers efficiently.

long primeCount = IntStream.range(2, 2_000_000)
    .parallel()
    .filter(NumberUtils::isPrime)
    .count();

160. Explain the risk of using a parallel stream for I/O-bound work like calling a remote service per element, and the preferred alternative.

// risky: parallelStream over blocking I/O calls can exhaust the common ForkJoinPool
List<String> risky = customerIds.parallelStream()
    .map(this::callRemoteProfileServiceBlocking)
    .collect(Collectors.toList());

// preferred: CompletableFuture with a dedicated executor for I/O-bound work
ExecutorService ioExecutor = Executors.newFixedThreadPool(20);
List<CompletableFuture<String>> futures = customerIds.stream()
    .map(id -> CompletableFuture.supplyAsync(() -> callRemoteProfileServiceBlocking(id), ioExecutor))
    .collect(Collectors.toList());

List<String> profiles = futures.stream()
    .map(CompletableFuture::join)
    .collect(Collectors.toList());

161. Use Collectors.toConcurrentMap when grouping Product objects in parallel needs a deterministic, thread-safe Map type.

ConcurrentMap<String, Long> countByCategory = products.parallelStream()
    .collect(Collectors.toConcurrentMap(
        Product::getCategory,
        p -> 1L,
        Long::sum));

Real-World / Production Scenarios

162. Read a large log file line by line, skip blank and comment lines, and keep only the first 100 valid lines.

try (Stream<String> lines = Files.lines(Paths.get("app.log"))) {
    List<String> first100 = lines
        .filter(line -> !line.isBlank() && !line.startsWith("#"))
        .limit(100)
        .collect(Collectors.toList());
} catch (IOException e) {
    throw new UncheckedIOException("Failed to read log file", e);
}

163. Parse a CSV file of Employee records, skip the header row, and map each remaining line into an Employee object.

try (Stream<String> lines = Files.lines(Paths.get("employees.csv"))) {
    List<Employee> employees = lines
        .skip(1)
        .map(line -> line.split(","))
        .map(fields -> new Employee(fields[0], fields[1], Double.parseDouble(fields[2])))
        .collect(Collectors.toList());
} catch (IOException e) {
    throw new UncheckedIOException("Failed to read employees.csv", e);
}

164. Count how many ERROR-level entries appear in an application log file.

try (Stream<String> lines = Files.lines(Paths.get("app.log"))) {
    long errorCount = lines
        .filter(line -> line.contains("ERROR"))
        .count();
} catch (IOException e) {
    throw new UncheckedIOException("Failed to read app.log", e);
}

165. Build a report of error counts per log file across a directory of log files.

try (Stream<Path> logFiles = Files.list(Paths.get("logs"))) {
    Map<String, Long> errorCountByFile = logFiles
        .filter(p -> p.toString().endsWith(".log"))
        .collect(Collectors.toMap(
            p -> p.getFileName().toString(),
            p -> {
                try (Stream<String> lines = Files.lines(p)) {
                    return lines.filter(l -> l.contains("ERROR")).count();
                } catch (IOException e) {
                    throw new UncheckedIOException(e);
                }
            }));
} catch (IOException e) {
    throw new UncheckedIOException("Failed to list logs directory", e);
}

166. Shape a raw list of internal Order entities into a simplified API response DTO, exposing only the fields clients need.

record OrderSummaryDto(String id, String customerName, double amount, String status) {}

List<OrderSummaryDto> response = orders.stream()
    .map(o -> new OrderSummaryDto(o.getId(), o.getCustomerName(), o.getAmount(), o.getStatus()))
    .collect(Collectors.toList());

167. Build a daily sales report by aggregating a batch of Transaction records into total revenue per day.

Map<LocalDate, Double> revenueByDay = transactions.stream()
    .collect(Collectors.groupingBy(
        t -> t.getTimestamp().toLocalDate(),
        Collectors.summingDouble(Transaction::getAmount)));

168. Deduplicate a batch of incoming webhook events by event id, preserving the first occurrence of each id.

Set<String> seenEventIds = new HashSet<>();
List<WebhookEvent> deduplicated = incomingEvents.stream()
    .filter(event -> seenEventIds.add(event.getId()))
    .collect(Collectors.toList());

169. Validate a batch of imported Customer records, splitting them into valid and invalid groups.

Map<Boolean, List<Customer>> partitioned = importedCustomers.stream()
    .collect(Collectors.partitioningBy(c -> c.getEmail() != null && c.getEmail().contains("@")));

List<Customer> valid = partitioned.get(true);
List<Customer> rejected = partitioned.get(false);

170. Build a paginated response by combining skip/limit with a total count, a common pattern for REST list endpoints.

int page = 2;
int pageSize = 20;

List<Product> pageResults = allProducts.stream()
    .skip((long) page * pageSize)
    .limit(pageSize)
    .collect(Collectors.toList());

long totalCount = allProducts.stream().count();

171. Merge persisted Order records from a database with pending Order records from a message queue into one deduplicated, sorted feed.

List<Order> combinedFeed = Stream.concat(persistedOrders.stream(), pendingOrders.stream())
    .collect(Collectors.toMap(Order::getId, Function.identity(), (a, b) -> a))
    .values().stream()
    .sorted(Comparator.comparing(Order::getOrderDate).reversed())
    .collect(Collectors.toList());

172. Build a fraud-review summary of customers with 3 or more failed payment attempts.

Map<String, Long> repeatedFailuresByCustomer = paymentAttempts.stream()
    .filter(attempt -> !attempt.isSuccessful())
    .collect(Collectors.groupingBy(PaymentAttempt::getCustomerId, Collectors.counting()))
    .entrySet().stream()
    .filter(entry -> entry.getValue() >= 3)
    .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

173. Flatten nested Department -> Employee -> Project data into flat rows for a CSV export.

record ExportRow(String department, String employeeName, String projectName) {}

List<ExportRow> rows = departments.stream()
    .flatMap(d -> d.getEmployees().stream()
        .flatMap(e -> e.getProjects().stream()
            .map(p -> new ExportRow(d.getName(), e.getName(), p.getName()))))
    .collect(Collectors.toList());

174. Build an inventory reconciliation report comparing ordered quantity against actual warehouse stock per Product.

Map<String, Integer> orderedQuantityByProduct = orders.stream()
    .flatMap(o -> o.getLines().stream())
    .collect(Collectors.groupingBy(
        line -> line.getProduct().getId(),
        Collectors.summingInt(OrderLine::getQuantity)));

Map<String, Integer> discrepancies = warehouseStock.entrySet().stream()
    .filter(e -> !e.getValue().equals(orderedQuantityByProduct.getOrDefault(e.getKey(), 0)))
    .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

175. Build a batch job processor that partitions a batch of jobs into ones that succeeded and ones that need retry.

Map<Boolean, List<JobResult>> results = jobs.stream()
    .map(this::executeJob)
    .collect(Collectors.partitioningBy(JobResult::isSuccess));

List<JobResult> needsRetry = results.get(false);

176. Aggregate API response durations from RequestLog entries to estimate the median (p50) latency for a monitoring dashboard.

List<Long> sortedDurations = requestLogs.stream()
    .map(RequestLog::getDurationMillis)
    .sorted()
    .collect(Collectors.toList());

long p50 = sortedDurations.get(sortedDurations.size() / 2);

177. Read a properties-style configuration file line by line into a Map<String, String>, ignoring comments and blank lines.

try (Stream<String> lines = Files.lines(Paths.get("app.config"))) {
    Map<String, String> config = lines
        .filter(line -> !line.isBlank() && !line.startsWith("#"))
        .map(line -> line.split("=", 2))
        .collect(Collectors.toMap(parts -> parts[0].trim(), parts -> parts[1].trim()));
} catch (IOException e) {
    throw new UncheckedIOException("Failed to read app.config", e);
}

178. Build a customer churn candidate list: customers whose most recent order is older than 90 days.

List<String> churnCandidates = customers.stream()
    .filter(c -> c.getOrders().stream()
        .max(Comparator.comparing(Order::getOrderDate))
        .map(Order::getOrderDate)
        .map(date -> date.isBefore(LocalDate.now().minusDays(90)))
        .orElse(true))
    .map(Customer::getName)
    .collect(Collectors.toList());

179. Normalize a batch of raw address Strings scraped from a legacy system: trim whitespace, collapse repeated spaces, and title-case each word.

List<String> normalizedAddresses = rawAddresses.stream()
    .map(String::trim)
    .map(addr -> addr.replaceAll("\\s+", " "))
    .map(addr -> Arrays.stream(addr.split(" "))
        .map(word -> word.isEmpty() ? word :
            Character.toUpperCase(word.charAt(0)) + word.substring(1).toLowerCase())
        .collect(Collectors.joining(" ")))
    .collect(Collectors.toList());

180. Build a monthly active user report from login events, deduplicating by user id per month.

Map<YearMonth, Long> activeUsersByMonth = loginEvents.stream()
    .collect(Collectors.groupingBy(
        e -> YearMonth.from(e.getTimestamp()),
        Collectors.mapping(LoginEvent::getUserId, Collectors.toSet())))
    .entrySet().stream()
    .collect(Collectors.toMap(Map.Entry::getKey, e -> (long) e.getValue().size()));

181. Process an incoming batch of Order events from a message queue, filtering out already-processed ids for idempotency.

Set<String> alreadyProcessedIds = orderRepository.findExistingIds(
    incomingEvents.stream().map(OrderEvent::getOrderId).collect(Collectors.toSet()));

List<OrderEvent> newEvents = incomingEvents.stream()
    .filter(event -> !alreadyProcessedIds.contains(event.getOrderId()))
    .collect(Collectors.toList());

Common Pitfalls & Tricky Questions

182. Show why reusing a Stream object after a terminal operation throws IllegalStateException.

Stream<Integer> stream = numbers.stream().filter(n -> n > 0);
long positiveCount = stream.count();
long total = stream.count(); // IllegalStateException: stream has already been operated upon or closed
Fix Build a fresh stream from the source collection for each independent terminal operation instead of storing a Stream in a variable and reusing it.

183. Demonstrate that intermediate operations are lazy and never execute without a terminal operation.

Stream<String> lazy = names.stream()
    .filter(n -> {
        System.out.println("filtering " + n); // never printed
        return n.startsWith("A");
    });
// nothing happens yet -- no terminal operation has been invoked

184. Show why a stateful lambda that mutates external state inside map() breaks under a parallel stream.

// unsafe: shared mutable counter accessed from multiple threads
int[] counter = { 0 };
List<Integer> indexed = names.parallelStream()
    .map(n -> counter[0]++) // race condition, results are unpredictable
    .collect(Collectors.toList());
Rule Stream lambdas should be stateless and side-effect free. Use an index-based approach such as IntStream.range(0, list.size()) instead of mutating shared state.

185. Show why peek() should not be used to mutate or record elements as business logic, since its execution is not guaranteed for every element.

// misuse: relying on peek to perform business logic
List<String> risky = names.stream()
    .peek(n -> auditLog.add(n)) // may not run for every element under some pipeline optimizations
    .filter(n -> n.startsWith("A"))
    .collect(Collectors.toList());

186. Explain why findFirst() after sorted() on a parallel stream is deterministic, while forEach() is not.

Optional<Integer> deterministicFirst = numbers.parallelStream()
    .sorted()
    .findFirst(); // always the same smallest value, regardless of thread scheduling

numbers.parallelStream().forEach(System.out::println); // print order can vary between runs

187. Show why modifying the backing List while iterating a Stream created from it throws ConcurrentModificationException.

List<String> items = new ArrayList<>(List.of("a", "b", "c"));
items.stream().forEach(item -> {
    if (item.equals("b")) {
        items.remove(item); // throws ConcurrentModificationException
    }
});

188. Show that distinct() always uses equals()/hashCode(), not the Comparator passed to a preceding sorted() call.

// inconsistent-with-equals comparator: only compares by length, ignoring content
List<String> words = List.of("cat", "dog", "ox");

List<String> sortedThenDistinct = words.stream()
    .sorted(Comparator.comparingInt(String::length))
    .distinct() // still uses equals(), never the Comparator from sorted()
    .collect(Collectors.toList());
Common mix-up A Comparator only controls ordering. Uniqueness in distinct() is always decided by equals()/hashCode().

189. Show why summing many double amounts with Collectors.summingDouble can accumulate floating point rounding error, and the safer alternative for money.

// risky: double accumulates rounding error over many additions
double totalRisky = payments.stream()
    .collect(Collectors.summingDouble(Payment::getAmount));

// safer: use BigDecimal for monetary totals
BigDecimal totalSafe = payments.stream()
    .map(Payment::getAmountAsBigDecimal)
    .reduce(BigDecimal.ZERO, BigDecimal::add);

190. Show why Collectors.toMap throws IllegalStateException on duplicate keys unless a merge function is supplied.

// throws IllegalStateException if two employees share the same department
Map<String, Employee> oneManagerPerDept = employees.stream()
    .collect(Collectors.toMap(Employee::getDepartment, Function.identity()));

// safe: supply a merge function to decide what happens on a collision
Map<String, Employee> safeMap = employees.stream()
    .collect(Collectors.toMap(
        Employee::getDepartment,
        Function.identity(),
        (first, second) -> first));

191. Show why calling .parallel() then later .sequential() on the same stream only affects the whole pipeline's final execution mode, not each call individually.

List<Integer> result = numbers.stream()
    .parallel()
    .filter(n -> n > 0)
    .sequential() // the LAST mode-setting call wins for the whole pipeline
    .map(n -> n * 2)
    .collect(Collectors.toList()); // runs sequentially, not parallel

192. Explain why Collectors.groupingBy returns mutable ArrayList values by default, and how to force an immutable result.

Map<String, List<Employee>> mutableGroups = employees.stream()
    .collect(Collectors.groupingBy(Employee::getDepartment));
mutableGroups.get("Sales").add(newHire); // compiles and works, but is often unintended

Map<String, List<Employee>> immutableGroups = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.collectingAndThen(Collectors.toList(), List::copyOf)));

193. Show why an infinite stream built with Stream.generate must always be bounded with limit() before a terminal operation.

// hangs forever: no limit before the terminal operation
// Stream.generate(Math::random).forEach(System.out::println);

// correct: bound it first
Stream.generate(Math::random)
    .limit(5)
    .forEach(System.out::println);

194. Demonstrate that sorted() is a stateful intermediate operation that must buffer the entire stream before emitting anything, unlike filter() or map().

List<Integer> sorted = hugeNumberList.stream()
    .sorted() // must consume the entire source before producing the first element
    .limit(10)
    .collect(Collectors.toList());
Performance note For very large sources, sorted().limit(n) is far more expensive than a bounded priority-queue approach, since sorted() cannot short-circuit.

195. Show why using == instead of equals() inside a stream filter for boxed Integer comparisons can silently produce wrong results outside the Integer cache range.

List<Integer> values = List.of(200, 127, 50);
Integer target = 200;

// unreliable: relies on reference identity, breaks outside the cached -128..127 range
long wrongCount = values.stream().filter(v -> v == target).count();

// correct: use equals()
long correctCount = values.stream().filter(v -> v.equals(target)).count();

196. Show why a method that returns a Stream field cached across calls is a bug, and the fix of building a fresh stream every time.

class ReportBuilder {
    private final List<Order> orders;

    ReportBuilder(List<Order> orders) {
        this.orders = orders;
    }

    // correct: build a brand new stream on every call --
    // never store a Stream itself as a field, since it can only be consumed once
    Stream<Order> ordersStream() {
        return orders.stream();
    }
}

Method References & Functional Interfaces

197. Replace a lambda that only calls a getter with a method reference inside map().

// lambda form
List<String> names1 = employees.stream().map(e -> e.getName()).collect(Collectors.toList());

// method reference form
List<String> names2 = employees.stream().map(Employee::getName).collect(Collectors.toList());

198. Use a static method reference inside a stream's map() to parse Strings into Integers.

List<Integer> parsed = numericStrings.stream()
    .map(Integer::parseInt)
    .collect(Collectors.toList());

199. Use a bound instance method reference from an existing validator object as a Predicate inside filter().

OrderValidator validator = new OrderValidator();

List<Order> validOrders = orders.stream()
    .filter(validator::isValid) // bound instance method reference
    .collect(Collectors.toList());

200. Use an unbound instance method reference as a Comparator key extractor in sorted().

List<Product> sortedByName = products.stream()
    .sorted(Comparator.comparing(Product::getName)) // unbound instance method reference
    .collect(Collectors.toList());

201. Use a constructor reference inside map() to convert DTOs into domain objects.

List<Employee> employees = employeeDtos.stream()
    .map(Employee::new) // constructor reference, assuming an Employee(EmployeeDto) constructor
    .collect(Collectors.toList());

202. Use a constructor reference with Collectors.toCollection to specify the target collection type.

TreeSet<String> sortedNames = names.stream()
    .collect(Collectors.toCollection(TreeSet::new));

203. Use Function.identity() instead of a redundant lambda when building a Map with Collectors.toMap.

Map<String, Product> productsById = products.stream()
    .collect(Collectors.toMap(Product::getId, Function.identity()));

204. Compose two Function references together with andThen() inside a stream's map() call.

Function<String, String> trim = String::trim;
Function<String, String> upper = String::toUpperCase;

List<String> cleaned = rawInputs.stream()
    .map(trim.andThen(upper))
    .collect(Collectors.toList());

205. Combine two Predicate references with and()/or()/negate() to build a composite filter condition for a stream.

Predicate<Employee> isSenior = e -> e.getYearsOfExperience() > 5;
Predicate<Employee> isInSales = e -> "Sales".equals(e.getDepartment());

List<Employee> seniorSalesReps = employees.stream()
    .filter(isSenior.and(isInSales))
    .collect(Collectors.toList());

206. Use a custom functional interface, instead of a built-in java.util.function type, as the argument to a stream operation.

@FunctionalInterface
interface DiscountRule {
    double apply(Product product);
}

DiscountRule seasonalDiscount = product -> product.getPrice() * 0.9;

List<Double> discountedPrices = products.stream()
    .map(seasonalDiscount::apply)
    .collect(Collectors.toList());

207. Use a BiFunction reference as the merge function parameter of Collectors.toMap to resolve key collisions.

BiFunction<Order, Order, Order> keepHigherAmount =
    (a, b) -> a.getAmount() >= b.getAmount() ? a : b;

Map<String, Order> bestOrderPerCustomer = orders.stream()
    .collect(Collectors.toMap(
        Order::getCustomerName,
        Function.identity(),
        keepHigherAmount));

208. Use a Supplier reference as the factory argument to Collectors.toCollection to control both the collection type and its initial capacity.

Supplier<ArrayList<Product>> capacityAwareFactory = () -> new ArrayList<>(products.size());

List<Product> copied = products.stream()
    .collect(Collectors.toCollection(capacityAwareFactory));
No comments
Leave a Comment