Streams, lambdas and Optional
Read and write filter, map, collect and groupingBy pipelines, use Optional without get(), and avoid reusing a consumed stream.
Streams are how modern Java code filters and summarises collections, and screenings use them to check that you can read a pipeline and predict its result. The good news is that a small set of operations covers most real work.
Lambdas and method references
A lambda is a short function passed as an argument: o -> o.amount() > 1000. A method reference such as Order::city is shorthand for o -> o.city(). Both work because the parameter type is a functional interface, an interface with one abstract method, such as Predicate, Function or Runnable.
A pipeline, step by step
A stream has a source, zero or more intermediate operations (filter, map, sorted, distinct) and one terminal operation (collect, sum, forEach, findFirst). Nothing runs until the terminal operation is called.
record Order(String city, double amount) {}
List<Order> orders = List.of(
new Order("Pune", 1200),
new Order("Delhi", 800),
new Order("Pune", 300));
double total = orders.stream()
.filter(o -> o.city().equals("Pune"))
.mapToDouble(Order::amount)
.sum();
System.out.println(total); // 1500.0
List<String> cities = orders.stream()
.map(Order::city)
.distinct()
.sorted()
.collect(Collectors.toList());
System.out.println(cities); // [Delhi, Pune]
Map<String, Double> byCity = orders.stream()
.collect(Collectors.groupingBy(Order::city,
Collectors.summingDouble(Order::amount)));
System.out.println(byCity.get("Pune")); // 1500.0mapToDouble and IntStream exist because primitive streams have sum() and average(); a plain Stream<Double> does not. IntStream.rangeClosed(1, 5).sum() returns 15.
Optional done properly
findFirst, max and min return an Optional because the stream might be empty. Calling get() on an empty one throws, so prefer orElse, map or ifPresent.
Optional<Order> none = orders.stream()
.filter(o -> o.amount() > 5000)
.findFirst();
System.out.println(none.map(Order::city).orElse("none")); // none
Comparator<Order> byAmount = Comparator.comparingDouble(Order::amount);
System.out.println(orders.stream().max(byAmount).get().amount()); // 1200.0The second get() is safe only because the list is known to be non-empty; in production code use orElseThrow with a clear message.
Common mistakes and a practice habit
- Using a stream twice. After a terminal operation it is closed, and the next call throws
IllegalStateException. Create a fresh stream from the collection. - Expecting
filterormapto run without a terminal operation. They are lazy. - Modifying an outside variable inside a lambda. Captured locals must be effectively final; collect results with
collectinstead. - Comparing strings with
==insidefilter. Useequals.
Practice habit: Take any loop you wrote this week that builds a list or a total and rewrite it as a stream, then rewrite the stream back as a loop. Doing both directions teaches you when a stream is clearer and when a plain loop is, and that judgement is what reviewers look for.
More in Programming