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.
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.
| Approach | Use when | Watch out for |
|---|---|---|
| Sequential stream | Default choice for small-to-medium collections and most business logic. | No parallel speedup, but usually fine and easier to reason about. |
| Parallel stream | Large, 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 / DoubleStream | Numeric 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); // 04. 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 closed11. 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 115. 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());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())));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();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;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 here159. 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 closed183. 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 invoked184. 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());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 runs187. 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());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 parallel192. 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());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));
Post a Comment
Add