عرض عملي: استبدال الفئات المجهولة بتعبيرات lambda
في هذا الدرس 11 موضوعات
هذا الدرس متاح حاليًا باللغة الإنجليزية فقط، ولم تُنشر نسخته العربية بعد.
Lesson Overview
This is a worked demo. You will take code written the long, noisy way and shrink it in small, safe steps, using everything from this module: lambdas, the standard functional interfaces, and method references. You will also hit, on purpose, the most common compiler error with lambdas, and see two ways out of it.
In the recipe-card picture, this lesson is a cleaning session: we start with cards cluttered with forms and stamps, and we trim each one down to the line that matters, checking after every step that the dish still tastes the same.
Why this matters: real codebases contain both styles. Older code (and some tutorials) use anonymous classes and copy-pasted loops, and new code uses lambdas. Refactoring from one to the other without changing behavior is a daily skill, and it is exactly what the lab "Filters" (BEG-035-LAB1) asks of you.
This lesson builds on all four earlier lessons in the module, and on Comparator from BEG-032.
Learning Objectives
By the end of this lesson you will be able to:
- Convert an anonymous
Comparatorinto a lambda and then into a comparator built from a method reference. - Replace a hard-coded filter loop with one method that takes a
Predicate. - Check that a refactoring kept the behavior the same.
- Recognize and explain the "effectively final" error in a realistic counting task.
- Fix that error by restructuring, and explain why a mutable holder such as
AtomicIntegeris a last resort. - Compare the before and after code in lines and clarity.
Written Explanation
The starting point
We have a small Contact class with a name and an age:
static class Contact {
private final String name;
private final int age;
Contact(String name, int age) { this.name = name; this.age = age; }
String getName() { return name; }
int getAge() { return age; }
@Override public String toString() { return name + "(" + age + ")"; }
}and a list we will reuse:
List<Contact> contacts = new ArrayList<>(List.of(new Contact("Sara", 12),
new Contact("Omar", 34), new Contact("Amal", 17), new Contact("Adam", 52)));The list is wrapped in new ArrayList<>(...) because List.of is unmodifiable and we want to sort it in place (BEG-030).
Step 0: the rule of refactoring
Before changing anything, record what the code does now (the output). After each step run it again and compare. If the output changes, the last step was not a refactoring. This is the habit that makes shrinking code safe.
Step 1: the anonymous Comparator
Collections.sort(contacts, new Comparator<Contact>() {
@Override
public int compare(Contact a, Contact b) {
return a.getName().compareTo(b.getName());
}
});
System.out.println(contacts);Output:
[Adam(52), Amal(17), Omar(34), Sara(12)]Seven lines for one idea: "compare by name".
Step 2: the lambda
Collections.sort expects a Comparator<Contact>, a functional interface, so the entire anonymous class collapses to its one method body. We use list.sort, the instance method you know from BEG-032:
contacts.sort((a, b) -> a.getName().compareTo(b.getName()));That is one line. To prove the lambda really does the job, here is the same call written so the order is reversed (b before a):
contacts.sort((a, b) -> b.getName().compareTo(a.getName()));Output after this reversed version:
[Sara(12), Omar(34), Amal(17), Adam(52)]Step 3: the method reference and the builder
The lambda still repeats the idea "get the name" twice. Comparator.comparing takes a key extractor, a Function, so we can pass a method reference:
contacts.sort(Comparator.comparing(Contact::getName));[Adam(52), Amal(17), Omar(34), Sara(12)]Same result as step 1, now in a line that reads "sort by contact name". Here is the whole evolution side by side:
| Version | Lines | What the reader must parse |
|---|---|---|
| Anonymous class | 7 | class, annotation, method signature, parameter types, body |
| Lambda | 1 | two parameters and one comparison |
Comparator.comparing + reference | 1 | the property being sorted |
Each step removed noise and kept the output identical. For a numeric key such as age, the builder Comparator.comparingInt(Contact::getAge) avoids boxing.
Step 4: from a hard-coded loop to a Predicate
Now the filtering code. The "before" version has the condition fixed inside the loop:
static List<Contact> adultsLoop(List<Contact> in) {
List<Contact> out = new ArrayList<>();
for (Contact c : in) {
if (c.getAge() >= 18) {
out.add(c);
}
}
return out;
}If you also need "names starting with A", you would copy it. Instead, make the condition a parameter:
static List<Contact> keep(List<Contact> in, Predicate<Contact> p) {
List<Contact> out = new ArrayList<>();
for (Contact c : in) {
if (p.test(c)) {
out.add(c);
}
}
return out;
}System.out.println(adultsLoop(contacts));
System.out.println(keep(contacts, c -> c.getAge() >= 18));
System.out.println(keep(contacts, c -> c.getName().startsWith("A")));[Adam(52), Omar(34)]
[Adam(52), Omar(34)]
[Adam(52), Amal(17)]The first two lines are identical: the refactoring preserved behavior. The third line is new behavior that cost one line, not a whole new method.
Step 5: the planned mistake
Suppose you want to count adults, and, in the spirit of the lesson, you reach for forEach and a lambda:
int count = 0;
ages.forEach(x -> { if (x >= 18) { count++; } });
System.out.println(count);The compiler stops you:
Bad.java:6: error: local variables referenced from a lambda expression must be final or effectively final
ages.forEach(x -> { if (x >= 18) { count++; } });
^
1 errorYou met this rule in the lambda syntax lesson. The lambda wants to change a local variable owned by the method, and Java forbids it. There are two ways out.
Fix 1, restructure (preferred). Counting is not what forEach is for. A plain loop is fine, and so is the keep method you already have:
int count = 0;
for (Contact c : contacts) {
if (c.getAge() >= 18) {
count++;
}
}
// or, reusing what we built:
int count2 = keep(contacts, c -> c.getAge() >= 18).size();Fix 2, a mutable holder (use sparingly). The restriction is on the variable, not on the object it points to. An AtomicInteger is an object whose value can change, so the variable counter itself is never reassigned:
AtomicInteger counter = new AtomicInteger();
contacts.forEach(c -> {
if (c.getAge() >= 18) {
counter.incrementAndGet();
}
});
System.out.println(counter.get());AtomicInteger comes from java.util.concurrent.atomic and is designed for counters shared between threads; the concurrency modules explain it. Treat it here as a preview. It works, but it is a workaround that hides the real problem (a lambda that changes outside state). With the fix-1 code all three versions printed 2.
The final comparison
The refactoring achieved the lab's acceptance rule, same behavior with less code:
- Sort by name: 7 lines became 1.
- Each new filter: a new 8-line method became 1 line.
- Less duplicated logic, so fewer places for bugs.
Stop shrinking when the code stops getting clearer. A lambda that has grown braces and several statements should become a method (see the earlier lesson on lambda syntax), and a reference that puzzles readers should become a lambda or a named method.
Examples
Example 1: a tiny conversion (simple)
Before:
Runnable r = new Runnable() {
public void run() { System.out.println("tick"); }
};After:
Runnable r = () -> System.out.println("tick");Both print tick when r.run() is called.
Example 2: sorting by age, two directions (practical)
contacts.sort(Comparator.comparingInt(Contact::getAge));
System.out.println(contacts);
contacts.sort(Comparator.comparingInt(Contact::getAge).reversed());
System.out.println(contacts);[Sara(12), Amal(17), Omar(34), Adam(52)]
[Adam(52), Omar(34), Amal(17), Sara(12)]Compare this with writing two anonymous classes, one for each direction.
Example 3: the whole refactoring as a checklist (realistic)
You are given a service method with an anonymous comparator and two filter loops. Work in this order:
- Run the code or its tests and keep the output.
- Replace each anonymous class with a lambda. Run again.
- Where the lambda only calls one method, use a method reference or a
Comparatorbuilder. Run again. - Merge near-duplicate loops into one method with a
Predicateparameter. Run again. - Search for any lambda that changes a local variable, and restructure it.
- Compare before and after line counts, and write down what became clearer.
This is exactly the process of the lab, and it works because every step is small and checked.
Backend Insight
Refactoring toward lambdas is rarely about cleverness. It is about making the what visible: "sort by name", "keep adults". In a service class, such lines read like documentation, and a reviewer can check them quickly.
Two cautions for real projects. First, never refactor without a safety net: a test, or at least recorded output, as in step 0. Second, do not use lambdas to hide mutable state. The counter error was the compiler pointing at a real design smell: a callback that changes shared data is easy to get wrong when the callback runs on another thread, which is common in backend code. Prefer lambdas that take input and return output, and gather results with the right tool (a loop now, streams and collectors in BEG-036).
Common Mistakes
Mistake 1: Changing behavior while "just cleaning up"
Swapping a/b in a comparator or >= for > changes the result. Compare output after every step.
Mistake 2: Turning a long anonymous class into a long lambda
If the anonymous class had ten lines of logic, the lambda will too. Extract a method instead, and pass a lambda or reference that calls it.
Mistake 3: Reaching for AtomicInteger or an array to dodge the compiler
It compiles, but the compiler was pointing at a design problem. First ask whether a loop or a different method fits the job.
Mistake 4: Converting anonymous classes that need their own state or this
An anonymous class can have fields and this refers to itself; a lambda cannot hold its own fields, and this means the enclosing object. If the anonymous class stores state, keep it or turn it into a proper class.
Mistake 5: Forgetting that List.of cannot be sorted
List.of(...).sort(...) throws UnsupportedOperationException at runtime. Copy into an ArrayList first.
Exercises
Try each exercise yourself before opening the solutions. Use the Contact class and the keep method above.
Level A: Foundation
A1. Rewrite this anonymous class as a lambda:
Runnable r = new Runnable() {
public void run() { System.out.println("done"); }
};أظهر الحل A1
A1. Runnable r = () -> System.out.println("done");
A2. Rewrite contacts.sort((a, b) -> a.getName().compareTo(b.getName())); using Comparator.comparing and a method reference.
أظهر الحل A2
A2. contacts.sort(Comparator.comparing(Contact::getName));
A3. In "sort by name with a method reference", which functional interface does sort take, and which one does comparing take?
أظهر الحل A3
A3. List.sort takes a Comparator<Contact>; Comparator.comparing takes a Function (the key extractor) and returns a Comparator.
Level B: Application
B1. Sort contacts from oldest to youngest, using a comparator builder.
أظهر الحل B1
B1. contacts.sort(Comparator.comparingInt(Contact::getAge).reversed()); For the list above the result is [Adam(52), Omar(34), Amal(17), Sara(12)].
B2. Use keep to get contacts whose name has exactly four letters and who are under 18.
أظهر الحل B2
B2. keep(contacts, c -> c.getName().length() == 4 && c.getAge() < 18). With the sample data this returns Sara and Amal (both are four letters and under 18).
B3. A teammate writes int minors = 0; contacts.forEach(c -> { if (c.getAge() < 18) minors++; });. State the compiler's complaint and fix the code without a lambda.
أظهر الحل B3
B3. The complaint is "local variables referenced from a lambda expression must be final or effectively final", because minors++ changes a captured local. Fix:
int minors = 0;
for (Contact c : contacts) {
if (c.getAge() < 18) {
minors++;
}
}Level C: Challenge
C1. Write a new method static <T> int countIf(List<T> in, Predicate<T> p) and use it to count minors, so the original forEach problem disappears. Which earlier lesson's helper is it similar to?
أظهر الحل C1
C1.
static <T> int countIf(List<T> in, Predicate<T> p) {
int n = 0;
for (T t : in) {
if (p.test(t)) {
n++;
}
}
return n;
}
// countIf(contacts, c -> c.getAge() < 18) -> 2It is the count helper from the standard-interfaces lesson, the same shape as keep but counting instead of collecting. The counter variable lives inside the method, where a normal loop may change it.
C2. Write a short before/after summary (three bullet points) for a refactoring that replaced two anonymous comparators and two filter loops. What must stay the same, and what should shrink?
أظهر الحل C2
C2.
- Behavior must be identical: same sorted order and same lists, checked by comparing output before and after.
- Four anonymous classes or loops of about 7 to 8 lines each should shrink to one-liners plus a single
keepmethod. - Each remaining lambda should be a single clear idea, and any logic that needed braces should have become a named method.
Mini Challenge
This is practice for the lab. Write (or copy from your own earlier work) a short program with the following "before" code:
- A list of at least five
ContactorProductobjects. - An anonymous
Comparatorthat sorts by one field, and another that sorts by a second field. - Two loops that each filter the list with a different condition.
Save a copy of this "before" version. Then refactor in the order of the checklist in Example 3 and record the output after each step. Finally, count the lines before and after and write one sentence on what got clearer.
Check your work: every output matches the "before" version, no lambda changes a local variable, and none of your lambdas needs braces.
Summary & Next Lesson
Key takeaways
- Refactor in small steps and compare the output after each one.
- An anonymous class that implements a functional interface becomes a lambda, and often a method reference or builder such as
Comparator.comparing(Contact::getName). - Loops that differ only in a condition merge into one method with a
Predicateparameter. - Changing a local variable inside a lambda is a compile error. Restructure first; a mutable holder such as
AtomicIntegeris a last resort (preview of the concurrency modules). - Shrink until the code is clearer, not just shorter.
Next lesson
This is the last lesson of the module. Next, the lab "Filters" (BEG-035-LAB1) asks you to apply this process yourself. After the module, the course continues with BEG-036, "Streams", whose first lesson is "Streams vs loops".
Self-check
- In what order do you convert an anonymous
Comparator? - What do you check after every refactoring step?
- Why did
count++in a lambda fail to compile? - Name one better fix than
AtomicInteger. - When should you stop shrinking a lambda?
أظهر الإجابات
- First to a lambda, then, where it only calls one method, to a method reference or a
Comparatorbuilder. - That the output (behavior) is unchanged.
countis a captured local variable that the lambda tries to change, so it is not effectively final.- A plain loop, or a helper such as
countIfwith aPredicate. - When the next shrink would make the code harder to read, or when the lambda needs braces and several statements, which means it should be a named method.
References
- Lambda Expressions, The Java Tutorials (Oracle). Supports: replacing anonymous classes with lambdas, the final-or-effectively-final capture rule, and the difference in
thisbetween anonymous classes and lambdas. - Method References, The Java Tutorials (Oracle). Supports: using a method reference where a lambda only calls an existing method, such as the key extractor passed to
Comparator.comparing. - All code and the compiler message in this lesson were run with JDK 21; the outputs shown are real. The description of
AtomicIntegeras a thread-safe counter was not verified against its documentation in this session, so treat it as a preview that the concurrency modules will cover properly.
التطبيق
طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.
تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.