The previous lesson ended with an uncomfortable observation: every comparator in the BiblioTech catalogue has one line of useful logic wrapped in five of ceremony. And that ceremony adds nothing, because all of it is information the compiler already has: it knows which interface you expect (the parameter of Arrays.sort says so), it knows which method has to be implemented (Comparator has only one) and it knows the type of the two arguments.
Lambda expressions, introduced in Java 8, let you write only what carries information: the parameters and the body. They were the biggest change to the language's syntax since its creation and they completely transformed the way Java is written: sorting collections, defining callbacks, launching concurrent tasks and — above all — processing data with the Streams API. This lesson teaches you what they really are (not what almost everybody believes), all of their syntax, their capture rules, the critical difference in this compared with anonymous classes, and how to use them in BiblioTech to change a class's behaviour without touching its code.
Contents
- From anonymous class to lambda, step by step
- Full syntax and shorthand forms
- What a lambda really is
- Type inference and the target type
- Capturing effectively final variables
- Why a lambda cannot modify a local variable
thisinside a lambda: the contrast with 04-04- Where lambdas are used today
- A BiblioTech functional interface of your own
- Parameterising behaviour:
MaterialFilter - Readability: when a lambda should be a method
- Common Mistakes and Tips
- Exercises
- From anonymous class to lambda, step by step
We start from the comparator by title of 04-04. We are going to remove ceremony in five steps, checking at each one what information is lost: none.
Step 0. The complete anonymous class.
Arrays.sort(catalog, new Comparator<Material>() {
@Override
public int compare(Material a, Material b) {
return a.getTitle().compareToIgnoreCase(b.getTitle());
}
});Step 1. Out with new Comparator<Material>(). The compiler already knows that Arrays.sort(T[], Comparator<? super T>) expects a Comparator<Material>: the array's type tells it so. Repeating it is redundant.
Arrays.sort(catalog,
public int compare(Material a, Material b) {
return a.getTitle().compareToIgnoreCase(b.getTitle());
}
);Step 2. Out with @Override, public and the method name. Comparator has a single abstract method: compare. There is no possible ambiguity about which one you are implementing, so naming it adds nothing.
Arrays.sort(catalog,
(Material a, Material b) {
return a.getTitle().compareToIgnoreCase(b.getTitle());
}
);Step 3. The arrow -> appears. Java needs a separator between the parameters and the body. That separator is the lambda operator.
Arrays.sort(catalog, (Material a, Material b) -> {
return a.getTitle().compareToIgnoreCase(b.getTitle());
});This is already a valid lambda and it compiles.
Step 4. Out with the parameter types. The compiler knows that compare receives two Materials. Writing it is optional.
Step 5. Out with the braces and the return. If the body is a single expression, its value is returned automatically.
From six lines to one. And the final line contains exactly the information that was not deducible: the two parameters and the operation.
| Step | What is removed | Why it can be |
|---|---|---|
| 1 | new Comparator<Material>() |
The type is imposed by the method's parameter |
| 2 | @Override public int compare |
The interface has a single abstract method |
| 3 | — | -> is added as a separator |
| 4 | Parameter types | They are inferred from the method signature |
| 5 | { } and return |
Single-expression body |
flowchart TD
A["new Comparator<Material>() { @Override public int compare(Material a, Material b) { ... } }"] --> B["The type is imposed by Arrays.sort: out with new Comparator"]
B --> C["The interface has a single abstract method: out with the name and the modifiers"]
C --> D["The arrow is added as a separator: (Material a, Material b) -> { ... }"]
D --> E["The types are inferred: (a, b) -> { ... }"]
E --> F["Single-expression body: (a, b) -> a.getTitle().compareToIgnoreCase(b.getTitle())"]
- Full syntax and shorthand forms
The general form is:
And it accepts several abbreviations depending on the number of parameters and the shape of the body:
| Form | Example | When it can be used |
|---|---|---|
| No parameters | () -> System.out.println("Hello") |
Whenever the method receives nothing. The parentheses are mandatory |
| One parameter with type | (Material m) -> m.getTitle() |
Always |
| One parameter without type | (m) -> m.getTitle() |
When the type can be inferred |
| One parameter without parentheses | m -> m.getTitle() |
Only with one parameter and no declared type |
| Several parameters with type | (Material a, Material b) -> ... |
Always |
| Several parameters without type | (a, b) -> ... |
When they can be inferred. All or nothing |
| Expression body | (a, b) -> a.getTitle().compareTo(b.getTitle()) |
Single-expression body; returns its value |
| Block body | (a, b) -> { ... return x; } |
Several statements; return is mandatory if there is a return value |
| Block without a return | m -> { System.out.println(m); } |
When the method returns void |
Equivalent examples of the same lambda, from most explicit to most concise:
// 1. Everything explicit
Comparator<Material> c1 = (Material a, Material b) -> {
return a.getTitle().compareToIgnoreCase(b.getTitle());
};
// 2. Without types
Comparator<Material> c2 = (a, b) -> {
return a.getTitle().compareToIgnoreCase(b.getTitle());
};
// 3. Expression body (the usual form)
Comparator<Material> c3 = (a, b) -> a.getTitle().compareToIgnoreCase(b.getTitle());And the rules to memorise, with their associated errors:
// With ONE parameter the parentheses are optional... if you do not declare the type
m -> m.getTitle() // valid
(m) -> m.getTitle() // valid
(Material m) -> m.getTitle() // valid
// Material m -> m.getTitle() // NOT VALID: with a type, the parentheses are mandatory
// With no parameters, ALWAYS the parentheses
() -> System.out.println("hi") // valid
// -> System.out.println("hi") // NOT VALID
// The types are ALL or NOTHING
(Material a, Material b) -> ... // valid
(a, b) -> ... // valid
// (Material a, b) -> ... // NOT VALID: a mixture
// Block body: return mandatory if there is a value
(a, b) -> { return a.getTitle().compareTo(b.getTitle()); } // valid
// (a, b) -> { a.getTitle().compareTo(b.getTitle()); } // DOES NOT COMPILE: return missingThere is one more variant, available since Java 11: var in the parameters, useful when you want to annotate them.
It is all or nothing too, and its only real reason to exist is to allow writing (@NotNull var a, var b) -> .... In day-to-day work it is rarely used.
- What a lambda really is
Here it is worth being precise, because there is a very widespread misunderstanding.
A lambda is NOT a function pointer. A lambda IS an instance of a functional interface.
A functional interface is an interface with exactly one abstract method. Comparator is one (compare), Runnable is one (run), Lendable is not (it has four).
When you write:
what you get is an object of a type implementing Comparator<Material>, whose compare method runs that body. You can check it:
Comparator<Material> byTitle = (a, b) -> a.getTitle().compareTo(b.getTitle());
System.out.println(byTitle instanceof Comparator); // true
System.out.println(byTitle.getClass().getName()); // App$$Lambda$14/0x...
System.out.println(byTitle.compare(book1, book2)); // invoked like any methodThe practical consequences of it being an object are important:
- You can store it in a variable, in a field or in an array.
- You can pass it as an argument and return it from a method.
- You must invoke its method by name:
byTitle.compare(a, b), notbyTitle(a, b). - It only works with functional interfaces. If the interface has two abstract methods, no lambda is possible: an anonymous class is needed (04-04).
Now, there is a technical difference from anonymous classes worth knowing. The compiler does not generate a .class file per lambda: it emits an invokedynamic instruction that creates the implementation at run time. That is why the class name you see above is so odd, and why thousands of lambdas do not make startup more expensive the way thousands of anonymous classes did.
The catalogue of functional interfaces shipped with the JDK (Function, Predicate, Consumer, Supplier...), the @FunctionalInterface annotation and method references are the full content of lesson 04-06. Here you will work with Comparator and with functional interfaces of your own.
- Type inference and the target type
If you do not write the types in the lambda, where do they come from? From the target type: the type the context expects at that point.
// Context 1: assignment to a declared variable
Comparator<Material> c = (a, b) -> a.getTitle().compareTo(b.getTitle());
// target type: Comparator<Material> -> a and b are Material
// Context 2: method argument
Arrays.sort(catalog, (a, b) -> a.getTitle().compareTo(b.getTitle()));
// target type: Comparator<? super Material> -> a and b are Material
// Context 3: return value
public Comparator<Material> titleCriterion() {
return (a, b) -> a.getTitle().compareTo(b.getTitle());
}And here comes the surprising consequence: the same lambda can mean different things depending on where you put it.
public interface TitleValidator {
boolean validate(String title);
}
public interface TextFilter {
boolean accepts(String text);
}TitleValidator v = t -> t.length() > 3; // implements TitleValidator.validate
TextFilter f = t -> t.length() > 3; // implements TextFilter.accepts
System.out.println(v.validate("Java")); // true
System.out.println(f.accepts("Java")); // true
System.out.println(v.getClass() == f.getClass()); // false: they are different typesThe lambda's text is identical and the objects are of completely different types. This explains two things:
A lambda has no type of its own. There is no such thing as "the type of t -> t.length() > 3". Its type is determined by the context. That is why this does not compile:
// var f = t -> t.length() > 3; // DOES NOT COMPILE: no target type to infer from
// Object o = t -> t.length() > 3; // DOES NOT COMPILE: Object is not a functional interfaceAn ambiguous lambda does not compile either. If a method is overloaded with two functional interfaces of compatible signature, the compiler cannot choose:
public void process(TitleValidator v) { }
public void process(TextFilter f) { }
// process(t -> t.length() > 3); // DOES NOT COMPILE: ambiguous reference
process((TitleValidator) t -> t.length() > 3); // disambiguated with a cast
- Capturing effectively final variables
A lambda can read:
- Its own parameters.
- The fields of the enclosing class (even mutable ones).
- The method's local variables, if they are
finalor effectively final.
public Material[] expensiveMaterials(Material[] materials, double threshold, int days) {
// 'threshold' and 'days' are parameters that are not reassigned: effectively final
Comparator<Material> byFine =
(a, b) -> Double.compare(b.calculateFine(days), a.calculateFine(days));
Material[] copy = Arrays.copyOf(materials, materials.length);
Arrays.sort(copy, byFine);
int count = 0;
for (Material m : copy) {
if (m.calculateFine(days) >= threshold) { count++; }
}
return Arrays.copyOf(copy, count);
}It is exactly the same rule as in local classes (04-03) and anonymous ones (04-04), and for the same reason: local variables live on the stack and die with the method; the lambda object lives on the heap and can outlive it. Java copies the value inside the object.
And the crucial distinction still holds: the restriction applies to the variable, not to the object.
Employee marta = new Employee("Marta Ruiz", "EMP-001");
StringBuilder log = new StringBuilder();
Runnable task = () -> {
marta.registerLoan(); // ALLOWED: the object is modified
log.append("loan registered"); // ALLOWED: the object is modified
// marta = new Employee(...); // FORBIDDEN: the variable is reassigned
};
- Why a lambda cannot modify a local variable
This is the mistake everybody makes in their first week:
public int countOverdue(Material[] materials, int days) {
int overdue = 0;
MaterialConsumer task = m -> {
if (m.calculateDaysLate(days) > 0) {
overdue++; // COMPILATION ERROR
}
};
// ...
}The cause, already familiar, is capture by value: the lambda stores a copy. If you could modify it, there would be two uncoordinated values with the same name. Java forbids it at compile time.
But there is a second reason, specific to lambdas and deeper: a lambda may run on another thread. If two threads incremented the same stack variable — something that is physically impossible anyway, because each thread has its own stack — the result would be undefined. The restriction cuts off at the root a whole category of concurrency errors you will study in module 8.
The three correct alternatives, from best to worst:
// A. A field of the class (lives on the heap, is legitimately mutable)
public class OverdueCounter {
private int overdue; // field, not a local variable
public void process(Material[] materials, int days) {
MaterialConsumer task = m -> {
if (m.calculateDaysLate(days) > 0) { overdue++; } // ALLOWED
};
for (Material m : materials) { task.accept(m); }
}
public int getOverdue() { return overdue; }
}
// B. An ordinary loop, no lambda: almost always the most readable
int overdue = 0;
for (Material m : materials) {
if (m.calculateDaysLate(days) > 0) { overdue++; }
}
// C. A one-element array (old trick: avoid it)
int[] overdue = {0};
MaterialConsumer task = m -> { if (m.calculateDaysLate(days) > 0) { overdue[0]++; } };Option B deserves a comment: accumulating into a variable is precisely what a loop does well. Do not force a lambda where a for is clearer. And for counting and summing over collections there will be a far better way: the Streams API, which is lesson 10-04.
this inside a lambda: the contrast with 04-04
this inside a lambda: the contrast with 04-04This section is the most important one in the lesson for anyone who already knows anonymous classes, because the behaviour is exactly the opposite.
Inside a lambda,
thisis the instance of the class containing it. A lambda does not introduce a newthisscope.
The lambda is said to have lexical scope: this, variable names and method names mean inside it the same as they would mean just outside.
package com.nexussoftware.bibliotech.service;
public class Auditor {
private final String source = "Auditor";
public void compare() {
// --- ANONYMOUS CLASS ---
Runnable withAnonymous = new Runnable() {
@Override
public void run() {
System.out.println("ANONYMOUS -> this.getClass() = "
+ this.getClass().getSimpleName());
System.out.println("ANONYMOUS -> outer source = "
+ Auditor.this.source);
}
};
// --- LAMBDA ---
Runnable withLambda = () -> {
System.out.println("LAMBDA -> this.getClass() = "
+ this.getClass().getSimpleName());
System.out.println("LAMBDA -> source = " + source);
// System.out.println(Auditor.this.source); // valid, but redundant
};
withAnonymous.run();
withLambda.run();
}
public static void main(String[] args) {
new Auditor().compare();
}
}ANONYMOUS -> this.getClass() = Auditor$1 ANONYMOUS -> outer source = Auditor LAMBDA -> this.getClass() = Auditor LAMBDA -> source = Auditor
The comparison table you must remember:
| Aspect | Anonymous class | Lambda |
|---|---|---|
this |
The anonymous instance | The instance of the enclosing class |
this.getClass() |
Outer$1 |
Outer |
| Accessing the outer field | Outer.this.field |
field directly |
| Can it have its own fields? | Yes | No |
| Can it shadow names from the outer class? | Yes (new scope) | No (lexical scope) |
Does it generate a .class? |
Yes, one per anonymous class | No: invokedynamic |
| Can it implement non-functional interfaces? | Yes | No |
That "cannot shadow" has a concrete consequence: inside a lambda you cannot declare a variable with the same name as one from the enclosing method.
public void example() {
String title = "Effective Java";
Runnable r1 = () -> {
// String title = "Other"; // DOES NOT COMPILE: 'title' is already defined
System.out.println(title);
};
Runnable r2 = new Runnable() {
@Override public void run() {
String title = "Other"; // IT DOES COMPILE: new scope, it shadows it
System.out.println(title); // prints "Other"
}
};
}It is the same reason as the change in this: the lambda does not create a new scope, it is code living in the same lexical scope. And this is the main source of bugs when migrating old code from anonymous classes to lambdas: if the anonymous class used this expecting to refer to itself, the equivalent lambda will do something different without giving any compilation error.
- Where lambdas are used today
Three scenarios you can already use with what you know, plus one that comes later.
Comparators. The case you have seen:
Arrays.sort(catalog, (a, b) -> a.getTitle().compareToIgnoreCase(b.getTitle()));
Arrays.sort(catalog, (a, b) -> Integer.compare(a.getLoanDays(), b.getLoanDays()));
Arrays.sort(catalog, (a, b) -> Double.compare(b.calculateFine(20), a.calculateFine(20)));Three criteria, three lines. Compare it with the fifteen lines per criterion of 04-04.
Runnable: a task with no arguments and no result.
Here you only run it directly. Its real use — passing it to a thread so it runs in parallel — is module 8.
Your own callbacks. The ReturnListener of 04-04 has a single abstract method, so it is a functional interface and accepts a lambda:
// Before (04-04): 6 lines of anonymous class
manager.setListener(new ReturnListener() {
@Override
public void onReturn(Loan loan, double fine) {
System.out.printf("RECEIPT %s: %.2f EUR%n", loan.getReference(), fine);
}
});
// Now: 1 line
manager.setListener((loan, fine) ->
System.out.printf("RECEIPT %s: %.2f EUR%n", loan.getReference(), fine));And where lambdas really shine: the Streams API. Operations such as filtering, transforming and grouping entire collections in a single chained expression. That is lesson 10-04, and we will not use it before then.
- A BiblioTech functional interface of your own
Defining your own functional interfaces is what turns lambdas into a design tool. Let us start with a configurable rate rule.
Today, a material's rate is fixed in its class: Book charges €0.25/day, always. But Nexus Software wants to run campaigns: half rate in August, double rate for materials in high demand, zero rate for interns. Adding an if per campaign inside Material would be exactly the design smell you learned to avoid in 03-06.
The solution: make the rule a parameter.
package com.nexussoftware.bibliotech.domain;
/**
* Rule for computing the daily rate applicable to a material.
*
* <p>It is a functional interface: it has a single abstract method, so
* any lambda of the form (Material, int) -> double implements it.</p>
*/
@FunctionalInterface
public interface RateRule {
/**
* @param material material the calculation applies to
* @param daysLate accumulated days late
* @return euros per day late that must be applied
*/
double rateFor(Material material, int daysLate);
}The @FunctionalInterface annotation makes the compiler verify that there is exactly one abstract method; if somebody adds a second one, the error appears here and not in the twenty places using lambdas. Its full meaning is 04-06.
Now a service that uses it:
package com.nexussoftware.bibliotech.service;
import com.nexussoftware.bibliotech.domain.Material;
import com.nexussoftware.bibliotech.domain.RateRule;
/** Computes fines applying a configurable rate rule. */
public class FineCalculator {
public static final double MAX_FINE = 20.0;
private final RateRule rule;
/** The rule is injected: the calculator does not know which one it is. */
public FineCalculator(RateRule rule) {
this.rule = (rule != null)
? rule
: (m, days) -> m.getDailyRate(); // default rule, as a lambda
}
public double calculate(Material material, int elapsedDays) {
int daysLate = material.calculateDaysLate(elapsedDays);
double rate = rule.rateFor(material, daysLate);
return Math.min(daysLate * rate, MAX_FINE);
}
}And the campaigns, each on one line:
Material dvd = new Dvd("Refactoring Live", "DVD-0007", 95);
// 1. Standard rule: each material's own rate
FineCalculator standard = new FineCalculator((m, days) -> m.getDailyRate());
// 2. August campaign: half rate
FineCalculator august = new FineCalculator((m, days) -> m.getDailyRate() / 2);
// 3. Progressive rate: doubles from the eighth day late onwards
FineCalculator progressive = new FineCalculator((m, days) ->
days > 7 ? m.getDailyRate() * 2 : m.getDailyRate());
// 4. Amnesty: no fines
FineCalculator amnesty = new FineCalculator((m, days) -> 0.0);
System.out.printf("Standard: %.2f EUR%n", standard.calculate(dvd, 20));
System.out.printf("August: %.2f EUR%n", august.calculate(dvd, 20));
System.out.printf("Progressive: %.2f EUR%n", progressive.calculate(dvd, 20));
System.out.printf("Amnesty: %.2f EUR%n", amnesty.calculate(dvd, 20));What has happened deserves a pause: four different fine policies, and neither Material nor Dvd nor FineCalculator has changed a single line. The variable behaviour is passed as if it were data. This has a name — the Strategy pattern — and you will formalise it in 12-02; here you have written it almost without noticing.
- Parameterising behaviour:
MaterialFilter
MaterialFilterA second example, with the classic problem of "give me the materials that meet X".
Without lambdas, each new criterion forces you to add a method to the catalogue: findByTitle, findAvailable, findByType, findWithFineGreaterThan... The class grows without end and each new criterion modifies it.
package com.nexussoftware.bibliotech.domain;
/** Material selection criterion. Functional interface. */
@FunctionalInterface
public interface MaterialFilter {
/** @return true if the material meets the criterion. */
boolean accepts(Material material);
}package com.nexussoftware.bibliotech.service;
import com.nexussoftware.bibliotech.domain.MaterialFilter;
import com.nexussoftware.bibliotech.domain.Material;
import java.util.Arrays;
public class Catalog {
private final Material[] materials;
public Catalog(Material[] materials) {
this.materials = Arrays.copyOf(materials, materials.length);
}
/**
* A single search method, valid for ANY criterion.
* The behaviour arrives as a parameter.
*/
public Material[] find(MaterialFilter filter) {
Material[] result = new Material[materials.length];
int n = 0;
for (Material m : materials) {
if (filter.accepts(m)) {
result[n++] = m;
}
}
return Arrays.copyOf(result, n); // module 5 will do this much better
}
/** Counts how many meet the criterion, without building the array. */
public int count(MaterialFilter filter) {
int n = 0;
for (Material m : materials) {
if (filter.accepts(m)) { n++; }
}
return n;
}
}And now, unlimited criteria without touching Catalog:
Catalog catalog = new Catalog(new Material[] {
new Book("Effective Java", "Joshua Bloch", "978-0000000001", 2018),
new Book("Design Patterns", "Erich Gamma", "978-0000000002", 1994),
new Book("Refactoring", "Martin Fowler", "978-0000000003", 1999),
new Magazine("Java Magazine", "REV-2024-03", 42, "Monthly"),
new Dvd("Refactoring Live", "DVD-0007", 95)
});
System.out.println("Available: "
+ catalog.count(m -> m.isAvailable()));
System.out.println("Books only: "
+ catalog.count(m -> m instanceof Book));
System.out.println("Short term (<= 7 days): "
+ catalog.count(m -> m.getLoanDays() <= 7));
System.out.println("With 'Refactor' in the title:");
for (Material m : catalog.find(m -> m.getTitle().contains("Refactor"))) {
System.out.println(" " + m.describe());
}
// A filter stored in a variable and reused
MaterialFilter expensive = m -> m.calculateFine(20) > 5.0;
System.out.println("High fine after 20 days: " + catalog.count(expensive));Available: 5 Books only: 3 Short term (<= 7 days): 2 With 'Refactor' in the title: Book "Refactoring" (ref. 978-0000000003) - Martin Fowler, 1999 DVD "Refactoring Live" (ref. DVD-0007) - 95 min High fine after 20 days: 1
This is the change of mindset in the module: behaviour is data. Before you passed numbers and strings; now you pass pieces of logic. The JDK already ships an interface identical to MaterialFilter — it is called Predicate — and you will see it in 04-06, together with the way to combine filters (and, or, negate).
- Readability: when a lambda should be a method
Lambdas are concise, and misapplied conciseness produces unreadable code. Three practical criteria:
The three-line rule. If the lambda's body goes beyond three lines, extract it into a named method.
// BAD: business logic buried in a long lambda
Material[] critical = catalog.find(m -> {
int daysLate = m.calculateDaysLate(20);
if (daysLate == 0) { return false; }
double fine = m.calculateFine(20);
boolean expensive = fine >= Material.MAX_FINE * 0.5;
boolean sensitiveType = m.getLoanDays() <= 3;
return expensive || (sensitiveType && daysLate > 5);
});
// GOOD: the condition has a name and can be tested separately
Material[] critical = catalog.find(m -> isCritical(m, 20));
private static boolean isCritical(Material m, int days) {
int daysLate = m.calculateDaysLate(days);
if (daysLate == 0) { return false; }
boolean expensive = m.calculateFine(days) >= Material.MAX_FINE * 0.5;
boolean sensitiveType = m.getLoanDays() <= 3;
return expensive || (sensitiveType && daysLate > 5);
}The good version gains three things: the condition has a name (isCritical explains the intent better than six lines of booleans), it is reusable in other criteria, and it is testable in isolation with JUnit in module 11.
If it repeats, give it a name. A lambda used in three places should be a constant:
public static final MaterialFilter AVAILABLE = m -> m.isAvailable();
public static final MaterialFilter BOOKS_ONLY = m -> m instanceof Book;
public static final Comparator<Material> BY_TITLE =
(a, b) -> a.getTitle().compareToIgnoreCase(b.getTitle());Name the parameters well. (a, b) is fine for a generic comparator; m is fine for a material in an obvious context. But in a two-line lambda, (loan, fine) reads infinitely better than (l, x). Conciseness does not justify cryptic names.
Common Mistakes and Tips
Believing a lambda is a free-standing function. It is an object implementing a functional interface. That is why it is invoked by the method's name (filter.accepts(m), not filter(m)) and why you cannot assign it to var or to Object.
Trying a lambda on a non-functional interface. Lendable l = () -> true; does not compile: Lendable has four abstract methods. Only interfaces with exactly one.
Forgetting the return in a block body. If you open braces and the method returns something, the return is mandatory. (a, b) -> { a.compareTo(b); } does not compile.
Putting a semicolon after an expression body. m -> m.getTitle(); inside a method call is an error. With an expression body there is no internal ;; with a block body, each statement carries its own.
Modifying a captured local variable. Forbidden. Use a field or a loop. And do not resort to the one-element array unless there is no alternative.
Getting this wrong. In a lambda, this is the enclosing class; in an anonymous class, the anonymous one. It is the number one error when converting old code, and it gives no compilation error: it simply does something else.
Mixing types in the parameters. (Material a, b) -> ... does not compile. Either all with types or none.
Kilometre-long lambdas. A twenty-line lambda inside a method call is worse than the anonymous class it replaces. Extract a method.
Tip: always annotate @FunctionalInterface on your single-method interfaces. It documents the intent and prevents a colleague from adding a second abstract method and breaking every lambda in the project.
Tip: learn to read inference errors. When a lambda does not compile, the message usually points at the target type, not at the lambda. If you get lost, temporarily write out the parameter types: the error becomes much clearer.
Tip: keep reused lambdas as static final constants. A single instance is created, the name documents the criterion and error traces are more readable.
Exercises
Exercise 1: rewriting the comparators of 04-04
Take the showSorted method from exercise 1 of lesson 04-04 and rewrite it using lambdas instead of anonymous classes. Count the lines before and after. Add a fourth criterion, "type", which sorts by type and breaks ties by title.
Exercise 2: NoticeRule, a functional interface of your own
Create a functional interface NoticeRule with the method String compose(Loan loan, double fine). Create a class NoticeService with a method send(Loan, double, NoticeRule) that prints the composed text. Pass it three different lambdas: a formal notice, a brief one and one that only writes something if the fine exceeds €5. Explain what advantage this has over three methods sendFormal, sendBrief and sendIfHigh.
Exercise 3: capture and this
Write a class Recorder with a field private int notices and a method process(Material[] materials, int days) that:
- Declares a local variable
int localCounter = 0and tries to increment it inside a lambda. Check the error and comment on it. - Increments the field
noticesfrom the lambda instead. - Prints
this.getClass().getSimpleName()inside the lambda and explains the result by comparing it with what an anonymous class would return.
Solutions
Solution 1
package com.nexussoftware.bibliotech.service;
import com.nexussoftware.bibliotech.domain.*;
import java.util.Arrays;
import java.util.Comparator;
public class CatalogSorter {
public static void showSorted(Material[] materials, String criterion, int days) {
Material[] copy = Arrays.copyOf(materials, materials.length);
Comparator<Material> comparator = switch (criterion) {
case "title" -> (a, b) -> a.getTitle().compareToIgnoreCase(b.getTitle());
case "term" -> (a, b) -> Integer.compare(a.getLoanDays(),
b.getLoanDays());
case "fine" -> (a, b) -> Double.compare(b.calculateFine(days),
a.calculateFine(days));
// Composite criterion: block body, because there is an intermediate condition
case "type" -> (a, b) -> {
int byType = a.getType().compareTo(b.getType());
if (byType != 0) {
return byType;
}
return a.getTitle().compareToIgnoreCase(b.getTitle());
};
default -> (a, b) -> 0;
};
Arrays.sort(copy, comparator);
System.out.println("--- Sorted by " + criterion + " ---");
for (Material m : copy) {
System.out.printf(" %-10s %-24s term %2d fine %5.2f EUR%n",
m.getType(), m.getTitle(),
m.getLoanDays(), m.calculateFine(days));
}
}
}--- Sorted by type --- Book Design Patterns term 15 fine 1.25 EUR Book Effective Java term 15 fine 1.25 EUR DVD Refactoring Live term 3 fine 8.50 EUR Magazine Java Magazine term 7 fine 1.30 EUR
The count: the switch with anonymous classes took 30 lines for four criteria; with lambdas it takes 14 including the composite criterion, which is the only one needing a block body. And the gain is not only in size: the sorting criterion now reads on a single line next to its case, without the eye having to jump over @Override public int compare(...) four times.
Note the two styles coexisting: the first three use an expression body (no braces, no return) and the fourth uses a block body because it has an intermediate condition. In 04-06 you will see that even that case can be written on one line with Comparator.comparing(...).thenComparing(...).
Solution 2
package com.nexussoftware.bibliotech.domain;
/** Composes the text of a return notice. Functional interface. */
@FunctionalInterface
public interface NoticeRule {
/**
* @return text to send, or null / an empty string if no notice applies
*/
String compose(Loan loan, double fine);
}package com.nexussoftware.bibliotech.service;
import com.nexussoftware.bibliotech.domain.Loan;
import com.nexussoftware.bibliotech.domain.NoticeRule;
/** Sends notices applying whichever composition rule it is given. */
public class NoticeService {
private int sent;
public void send(Loan loan, double fine, NoticeRule rule) {
String text = rule.compose(loan, fine);
if (text == null || text.isBlank()) {
System.out.println("(no notice for " + loan.getReference() + ")");
return;
}
sent++; // field: the lambda does not touch it, the service does
System.out.println(text);
}
public int getSent() { return sent; }
}NoticeService service = new NoticeService();
// 1. Formal notice
NoticeRule formal = (l, fine) -> String.format(
"Dear %s,%n The material \"%s\" (ref. %s) is %d days late.%n"
+ " Amount due: %.2f EUR.%n Yours sincerely, BiblioTech - Nexus Software.",
l.getEmployeeName(), l.getMaterialTitle(), l.getReference(),
l.calculateDaysLate(), fine);
// 2. Brief notice
NoticeRule brief = (l, fine) ->
String.format("[%s] %s: %.2f EUR", l.getReference(), l.getMaterialTitle(), fine);
// 3. Only if the fine is high: returns null and the service sends nothing
NoticeRule highOnly = (l, fine) -> fine > 5.0
? String.format("URGENT %s: %.2f EUR outstanding", l.getReference(), fine)
: null;
Employee marta = new Employee("Marta Ruiz", "EMP-001");
Loan l1 = new Loan(
new Book("Effective Java", "Joshua Bloch", "978-0000000001", 2018), marta, 100, 20);
Loan l2 = new Loan(
new Dvd("Refactoring Live", "DVD-0007", 95), marta, 100, 20);
service.send(l1, 1.25, brief);
service.send(l1, 1.25, highOnly);
service.send(l2, 8.50, highOnly);
service.send(l2, 8.50, formal);
System.out.println("Notices sent: " + service.getSent());[LN-0001] Effective Java: 1.25 EUR (no notice for LN-0001) URGENT LN-0002: 8.50 EUR outstanding Dear Marta Ruiz, The material "Refactoring Live" (ref. LN-0002) is 17 days late. Amount due: 8.50 EUR. Yours sincerely, BiblioTech - Nexus Software. Notices sent: 3
Advantages over three methods sendFormal, sendBrief and sendIfHigh:
| Aspect | Three methods | One method + NoticeRule |
|---|---|---|
| Adding a new format | Modify NoticeService |
Write a lambda where it is used |
| Sending logic (counter, empty check) | Duplicated three times | Written once |
| Composition | Impossible to combine formats | One rule can wrap another |
| Testing | Three methods to test | Sending is tested once and the rules separately |
| External configuration | Impossible | The rule can be chosen at run time |
It is the open/closed principle: the class stays open to extension (new rules) and closed to modification (its code does not change). You will formalise it in 12-02.
Solution 3
package com.nexussoftware.bibliotech.service;
import com.nexussoftware.bibliotech.domain.MaterialFilter;
import com.nexussoftware.bibliotech.domain.Material;
public class Recorder {
private int notices; // FIELD: lives on the heap, is mutable
public void process(Material[] materials, int days) {
int localCounter = 0; // LOCAL VARIABLE: lives on the stack
MaterialFilter isOverdue = m -> {
// localCounter++; // (1) DOES NOT COMPILE:
// "local variables referenced from a lambda expression
// must be final or effectively final"
//
// The lambda stores a COPY of the value. If it could modify it,
// there would be two different values with the same name: the method's
// and the lambda object's. Besides, the lambda could run on
// another thread, which has its own stack (module 8).
if (m.calculateDaysLate(days) > 0) {
notices++; // (2) IT DOES COMPILE: it is a field of the class
return true;
}
return false;
};
for (Material m : materials) {
isOverdue.accepts(m);
}
// (3) this inside the lambda
Runnable report = () -> System.out.printf(
"%s has recorded %d notices. this = %s%n",
getClass().getSimpleName(), notices,
this.getClass().getSimpleName());
report.run();
System.out.println("localCounter is still " + localCounter);
}
public int getNotices() { return notices; }
public static void main(String[] args) {
Material[] catalog = {
new Book("Effective Java", "Joshua Bloch", "978-0000000001", 2018),
new Magazine("Java Magazine", "REV-2024-03", 42, "Monthly"),
new Dvd("Refactoring Live", "DVD-0007", 95)
};
new Recorder().process(catalog, 20);
}
}The three answers:
localCounter++does not compile. The lambda captures the value by copy because the object may outlive the method, and modifying the copy would produce two uncoordinated values. The compiler stops it before the problem exists.notices++does compile.noticesis an instance field: it lives on the heap next to theRecorderobject, and the lambda reaches it throughthis, which it does capture. Mutability belongs to the object, not to the captured variable. (With several threads this would stop being safe without synchronisation: module 8.)this.getClass()returnsRecorder. The lambda does not create athisscope of its own: itsthisis the enclosing method's. With an anonymous class, the same line would have printedRecorder$1. And notice that inside the lambdagetClass()without a prefix works just likethis.getClass(), something that in an anonymous class would have requiredRecorder.this.getClass().
Conclusion
You have made the full journey from anonymous class to lambda, removing ceremony step by step and checking that no information is lost at any point: the type is imposed by the context, the method name is unique, the parameter types are inferred and the return is superfluous when the body is an expression. From six lines to one, and the remaining line contains exactly what was not deducible.
You have mastered the full syntax with all its forms: parentheses mandatory when there are no parameters or when the types are declared, optional with a single untyped parameter; types all or nothing; expression body without braces or return versus block body with a mandatory return. And you know what a lambda really is: not a function pointer but an instance of a functional interface — an interface with exactly one abstract method — a fully-fledged object you can store, pass and return, and whose method has to be invoked by name.
You understand inference by target type and its most striking consequence: the same lambda written word for word can produce objects of completely different types depending on where you put it, and that is why there is no such thing as "the type of a lambda" and why it cannot be assigned to var or Object. You know the rule for capturing effectively final variables, its twofold reason — copying by value and the possibility of running on another thread — and the correct alternatives when you genuinely need to accumulate: a field, or simply a loop. And you are perfectly clear about the difference that costs the most when migrating old code: in a lambda, this is the enclosing class, exactly the opposite of an anonymous class, and that change produces no compilation error whatsoever.
Above all, you have changed your mindset: behaviour is data. FineCalculator applies four different rate policies without Material or itself changing a line, and Catalog answers any imaginable search criterion with a single find(MaterialFilter) method. That is what lambdas really contribute: classes open to extension and closed to modification.
But you have written two functional interfaces of your own — MaterialFilter and NoticeRule — that look suspiciously like something that ought to be standard. And it is: the JDK ships a complete catalogue of functional interfaces in the java.util.function package, with Predicate for filtering, Function for transforming, Consumer for consuming and Supplier for producing, plus ways of combining them (and, or, negate, andThen) that turn two criteria into one. And there is more: when a lambda merely calls a method that already exists — m -> m.getTitle() — even that line is too much ceremony. In lesson 04-06, Functional Interfaces and Method References, you will see the whole catalogue, the @FunctionalInterface annotation and exactly what it checks, the composition of functions and comparators, and the four forms of method reference that reduce m -> m.getTitle() to Material::getTitle.
Java Programming Course
Module 1: Introduction to Java
- Introduction to Java
- Setting Up the Development Environment
- Basic Syntax and Structure
- Variables and Data Types
- Operators
- Console Input and Output
- Your First Complete Program: BiblioTech
Module 2: Control Flow
- Conditional Statements
- Loops
- Switch Statements
- Break and Continue
- Debugging and Execution Traces
- Project: The BiblioTech Interactive Menu
Module 3: Object-Oriented Programming
- Introduction to OOP
- Classes and Objects
- Methods
- Constructors
- Inheritance
- Polymorphism
- Encapsulation
- Abstraction
- The Object Class: equals, hashCode and toString
Module 4: Advanced Object-Oriented Programming
- Interfaces
- Abstract Classes
- Inner Classes
- Anonymous Classes
- Lambda Expressions
- Functional Interfaces and Method References
- Enums and Records
Module 5: Data Structures and Collections
- Arrays
- The Collections Framework
- ArrayList
- LinkedList
- HashMap
- HashSet
- Queue and Deque
- Stack
- Sorting and Searching Collections
Module 6: Exception Handling
- Introduction to Exceptions
- The Try-Catch Block
- Throw and Throws
- Custom Exceptions
- The Finally Block
- Try-with-resources and AutoCloseable
- Error Handling Strategies and Logging
Module 7: File Input/Output
- Reading Files
- Writing Files
- File Streams
- BufferedReader and BufferedWriter
- Serialization
- The NIO.2 API: Path and Files
- Interchange Formats: CSV and Properties
Module 8: Multithreading and Concurrency
- Introduction to Multithreading
- Creating Threads
- Thread Lifecycle
- Synchronization
- Concurrency Utilities
- Concurrent Collections and Atomic Variables
- Asynchronous Tasks with CompletableFuture
Module 9: Networking
- Introduction to Networking
- Sockets
- ServerSocket
- DatagramSocket and DatagramPacket
- URL and HttpURLConnection
- The Modern HTTP Client
Module 10: Advanced Topics
- Generics
- Annotations
- Reflection
- Java 8 Features: Streams and Optional
- Dates and Times with java.time
- Java 9 and Beyond
- Memory, Garbage Collection and Performance
Module 11: Java Frameworks and Libraries
- Introduction to Java Frameworks
- Spring Framework
- Hibernate
- JUnit
- Maven
- Advanced Testing with Mockito
- Essential Ecosystem Libraries
