صياغة Lambda
في هذا الدرس 11 موضوعات
هذا الدرس متاح حاليًا باللغة الإنجليزية فقط، ولم تُنشر نسخته العربية بعد.
Lesson Overview
In the last lesson you saw that a lambda is a compact implementation of a functional interface, and you wrote a few without a full explanation of the notation. This lesson gives you the grammar: what a lambda can look like, how the compiler fills in the missing pieces, and the one rule about outside variables that causes most beginner errors.
In the recipe-card picture: a lambda is a card with a list of ingredients on the left (the parameters), an arrow, and the steps on the right (the body). Some cards are one line. Some need a full page. When a card needs several pages, it has stopped being a card and should be a real recipe in the cookbook, a named method.
Why this matters: lambdas are everywhere in modern Java, so reading them fluently is as basic as reading for loops. The "effectively final" rule is also a gateway to a deeper idea you will meet in the concurrency modules: shared mutable state is dangerous.
This lesson builds on "Passing behavior as a value", where a lambda was introduced as an implementation of a functional interface chosen by the target type.
Learning Objectives
By the end of this lesson you will be able to:
- Write a lambda with zero, one, or several parameters.
- Choose between an expression body and a block body, and use
returncorrectly. - Explain that parameter types are inferred from the target type, and add explicit types when you want them.
- Use a local variable from outside a lambda, and explain "effectively final".
- Recognize the compiler errors for changing a captured variable and for redeclaring a name.
- Decide when a lambda is too long and should become a method.
Written Explanation
The shape
A lambda has three parts, as the Oracle Java Tutorials describe them: a parameter list, the arrow token ->, and a body.
(parameters) -> bodyThe body is either a single expression or a block in braces. Here is a small functional interface we will use for the examples:
interface Op {
int apply(int a, int b);
}Its one abstract method takes two ints and returns an int. Any lambda assigned to an Op must match that shape.
Parameters
| Situation | Write | Example |
|---|---|---|
| No parameters | empty parentheses | () -> System.out.println("hi") |
| One parameter | parentheses optional | x -> x * 2 or (x) -> x * 2 |
| Several parameters | parentheses required | (a, b) -> a + b |
| With explicit types | all parameters typed | (int a, int b) -> a + b |
You normally leave the types out. The compiler already knows from the target type (Op) that a and b are int, so writing them is repeating yourself. Explicit types are legal, and handy when you want the code to document itself. Do not mix: either type every parameter or none.
Expression body
If the body is a single expression, write it directly. There is no return and no semicolon inside the lambda:
Op add = (a, b) -> a + b;If the interface method returns a value, the expression's value is the result. If the interface method returns void, the expression is just evaluated for its effect. For example, Runnable r = () -> System.out.println("hi"); is fine, even though println is a call, because Runnable.run returns void.
Block body
When you need several statements, use braces. Now you write statements, with semicolons, and a value-returning lambda must use return:
Op bigger = (a, b) -> {
if (a > b) {
return a;
}
return b;
};The braces-and-return form and the expression form are interchangeable when the body is a single statement: (a, b) -> a + b and (a, b) -> { return a + b; } mean the same.
Quick check: does the shape match?
For a lambda to compile, three things must line up with the interface's single abstract method:
- The number of parameters.
- The parameter types (inferred or stated).
- The return: a value of the right type, or nothing if the method is
void.
If a lambda does not compile, compare it with the method signature of the interface one piece at a time.
Using variables from outside: capturing
A lambda can read local variables and parameters from the method around it:
int taxPercent = 15;
Op withTax = (price, qty) -> price * qty * (100 + taxPercent) / 100;This is called capturing. The rule, from the Oracle tutorial and the Java Language Specification: a captured local variable must be final or effectively final, meaning it is never assigned a new value after it is first set. You do not have to write final; you just must never change the variable.
Why such a strict rule? A lambda may run later, or in another thread, long after the method that created it has moved on. A local variable lives in that method's frame, so Java copies the value into the lambda. If the variable could change afterwards, the lambda and the method would see different values, and the code would be confusing. The compiler removes the problem by forbidding the change.
Two errors show this rule. Changing a captured variable inside the lambda:
int count = 0;
names.forEach(n -> count++);Bad1.java:6: error: local variables referenced from a lambda expression must be final or effectively finalOr changing it anywhere, even outside the lambda, after you have set it:
int limit = 10;
limit = 20;
Runnable r = () -> System.out.println(limit);Bad2.java:5: error: local variables referenced from a lambda expression must be final or effectively finalNotice that the rule is about local variables. Fields of an object are not subject to it. That does not make it a good idea to change them from a lambda, but it is not a compiler error. The real fix is to restructure: count with a normal loop, or (next module) with a stream operation designed for counting.
No shadowing
A lambda does not create a new scope for names. The parameter of a lambda cannot reuse the name of a local variable that is already in scope:
int x = 5;
Function<Integer, Integer> f = x -> x * 2;Bad3.java:4: error: variable x is already defined in method main(String[])Rename the lambda parameter (n -> n * 2).
When a lambda gets too long
A lambda is at its best when it fits on one line. Once it grows to a block of five, ten, or twenty lines, you have a new problem: the logic hides inside an argument list, it has no name, and you cannot test it alone.
The fix is to move the logic into a named method and pass the method (the next lessons show the short syntax for this, method references). A good rule of thumb for this course: if you need braces, ask whether this deserves a name. If the body needs more than three lines, it does.
Examples
Example 1: all the shapes (simple)
Runnable r = () -> System.out.println("hi");
Op add = (a, b) -> a + b;
Op addTyped = (int a, int b) -> a + b;
Op bigger = (a, b) -> {
if (a > b) {
return a;
}
return b;
};
r.run();
System.out.println(add.apply(2, 3) + " " + addTyped.apply(4, 5) + " " + bigger.apply(7, 3));Output:
hi
5 9 7Three lambdas, three shapes: no parameters, an expression body, a block body. add and addTyped are identical in meaning.
Example 2: a captured variable (practical)
int taxPercent = 15;
Op withTax = (price, qty) -> price * qty * (100 + taxPercent) / 100;
System.out.println(withTax.apply(20, 5));Output:
115Twenty times five is 100; adding 15 percent gives 115. The lambda reads taxPercent from the method. If you added taxPercent = 20; anywhere later in the method, the code would stop compiling.
Example 3: short lambdas with collection methods (realistic)
List has a method removeIf that takes a condition, and a method forEach that takes an action. We use them here to show lambdas in context; you will meet them again with the standard interfaces in the next lesson.
List<String> names = new ArrayList<>(List.of("ana", "bo", "carl"));
names.removeIf(n -> n.length() < 3);
System.out.println(names);
names.forEach(n -> System.out.println(n.toUpperCase()));Output:
[ana, carl]
ANA
CARLEach lambda is one line and has no braces. That is the sweet spot.
Backend Insight
In backend code, lambdas often run later and elsewhere: when a request arrives, when a timer fires, or on a different thread from a pool. That is the real reason for the effectively-final rule. If a lambda could change a variable owned by the method that created it, you would have two pieces of code changing the same data at unknown times, which is the classic recipe for race conditions (you will study them in the concurrency modules).
The practical habit: lambdas should be small and free of side effects on outside variables. They compute something from their parameters, or they perform one clear action. If you need to accumulate a result, accumulate it with a purpose-built tool (a loop, a collector in BEG-036), not by mutating a variable from inside a lambda.
Another habit: a long lambda in a service class usually means a missing method. A named method can have a clear name, a unit test, and a breakpoint.
Common Mistakes
Mistake 1: Writing return in an expression body
- Incorrect:
Op add = (a, b) -> return a + b; - Correct:
Op add = (a, b) -> a + b;orOp add = (a, b) -> { return a + b; };
With braces you write statements, so return and semicolons are needed. Without braces you write one expression, so they are not.
Mistake 2: Changing a captured local variable
Using count++ or total += n inside a lambda on a local variable does not compile. Reassigning the variable outside the lambda after the lambda uses it fails as well. Restructure instead of looking for tricks.
Mistake 3: Mixing typed and untyped parameters
(int a, b) -> a + b is rejected. Either type all parameters or none.
Mistake 4: Reusing a local variable's name as a parameter
A lambda parameter named x clashes with an existing local x. Rename it.
Mistake 5: A twenty-line lambda
If the lambda needs braces, many statements, and its own local variables, extract a named method.
Exercises
Try each exercise yourself before opening the solutions. Use the interfaces Op (above) and Shout, where Shout has String shout(String s).
Level A: Foundation
A1. Write an Op lambda that multiplies its two parameters.
أظهر الحل A1
A1. Op mul = (a, b) -> a * b; For example mul.apply(6, 7) is 42.
A2. Write a Shout lambda that returns its input repeated twice, for example ab becomes abab.
أظهر الحل A2
A2. Shout twice = t -> t + t; For example twice.shout("ab") is abab.
A3. Which of these compile for Op? (a) (a, b) -> a - b (b) a, b -> a - b (c) (a, b) -> { a - b; } (d) (a, b) -> { return a - b; }
أظهر الحل A3
A3. (a) and (d) compile. (b) needs parentheses around two parameters. (c) has braces but no return, and a - b; is not a statement.
Level B: Application
B1. Write the larger-of-two Op using a block body with return.
أظهر الحل B1
B1. Op max = (a, b) -> { return a > b ? a : b; }; This prints 9 for max.apply(3, 9).
B2. The variable int base = 100; exists in your method. Write an Op lambda that returns a + b + base. What would happen if you then wrote base = 200; further down the method?
أظهر الحل B2
B2. Op plusBase = (a, b) -> a + b + base; gives 103 for apply(1, 2). If base = 200; appears later, base is no longer effectively final and the lambda fails to compile with the "final or effectively final" message.
B3. This code does not compile. Explain why, then rewrite it without a lambda that changes a local variable:
int total = 0;
List<Integer> nums = List.of(3, 4, 5);
nums.forEach(n -> total += n);أظهر الحل B3
B3. The lambda tries to change total, a captured local variable, which is not allowed. Use a loop:
int total = 0;
for (int n : nums) {
total += n;
}(Streams in BEG-036 offer another way.)
Level C: Challenge
C1. Write a static Op chooser(boolean wantMax) method that returns a lambda: the larger value if wantMax is true, the smaller otherwise. Explain why wantMax may be captured.
أظهر الحل C1
C1.
static Op chooser(boolean wantMax) {
if (wantMax) {
return (a, b) -> a > b ? a : b;
}
return (a, b) -> a < b ? a : b;
}Here the lambdas do not even use wantMax inside their body; the decision happens when the lambda is chosen. If a lambda did read it, that would be fine, because the parameter is never reassigned, so it is effectively final.
C2. A teammate wrote a 12-line block lambda inside a removeIf call that checks whether an order is overdue. Describe, in three or four sentences, how you would refactor it, and what you gain.
أظهر الحل C2
C2. Move the body into a named method such as isOverdue(Order order) in the right class, then call it from the lambda (o -> isOverdue(o)) or, after the next lessons, with a method reference. You gain a name that explains the intent, a method you can unit test, a place for a breakpoint, and a much shorter call site.
Mini Challenge
Write a small main with an interface Calc that has double compute(double x, double y). Create a lambda for each of: addition, "average of x and y", and "x as a percentage of y" (guard against y == 0 with a block body that returns 0). Store all three in a Calc[] or List<Calc> and print the result of each for the pair (8, 2).
Check your work: expression bodies have no return, the block body does, you did not mix typed and untyped parameters, and no lambda changes a local variable.
Summary & Next Lesson
Key takeaways
- A lambda is
(parameters) -> body. Parentheses are optional only with exactly one untyped parameter. - Types are inferred from the target type; write them only for clarity, and never mix.
- Use an expression body (no
return) for one expression, a block body (braces andreturn) for several statements. - A lambda may capture local variables only if they are final or effectively final. It cannot reuse a local variable's name as a parameter.
- If the lambda needs many lines, make it a named method.
Next lesson
The next lesson, "The standard functional interfaces", introduces the ready-made interfaces in java.util.function (Predicate, Function, Consumer, Supplier, BiFunction), so that you rarely need to write your own Op or PersonFilter.
Self-check
- When can the parentheses around lambda parameters be left out?
- What is the difference between
a -> a + 1anda -> { return a + 1; }? - What does "effectively final" mean?
- Why does Java forbid changing a captured local variable?
- When should a lambda become a method?
أظهر الإجابات
- When there is exactly one parameter and you do not write its type.
- Nothing in meaning: the first is an expression body, the second a block body with an explicit
return. - The variable is never assigned again after it is first given a value, even though it is not declared
final. - The lambda may run later or elsewhere and holds a copy of the value; allowing changes would give the lambda and the method different views of the variable.
- When it needs braces and several statements; a name, a test, and a breakpoint are worth more than saving one method declaration.
References
- Lambda Expressions, The Java Tutorials (Oracle). Supports: the three parts of a lambda, optional parentheses and types, expression versus block body, capture of final or effectively final locals, and no redeclaring of enclosing local names.
- Java Language Specification, Chapter 15 (SE 21). Fetched, but the page was too large to read as far as the lambda sections; the effectively-final rule here is therefore backed by the Oracle tutorial and by the compiler messages shown, which come from running JDK 21.
- The explanation of why the rule exists (lambdas may run later or elsewhere and hold a copy of the value) is the course's own simplified teaching explanation, not a quotation from the specification.
- All Java code in this lesson was compiled with JDK 21; the compiler messages shown are real.
التطبيق
طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.
تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.