العمليات الوسيطة
في هذا الدرس 11 موضوعات
هذا الدرس متاح حاليًا باللغة الإنجليزية فقط، ولم تُنشر نسخته العربية بعد.
Lesson Overview
Lesson 1 showed the shape of a pipeline. This lesson fills the middle of it. Intermediate operations are the stations on the conveyor belt: each one takes the stream arriving from the left, does one small job, and hands a new stream to the right. Because each returns a stream, you can chain as many as you like.
Six stations cover most real work: filter (keep matching items), map (change each item), sorted (order them), distinct (drop duplicates), and limit and skip (take or skip items from the front). Order matters: swapping two stations can change the answer, and sometimes the cost of getting it.
Why this matters: nearly every list you return from a backend method is "the right items, changed into the right shape, in the right order, maybe only the first page". That sentence maps directly onto these six operations.
This lesson builds on Lesson 1 and on Comparator from BEG-032. Records appear in the examples as a short way to declare a value class; BEG-026 previewed them, and p.price() is simply the record's getter.
Learning Objectives
By the end of this lesson you will be able to:
- Use
filter,map,sorted,distinct,limit, andskipcorrectly. - Say which functional interface each operation expects.
- Sort a stream with natural order and with a
Comparator. - Predict how the order of stations changes the result.
- Explain why
mapis not a place to change outside variables. - Read a four- or five-step pipeline aloud, one station per sentence.
Written Explanation
The six stations
| Operation | Expects | Effect | Changes the element type? |
|---|---|---|---|
filter(predicate) | Predicate<T> | Keep items for which the test is true | No |
map(function) | Function<T, R> | Replace each item with a computed one | Yes, if the function returns another type |
sorted() | nothing | Order by natural order (Comparable) | No |
sorted(comparator) | Comparator<T> | Order using your rule | No |
distinct() | nothing | Remove duplicates, judged by equals | No |
limit(n) | a number | Keep at most the first n items | No |
skip(n) | a number | Drop the first n items | No |
Each one returns a new Stream. Nothing is computed until a terminal operation runs (Lesson 3).
filter and map
filter asks a yes/no question about each item. map computes a replacement:
ps.stream()
.filter(p -> p.category().equals("Furniture")) // keep Product items
.map(Product::name) // Product becomes String
.toList();After map(Product::name) the belt carries Strings instead of Products. The type of the stream changes, and the compiler tracks that for you: Stream<Product> becomes Stream<String>. This is why generics (BEG-031) appear in every stream signature.
sorted
With no argument, sorted() uses the elements' natural order, so the elements must be Comparable (numbers and strings are). Strings sort alphabetically by code unit, so upper-case letters come before lower-case in plain sorted(). If the elements are not Comparable, the pipeline fails at the terminal operation with a ClassCastException, according to the Javadoc.
With a Comparator, you use exactly the tools from BEG-032:
.sorted(Comparator.comparingDouble(Product::price))
.sorted(Comparator.comparing(Product::category)
.thenComparing(Product::price, Comparator.reverseOrder()))For ordered streams (a list is ordered), the sort is stable: items that compare equal keep their original relative order. That is the Javadoc's guarantee, and it is what lets you rely on multi-key sorts behaving predictably.
distinct
distinct() removes duplicates using equals. For a stream from a list, it keeps the first occurrence of each value. For your own classes it only works well if equals and hashCode are properly written (BEG-026), which records give you for free.
limit and skip
limit(n) keeps at most n items. skip(n) drops the first n. Together they make a page: skip(20).limit(10) is "page 3 with ten items per page". If there are fewer items than skip asks for, you get an empty stream, not an error. A negative limit throws IllegalArgumentException.
The order of stations matters
Two pipelines with the same stations in a different order can answer different questions. Take 5, 3, 9, 3, 7, 5, 1 and look at three results:
| Pipeline | Question it answers | Result |
|---|---|---|
distinct, sorted(reverseOrder), limit(3) | "the three largest different numbers" | [9, 7, 5] |
limit(3), sorted() | "sort just the first three numbers" | [3, 5, 9] |
sorted(), limit(3) | "the three smallest numbers" | [1, 3, 3] |
There is also a cost reason to filter early. If you filter before an expensive map, the expensive work runs on fewer items. A good habit: shrink first, change second, sort last, unless the question needs a different order.
Do not put side effects in map
map is for computing a replacement item. It is not a place to add to an outside list, increase a counter, or print progress. The Javadoc says side effects in behavioral parameters are generally discouraged, and gives no guarantees about when or whether those effects happen, because the implementation is free to skip or reorder work it can prove unnecessary. A pipeline whose correctness depends on a side effect is fragile. Lesson 8 shows one such pipeline and its repair.
Reading a pipeline
A long chain is readable only if you can read it as a sequence of plain sentences:
List<String> result = ps.stream()
.filter(p -> p.price() > 5) // only items over 5
.sorted(Comparator.comparingDouble(Product::price)) // cheapest first
.map(Product::name) // just the names
.limit(2) // the first two
.toList();Put each station on its own line, and add a short end-of-line comment if a lambda is not obvious.
Examples
All three examples use this small record. Put it next to your main method or in its own file.
record Product(String name, String category, double price) { }Example 1: the three largest different numbers (simple)
import java.util.*;
public class TopThree {
public static void main(String[] args) {
List<Integer> nums = List.of(5, 3, 9, 3, 7, 5, 1);
List<Integer> top3 = nums.stream()
.distinct()
.sorted(Comparator.reverseOrder())
.limit(3)
.toList();
System.out.println(top3);
}
}Real output:
[9, 7, 5]Read it as: drop duplicates, order from largest, take three.
Example 2: furniture, cheapest first (practical)
import java.util.*;
public class Catalog {
record Product(String name, String category, double price) { }
public static void main(String[] args) {
List<Product> ps = List.of(
new Product("Pen", "Office", 1.5),
new Product("Desk", "Furniture", 120.0),
new Product("Lamp", "Furniture", 30.0),
new Product("Stapler", "Office", 8.0),
new Product("Chair", "Furniture", 85.5),
new Product("Paper", "Office", 4.0));
List<String> furniture = ps.stream()
.filter(p -> p.category().equals("Furniture"))
.sorted(Comparator.comparingDouble(Product::price))
.map(Product::name)
.toList();
System.out.println(furniture);
List<String> byCategory = ps.stream()
.sorted(Comparator.comparing(Product::category)
.thenComparing(Product::price, Comparator.reverseOrder()))
.map(Product::name)
.toList();
System.out.println(byCategory);
}
}Real output:
[Lamp, Chair, Desk]
[Desk, Chair, Lamp, Stapler, Paper, Pen]The second pipeline sorts by category, then within each category by price from highest to lowest. It uses the multi-key idea from BEG-032 inside a stream.
Example 3: a side effect inside map (realistic)
A developer wants the lengths of the product names and also a log of which names were seen. They write:
List<String> seen = new ArrayList<>();
List<Integer> lens = ps.stream()
.map(p -> { seen.add(p.name()); return p.name().length(); })
.toList();
System.out.println(lens + " " + seen);Real output with a plain sequential stream:
[3, 4, 4, 7, 5, 5] [Pen, Desk, Lamp, Stapler, Chair, Paper]It happens to work today. But map's job is to compute lengths; the hidden seen.add is a second job whose timing is not guaranteed, and the code would be unsafe if someone ever made the stream parallel, because ArrayList is not thread-safe. The cleaner version computes the two results in two separate, honest steps:
List<String> seen = ps.stream().map(Product::name).toList();
List<Integer> lens = ps.stream().map(p -> p.name().length()).toList();Two short pipelines, no hidden state.
Backend Insight
Page and sort parameters on an HTTP endpoint (?sort=price&page=3&size=10) correspond almost one-to-one to sorted, skip, and limit. For small in-memory lists you could implement a page with exactly these three calls. For large data, a real application asks the database to do it with ORDER BY, OFFSET, and LIMIT, because pulling a million rows into memory just to skip most of them wastes time and memory. Knowing both forms helps you explain the same idea in two places.
Two more habits matter on a server. First, filter early, so later stations (and any expensive map, such as one that formats text or calls a helper method) see fewer items. Second, return a new list built by the pipeline rather than changing the caller's list: callers rarely expect a "get products" method to rearrange their data.
Common Mistakes
Mistake 1: Sorting by a field without a Comparator
- Incorrect:
ps.stream().sorted()whenProductis notComparable. - Correct:
ps.stream().sorted(Comparator.comparingDouble(Product::price)).
The failure appears at runtime, when the terminal operation runs, not at compile time.
Mistake 2: Putting limit before sorted and expecting the "top N"
limit(3).sorted() sorts only the first three items it received. For "the three largest" you must sort first, then limit.
Mistake 3: Using map to filter
- Incorrect:
map(p -> p.price() > 5)expecting to keep only the expensive items. It produces a stream oftrueandfalse. - Correct:
filter(p -> p.price() > 5).
Mistake 4: Expecting distinct to work on a class without equals
If your class does not override equals and hashCode, two objects with equal fields count as different. A record, or a class with a correct equals, avoids this.
Mistake 5: Side effects inside map
Adding to an outside list, counting, or printing from map makes the pipeline depend on timing. Compute results with the terminal operation instead, or use a loop.
Exercises
Try each exercise yourself before opening the solutions.
Level A: Foundation
A1. Which operation do you use to (a) keep only even numbers, (b) turn each word into its length, (c) remove repeated words?
أظهر الحل A1
A1. (a) filter. (b) map. (c) distinct.
A2. Given List.of(4, 8, 15, 16, 23, 42), write a pipeline that returns the numbers greater than 10, as a list.
أظهر الحل A2
A2.
List<Integer> big = List.of(4, 8, 15, 16, 23, 42).stream()
.filter(n -> n > 10)
.toList(); // [15, 16, 23, 42]A3. Write a pipeline over List.of("kiwi", "fig", "apple", "plum", "date") that returns the sorted list of words (natural order).
أظهر الحل A3
A3.
List<String> sorted = words.stream().sorted().toList();
// [apple, date, fig, kiwi, plum]Level B: Application
B1. With the same five fruit words, return the distinct word lengths in increasing order.
أظهر الحل B1
B1.
List<Integer> lengths = words.stream()
.map(String::length)
.distinct()
.sorted()
.toList(); // [3, 4, 5]B2. With the Product list from Example 2, return the names of the two most expensive products, most expensive first.
أظهر الحل B2
B2.
List<String> top2 = ps.stream()
.sorted(Comparator.comparingDouble(Product::price).reversed())
.limit(2)
.map(Product::name)
.toList(); // [Desk, Chair]B3. With the same list, return the upper-case names of the Office products, in their original order.
أظهر الحل B3
B3.
List<String> office = ps.stream()
.filter(p -> p.category().equals("Office"))
.map(p -> p.name().toUpperCase())
.toList(); // [PEN, STAPLER, PAPER]Level C: Challenge
C1. With the Product list, return the names of the products ranked second and third by price from the top (skip the most expensive, take the next two).
أظهر الحل C1
C1.
List<String> next = ps.stream()
.sorted(Comparator.comparingDouble(Product::price).reversed())
.skip(1)
.limit(2)
.map(Product::name)
.toList(); // [Chair, Lamp]C2. Explain in two sentences why sorted().limit(3) and limit(3).sorted() give different answers on List.of(5, 3, 9, 3, 7, 5, 1), and say what each returns.
أظهر الحل C2
C2. sorted().limit(3) first orders the whole stream, so it returns the three smallest values: [1, 3, 3]. limit(3).sorted() first keeps only the first three received, 5, 3, 9, and then sorts those, so it returns [3, 5, 9].
Mini Challenge
Make a list of at least eight items of your own, for example books with a title, an author, and a page count (a record is fine). Write one pipeline that returns "the titles of the three longest books that have more than 100 pages, longest first". Add a short end-of-line comment on every line. Then change the order of two stations on purpose and write one sentence about what changed.
Check your work: the pipeline ends with a terminal operation, the result is a list of titles (not books), and the longest is first.
Summary & Next Lesson
Key takeaways
- Intermediate operations return a new stream, so they chain.
filterkeeps items,mapchanges them,sortedorders them,distinctremoves duplicates,limitandskiptake or drop items from the front.- The order of stations changes the answer: sort before you limit when you want "top N".
- Shrink the data early and sort late.
- Keep
mapfree of side effects.
Next lesson
The next lesson, "Terminal operations and laziness", covers the station that ends every pipeline: collect, reduce, count, anyMatch, and findFirst, and why nothing runs until one of them is called.
Self-check
- Which operation changes the element type of a stream?
- What does
skip(20).limit(10)give you? - What exception can
sorted()cause on non-Comparableelements, and when does it appear? - Why use
filterbefore an expensivemap? - Why is adding to an outside list inside
mapa bad idea?
أظهر الإجابات
map(when its function returns another type).- Items 21 to 30: the third page of ten items.
- A
ClassCastException, at runtime when the terminal operation runs. - The expensive step then runs on fewer items.
- It adds a hidden second job whose timing is not guaranteed, and is unsafe if the stream runs in parallel.
References
- Stream, Java SE 21 API. Supports:
sortedis stable for ordered streams and may throwClassCastExceptionfor non-Comparableelements,distinctusesequals,limitthrowsIllegalArgumentExceptionfor a negative size. - java.util.stream package summary, Java SE 21 API. Supports: intermediate operations are lazy, side effects in behavioral parameters are discouraged with no guarantees, and stateful lambdas can make results unreliable.
- All outputs shown were produced by compiling and running the examples with JDK 21. The statement that plain
sorted()on strings puts upper-case before lower-case is standardStringnatural ordering and was not checked against a fetched page here.
التطبيق
طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.
تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.