العمليات النهائية والتقييم الكسول (laziness)
في هذا الدرس 11 موضوعات
هذا الدرس متاح حاليًا باللغة الإنجليزية فقط، ولم تُنشر نسخته العربية بعد.
Lesson Overview
Every conveyor belt needs a worker at the far end who packs the finished items, or counts them, or shouts "found one!". In a stream pipeline that worker is the terminal operation. It is the one step that produces a real result, and it is also the step that makes the belt run. Until a terminal operation is called, the stations you wrote do nothing at all. That property is called laziness.
This lesson covers the terminal operations you will use every day: collect and toList (gather), count, reduce (combine into one value), anyMatch, allMatch, noneMatch (yes/no questions), and findFirst (get one item). It also shows, with real printed output, when the work happens and why some pipelines stop early.
Why this matters: a pipeline that "does nothing" is a classic beginner puzzle, and knowing that work stops early explains both good performance and surprising log output.
This lesson builds on Lessons 1 and 2. It previews one idea from BEG-037: findFirst returns an Optional. You will only learn the minimum here, enough to use it safely.
Learning Objectives
By the end of this lesson you will be able to:
- Name the common terminal operations and say what each returns.
- Use
reduceto combine all items into one value. - Use
anyMatch,allMatch, andnoneMatch, including on an empty stream. - Use
findFirstand read itsOptionalresult withmapandorElse. - Explain laziness and predict the order of printed output in a pipeline.
- Explain short-circuiting.
Written Explanation
The terminal operations
| Operation | Returns | What it does |
|---|---|---|
toList() | List<T> (unmodifiable) | Gathers all items into a list |
collect(collector) | depends on the collector | Gathers items using a Collector (Lesson 5) |
count() | long | Counts the items |
reduce(identity, op) | T | Combines all items into one value |
reduce(op) | Optional<T> | Same, with no starting value |
anyMatch(p) | boolean | Is there at least one match? |
allMatch(p) | boolean | Do all items match? |
noneMatch(p) | boolean | Does no item match? |
findFirst() | Optional<T> | The first item, if any |
forEach(consumer) | void | Does something with each item |
A pipeline has exactly one terminal operation, at the end.
count and collect
count() returns a long, not an int:
long evens = nums.stream().filter(n -> n % 2 == 0).count();toList() is the shortest way to gather results, and since Java 16 it is built into Stream. collect is the general form. collect(Collectors.joining(", ")) joins strings into one String, and collect(Collectors.toSet()) gathers into a Set. Lesson 5 gives collect its full treatment. The Javadoc for Collectors.toList() says it makes no promise about the kind of list returned or whether it can be changed, so do not rely on modifying it either.
reduce: many items into one
reduce repeatedly combines two values until one remains. It takes a starting value (the identity) and a rule for combining two values:
int sum = nums.stream().reduce(0, Integer::sum);Think of it as this loop:
int result = 0; // the identity
for (int n : nums) { result = result + n; }The identity must be a value that changes nothing when combined: 0 for addition, 1 for multiplication. If the stream is empty, you get the identity back, so reduce(0, Integer::sum) on no numbers is 0. The combining rule must also be associative (grouping must not change the answer), which addition and maximum are, and the Javadoc requires this so a reduction could run in parallel.
Without an identity, reduce(Integer::max) returns an Optional, because an empty stream has no maximum.
Yes/no questions: anyMatch, allMatch, noneMatch
boolean hasNegative = nums.stream().anyMatch(n -> n < 0);
boolean allSmall = nums.stream().allMatch(n -> n < 100);
boolean noZero = nums.stream().noneMatch(n -> n == 0);The empty stream deserves attention, because the answers can surprise you:
| Operation on an empty stream | Result |
|---|---|
anyMatch | false (there is nothing to match) |
allMatch | true (nothing breaks the rule) |
noneMatch | true |
This follows the Javadoc. So allMatch on an empty list says true; check that the list is not empty if that matters to you.
findFirst and a first look at Optional
findFirst() returns the first item that reaches the end of the pipeline. But there may be none, so it cannot just return the item. It returns an Optional, a small box that holds either one value or nothing. (BEG-037 teaches Optional properly; for now you need only two moves.)
String name = people.stream()
.filter(p -> p.age() > 60)
.findFirst() // Optional<Person>
.map(Person::name) // Optional<String>
.orElse("none"); // String: the name, or "none" if the box is emptymap on an Optional changes the value inside, if there is one. orElse unwraps the box and gives a fallback when it is empty. Printing an Optional directly shows its box form, such as Optional[Person[name=Lina, age=67]] or Optional.empty. Avoid calling get() for now, because it fails on an empty box.
Laziness: nothing happens until the end
Intermediate operations only describe work. They do not do it. The Javadoc says intermediate operations such as filter and map are lazy, and traversal begins only when a terminal operation runs. A pipeline with no terminal operation does nothing. Also, laziness lets Java fuse the stations: instead of filtering the whole list and then mapping the whole list, it can push items through the stations one at a time.
You can see this by putting println in the lambdas. This is for learning only; remember that side effects in lambdas are discouraged in real code.
Short-circuiting: stopping early
Some operations can finish without seeing every item. They are called short-circuiting. anyMatch stops at the first match, allMatch and noneMatch stop at the first counterexample, findFirst stops at the first item, and limit(n) stops after n items. The Javadoc's own example is a search for the first string longer than a thousand characters: only as many elements as needed are examined. Short-circuiting is also what makes it possible to use an endless stream with limit.
forEach
forEach performs an action on each item and returns nothing. It is fine for printing results at the end of a pipeline: names.stream().sorted().forEach(System.out::println). It is not the right tool for building a result by adding to an outside list; use toList() or collect for that.
Examples
Example 1: sum, any, count (simple)
import java.util.*;
public class Terminals {
public static void main(String[] args) {
List<Integer> nums = List.of(4, -2, 7, 10, 3, 8);
int sum = nums.stream().reduce(0, Integer::sum);
boolean hasNeg = nums.stream().anyMatch(n -> n < 0);
long evens = nums.stream().filter(n -> n % 2 == 0).count();
System.out.println(sum + " " + hasNeg + " " + evens);
System.out.println(nums.stream().reduce(Integer::max));
System.out.println(List.<Integer>of().stream().reduce(0, Integer::sum)
+ " " + List.<Integer>of().stream().anyMatch(n -> true)
+ " " + List.<Integer>of().stream().allMatch(n -> false));
}
}Real output:
30 true 4
Optional[10]
0 false trueThe last line shows the empty-stream rules: the sum of nothing is the identity 0, anyMatch is false, allMatch is true.
Example 2: the first person older than 60 (practical)
import java.util.*;
public class FindFirstDemo {
record Person(String name, int age) { }
public static void main(String[] args) {
List<Person> ps = List.of(
new Person("Sam", 34), new Person("Lina", 67), new Person("Omar", 71));
System.out.println(ps.stream().filter(p -> p.age() > 60).findFirst());
System.out.println(ps.stream().filter(p -> p.age() > 90).findFirst());
String a = ps.stream().filter(p -> p.age() > 60)
.findFirst().map(Person::name).orElse("none");
String b = ps.stream().filter(p -> p.age() > 90)
.findFirst().map(Person::name).orElse("none");
System.out.println(a + " / " + b);
}
}Real output:
Optional[Person[name=Lina, age=67]]
Optional.empty
Lina / noneNote that findFirst returned Lina, not Omar: both are over 60, and findFirst stops at the first one.
Example 3: watching laziness and short-circuiting (realistic)
import java.util.*;
import java.util.stream.*;
public class LazyDemo {
public static void main(String[] args) {
List<Integer> nums = List.of(4, -2, 7, 10, 3, 8);
Stream<Integer> s = nums.stream()
.filter(n -> { System.out.println("check " + n); return n > 5; })
.map(n -> { System.out.println("double " + n); return n * 2; });
System.out.println("pipeline built");
List<Integer> out = s.toList();
System.out.println(out);
System.out.println("--- anyMatch");
boolean any = nums.stream()
.peek(n -> System.out.println("see " + n))
.anyMatch(n -> n > 5);
System.out.println(any);
}
}Real output:
pipeline built
check 4
check -2
check 7
double 7
check 10
double 10
check 3
check 8
double 8
[14, 20, 16]
--- anyMatch
see 4
see -2
see 7
trueThree lessons are in this output. First, pipeline built printed before any check: building the pipeline did no work. Second, items are processed one at a time through all stations (check 7 then double 7), not "all checks, then all doubles". Third, in the anyMatch pipeline the stream stopped after 7: the remaining items were never looked at. (peek is an intermediate operation that runs an action on each item as it passes; it exists mainly for debugging, and is used here only to show what is seen.)
Backend Insight
Laziness is why a well-written pipeline can be cheap even over a big list: anyMatch on "does this user have any overdue invoice?" stops at the first overdue one, and findFirst stops at the first match. In service code, anyMatch is also an excellent way to write a business rule as a sentence: "the order is blocked if any item is out of stock".
findFirst returning an Optional is the Java way to say "might not exist" without returning null. The database lookups you write later (findById) follow the same idea, and you will use map and orElse constantly. For a missing user, you will often prefer to throw a "not found" exception than to use orElse with a made-up value; BEG-037 covers when to use each.
Finally, laziness has a debugging consequence: an exception thrown inside a lambda appears when the terminal operation runs, not at the line where you wrote the map. When a stack trace points at toList(), look at the stations above it.
Common Mistakes
Mistake 1: No terminal operation
names.stream().filter(...).map(...); compiles, does nothing, and returns an unused stream. Add toList(), count(), or another terminal operation.
Mistake 2: Using get() on an empty Optional
Unwrapping with get() when the box might be empty throws NoSuchElementException. Use map and orElse until BEG-037 shows more options.
Mistake 3: Thinking allMatch on an empty list means "there are items and all match"
It returns true for an empty stream. If "at least one and all" is the rule, also check that the list is not empty.
Mistake 4: Wrong identity in reduce
- Incorrect:
reduce(0, (x, y) -> x * y)to compute a product, which is always0. - Correct:
reduce(1, (x, y) -> x * y).
Mistake 5: Expecting println in a lambda to run for every item
A pipeline can stop early, and the Javadoc notes that count() may skip running the pipeline when it can compute the count from the source. Do not use lambdas for required side effects.
Exercises
Try each exercise yourself before opening the solutions.
Level A: Foundation
A1. Name the terminal operation you would use to: (a) tell whether any word is empty, (b) add all numbers, (c) get the first word that starts with "z".
أظهر الحل A1
A1. (a) anyMatch(String::isEmpty). (b) reduce(0, Integer::sum). (c) filter(w -> w.startsWith("z")).findFirst().
A2. What does a pipeline return or do if it has no terminal operation?
أظهر الحل A2
A2. Nothing runs. You get a stream object back, and no work is done.
A3. What does List.<Integer>of().stream().noneMatch(n -> n > 0) return, and why?
أظهر الحل A3
A3. true, because there is no item that matches; "none match" is true for an empty stream.
Level B: Application
B1. Given List.of(3, 8, 5, 12, 7), write one expression for each: the sum, the product, and the number of items greater than 6.
أظهر الحل B1
B1.
List<Integer> nums = List.of(3, 8, 5, 12, 7);
int sum = nums.stream().reduce(0, Integer::sum); // 35
int product = nums.stream().reduce(1, (x, y) -> x * y); // 10080
long big = nums.stream().filter(n -> n > 6).count(); // 3B2. Given a list of words, write a pipeline that returns the first word longer than 6 letters, or the text "short" if there is none.
أظهر الحل B2
B2.
String result = words.stream()
.filter(w -> w.length() > 6)
.findFirst()
.orElse("short");B3. Predict the printed output (no running first), then run to check:
Stream.of("a", "bb", "ccc", "dddd")
.filter(s -> { System.out.println("f " + s); return s.length() > 1; })
.findFirst();أظهر الحل B3
B3. Output: f a, f bb. The element a fails the test, bb passes, and findFirst stops there, so ccc and dddd are never examined. (The result of findFirst was thrown away, but the work still ran because a terminal operation was present.)
Level C: Challenge
C1. Try to check whether a List<Integer> is sorted in increasing order using a stream, and explain why this is awkward. Then write the loop version instead and say which you prefer. (Hint: you need to compare neighbours.)
أظهر الحل C1
C1. Comparing neighbours needs two positions at once, which a plain element-by-element stream does not give, so streams are awkward here. The loop is clearer:
boolean sorted = true;
for (int i = 1; i < list.size(); i++) {
if (list.get(i - 1) > list.get(i)) { sorted = false; break; }
}I prefer the loop: it needs an index, a flag, and an early break, exactly the cases where loops win.
C2. A teammate adds .map(x -> { total += x; return x; }) and ends the pipeline with .count(), hoping to compute a total. Give two reasons it is unreliable, and show a correct way to get the total.
أظهر الحل C2
C2. First, total is an outside variable: lambdas cannot assign to a local variable, and a field would work only by relying on side effects. Second, count() may skip the pipeline when it can work out the count from the source, in which case the map never runs and total stays 0. A correct way: int total = nums.stream().reduce(0, Integer::sum);.
Mini Challenge
Take a list of at least six prices (double values) and write a small program that prints: the total (reduce), whether any price is above 100 (anyMatch), whether all prices are positive (allMatch), and the first price below 10, or -1 if there is none (findFirst, map not needed: use .orElse(-1.0)). Then add a peek that prints each price it sees before anyMatch, run it, and write one sentence about which prices were printed and why.
Check your work: each result matches a hand calculation, and the peek output stops at the first price over 100 (or shows all of them if none is over 100).
Summary & Next Lesson
Key takeaways
- A pipeline needs a terminal operation; without one, nothing runs. That is laziness.
count,reduce,anyMatch,allMatch,noneMatch,findFirst,toList, andcollectare the everyday terminal operations.reduceneeds an identity that changes nothing:0for sums,1for products.findFirstreturns anOptional; usemapandorElsefor now.- Short-circuiting operations stop early, so unneeded items are never processed.
Next lesson
The next lesson, "flatMap", handles nested data, such as an order that contains many items, by turning many small lists into one flat stream.
Self-check
- What happens if a pipeline has no terminal operation?
- What does
allMatchreturn on an empty stream? - What is the identity for a
reducethat multiplies? - Why did
check 3andcheck 8not appear in theanyMatchpipeline? - What does
findFirstreturn when nothing matches?
أظهر الإجابات
- Nothing runs and no result is produced.
true.1.anyMatchis short-circuiting; it stopped at the first match,7.- An empty
Optional(Optional.empty).
References
- java.util.stream package summary, Java SE 21 API. Supports: intermediate operations are lazy and traversal starts at the terminal operation, short-circuiting operations (
limit,findFirst), the reduction rules, and why side effects in lambdas are discouraged. - Stream, Java SE 21 API. Supports:
anyMatch,allMatch,noneMatchresults on an empty stream,count()may skip the pipeline,reduceidentity and associativity requirements, andfindFirstreturning anOptional. - Collectors, Java SE 21 API. Supports:
Collectors.toList()gives no guarantees about the list's type or mutability, andjoiningconcatenates in encounter order. - Outputs shown were produced by compiling and running the examples with JDK 21. The behavior of
Optional.mapandorElsewas verified by running, not from a fetched page;Optionalis taught properly in BEG-037.
التطبيق
طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.
تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.