JAVA / STREAMS, LAMBDAS AND OPTIONAL
Reduction, match checks and finding elements
Collapse a stream to a single result with reduce, test elements with anyMatch/allMatch/noneMatch, and pull out one element using findFirst or findAny.
What you will learn
- Fold a stream into one value with reduce and its three overloads
- Pick an identity where op(identity, x) == x, or use the Optional-returning reduce
- Short-circuit boolean questions with anyMatch, allMatch and noneMatch
- Search with filter plus findFirst and unwrap the Optional without get()
Understanding Reduction, match checks and finding elements
A reduction folds a stream into one value by applying a two-argument operator over and over: take something, combine it with the next element, repeat. reduce has three shapes, and the shape you choose is a statement about empty streams and about parallelism. reduce(op) cannot invent an answer for an empty stream, so it returns an Optional; reduce(identity, op) always has identity to fall back on, which is why the operator must satisfy op(identity, x) == x for every x, otherwise the identity leaks into the result as if it were real data. The three-argument form adds a combiner because a parallel run folds chunks separately and then merges them, which is also where the associativity requirement comes from: (a op b) op c must equal a op (b op c), or the answer depends on how the work happened to be split.
anyMatch, allMatch and noneMatch are reductions down to a boolean, and they are permitted to stop early because one element can settle the question. anyMatch quits the moment the predicate is true, allMatch quits the moment it is false, and noneMatch quits on the first true; on an infinite or expensive source that is the difference between an answer and a program that never finishes. Their behaviour on an empty stream follows from logic rather than from a special case: no element satisfies the predicate so anyMatch is false, and there is no counterexample so allMatch and noneMatch are both true, with the predicate never invoked at all.
findFirst and findAny end a pipeline with at most one element, so both hand back an Optional instead of the element itself. findFirst is tied to encounter order: on an ordered stream it must return the element a plain loop with a break would have found, which costs some coordination when the stream is parallel. findAny drops that constraint and may return whichever element a thread has ready first, so it is cheaper but only meaningful when any hit will do. In both cases the Optional is the point: it forces you to write down what happens when nothing matches, which is exactly the branch a hand-written search loop tends to forget.
import java.util.List;
import java.util.Optional;
public class ReduceMatchFind {
public static void main(String[] args) {
List<String> words = List.of("kite", "arc", "bramble", "de");
int totalLength = words.stream()
.reduce(0, (acc, w) -> acc + w.length(), Integer::sum);
System.out.println("totalLength = " + totalLength);
Optional<String> longest = words.stream()
.reduce((a, b) -> b.length() > a.length() ? b : a);
System.out.println("longest = " + longest.orElse("none"));
Optional<String> huge = words.stream()
.filter(w -> w.length() > 20)
.reduce((a, b) -> b.length() > a.length() ? b : a);
System.out.println("huge present = " + huge.isPresent());
System.out.println("anyMatch(startsWith b) = " + words.stream().anyMatch(w -> w.startsWith("b")));
System.out.println("allMatch(length >= 2) = " + words.stream().allMatch(w -> w.length() >= 2));
System.out.println("noneMatch(isEmpty) = " + words.stream().noneMatch(String::isEmpty));
System.out.println("allMatch on empty stream = " + words.stream().filter(w -> w.length() > 20).allMatch(w -> w.isEmpty()));
System.out.println("findFirst(length == 3) = " + words.stream()
.filter(w -> w.length() == 3)
.findFirst()
.orElse("none"));
}
}reduce, the match checks and the find methods are all terminal folds, and each one's return type tells you what happens when the stream is empty or when the answer is already settled.
Worked examples
anyMatch stops the source
Shows that a match check consumes only as many elements as it needs, even from an infinite stream.
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;
public class ShortCircuit {
public static void main(String[] args) {
AtomicInteger tested = new AtomicInteger();
boolean found = Stream.iterate(1, n -> n + 1)
.anyMatch(n -> {
tested.incrementAndGet();
return n % 17 == 0 && n % 5 == 0;
});
System.out.println("found = " + found);
System.out.println("elements tested = " + tested.get());
}
}Example explained
Line 1Stream.iterate(1, n -> n + 1) is an unbounded source, so the pipeline can never be driven to completion.
Line 2anyMatch pulls one element at a time and returns true on the first satisfied predicate, so the source is simply abandoned.
Line 3The counter shows exactly 85 evaluations, because 85 is the smallest positive number divisible by both 17 and 5.
Line 4Replacing anyMatch with count() or a collect here would hang, since those terminal operations need every element.
Empty results from find and reduce
Contrasts findFirst keeping encounter order in parallel with the empty Optional that reduce(op) returns.
import java.util.List;
import java.util.Optional;
public class FindAndEmptyReduce {
public static void main(String[] args) {
List<Integer> nums = List.of(9, 4, 7, 4, 12, 3);
Optional<Integer> firstEven = nums.parallelStream()
.filter(n -> n % 2 == 0)
.findFirst();
System.out.println("firstEven = " + firstEven.orElse(-1));
Optional<Integer> product = nums.stream()
.filter(n -> n > 100)
.reduce((a, b) -> a * b);
System.out.println("product of >100 = " + product);
System.out.println("fallback = " + product.orElse(1));
}
}Example explained
Line 1findFirst on a parallel but ordered stream still yields 4, the first even number in encounter order, not whichever thread finished first.
Line 2The filter n > 100 removes every element, so reduce((a, b) -> a * b) has nothing to start from and prints Optional.empty.
Line 3orElse(1) is where you decide that an empty product means 1; reduce refuses to make that decision for you.
Line 4Using firstEven.orElse(-1) keeps the missing case explicit instead of risking an exception from get().
A bad identity is silently wrong
Demonstrates how a non-identity seed corrupts a max reduction on negative data.
import java.util.Optional;
import java.util.stream.Stream;
public class IdentityMatters {
public static void main(String[] args) {
int wrong = Stream.of(-5, -9, -3).reduce(0, Integer::max);
Optional<Integer> right = Stream.of(-5, -9, -3).reduce(Integer::max);
int safe = Stream.of(-5, -9, -3).reduce(Integer.MIN_VALUE, Integer::max);
System.out.println("identity 0 = " + wrong);
System.out.println("no identity = " + right);
System.out.println("identity MIN_VALUE = " + safe);
}
}Example explained
Line 10 is not an identity for max, because max(0, -5) is 0 rather than -5, so the seed wins over the real data.
Line 2reduce(Integer::max) has no seed to distort the fold and reports Optional[-3], the true maximum.
Line 3Integer.MIN_VALUE is a genuine identity for max over int values, so the seeded form becomes correct again.
Line 4Nothing throws in the first case: a wrong identity produces a plausible-looking number, which is why it survives testing.
Important notes
The accumulator handed to reduce must be stateless and side-effect free; if you find yourself adding to a list inside it, the operation you want is collect.
findAny may legally differ from findFirst, but on a sequential stream it usually returns the same element, so an order-dependent bug can stay hidden until the pipeline is parallelized.
Common mistakes
Seeding a max or min reduction with 0: on all-negative data reduce returns 0, a value that was never in the stream, and no exception is raised.
Calling get() on the Optional from findFirst or reduce(op): the first empty result throws NoSuchElementException at runtime instead of being handled.
Expecting allMatch to be false when nothing matched: an empty stream makes it true without ever calling the predicate, so an 'every record is valid' check passes with zero records.
Try it yourself
Change, predict, then run
Take List.of(-8, -3, -15, -1) and print its maximum twice, once with reduce(0, Integer::max) and once with reduce(Integer::max). Then print allMatch(n -> n < 0) on the whole list and on the same list filtered to n > 0, and explain the second boolean.
Open the Java workspaceCheck your understanding
A pipeline filters a list down to zero elements. Which pair of results is correct for that empty stream?
- allMatch returns true and reduce(BinaryOperator) returns an empty Optional
- allMatch returns false and reduce(BinaryOperator) returns an empty Optional
- allMatch returns true and reduce(BinaryOperator) throws NoSuchElementException
- allMatch throws IllegalStateException and reduce(BinaryOperator) returns an Optional holding the identity
Show answer
On an empty stream allMatch is vacuously true: there is no element that violates the predicate, so the predicate is never called. The one-argument reduce has no element to return and no identity to fall back on, so it yields Optional.empty rather than throwing; the NoSuchElementException in option three only happens if you go on to call get(). Option two is tempting because nothing matched, but 'nothing matched, so false' describes anyMatch, not allMatch.