At the end of the previous lesson two loose ends were left. The first: you wrote MaterialFilter and NoticeRule, two functional interfaces of your own that look suspiciously like something that ought to come as standard. And so it does: the JDK ships a complete catalogue of general-purpose functional interfaces in the java.util.function package, ready to use, with methods to combine them with each other. The second loose end: when a lambda merely calls a method that already exists — m -> m.getTitle() — even that line is too much ceremony; there is a way to write simply Material::getTitle.
This lesson closes the circle of programming with behaviour in Java. You will learn what a functional interface formally is and what @FunctionalInterface checks; you will go through the whole java.util.function catalogue knowing when to use each piece; you will compose functions, predicates and comparators to build complex criteria out of simple parts; and you will master the four forms of method reference. By the end, sorting the BiblioTech catalogue by type and then by title, in reverse order, will fit on one readable line.
Contents
- Formal definition of a functional interface
- The
@FunctionalInterfaceannotation: what exactly it checks - The
java.util.functionpackage: the full catalogue - The primitive variants and autoboxing
- The main interfaces applied to BiblioTech
- Composing functions:
andThenandcompose - Composing predicates:
and,or,negate - Composing comparators:
comparing,thenComparing,reversed - Method references: the four forms
- When the reference is more readable than the lambda
- Receiving behaviour as a parameter:
LoanManager - Common Mistakes and Tips
- Exercises
- Formal definition of a functional interface
A functional interface is an interface declaring exactly one abstract method.
It is also called a SAM (Single Abstract Method). It is the only kind of interface that can be implemented with a lambda or a method reference, because only with one abstract method can the compiler know, unambiguously, what your code is implementing.
The important word is abstract: there are three kinds of member that do not count towards the tally.
@FunctionalInterface
public interface MaterialFilter {
// 1. THE abstract method: it counts
boolean accepts(Material material);
// 2. default: does NOT count
default MaterialFilter negated() {
return m -> !this.accepts(m);
}
// 3. static: does NOT count
static MaterialFilter all() {
return m -> true;
}
// 4. private: does NOT count (Java 9)
private boolean never(Material m) {
return false;
}
}This interface is still functional: it has one abstract method, accepts. The default, static and private ones are implementations, not obligations.
And there is a fourth exception, less well known and very important in practice: redeclared public methods of Object do not count either.
@FunctionalInterface
public interface MaterialComparator {
int compare(Material a, Material b);
// Redeclaring equals does NOT break functionality:
// every class already inherits it from Object
@Override boolean equals(Object o);
}It is exactly the case of java.util.Comparator, which declares compare and equals, and is still functional. If you did not know this, seeing it in the JDK source would be bewildering.
// These ARE functional
Runnable // run()
Comparator<T> // compare(T,T) [+ equals, which does not count]
MaterialFilter // accepts(Material)
RateRule // rateFor(Material,int)
// These are NOT
Lendable // 4 abstract methods
Notifiable // 2 abstract methods
- The
@FunctionalInterface annotation: what exactly it checks
@FunctionalInterface annotation: what exactly it checks@FunctionalInterface is an optional but strongly recommended annotation. It does not change the code's behaviour: it enables a compile-time check.
@FunctionalInterface
public interface MaterialFilter {
boolean accepts(Material material);
boolean rejects(Material material); // a second abstract method
}error: Unexpected @FunctionalInterface annotation MaterialFilter is not a functional interface multiple non-overriding abstract methods found in interface MaterialFilter
What it checks exactly:
| It checks | It does not check |
|---|---|
| That it is an interface (not a class or enum) | That it is actually used with lambdas |
| That it has exactly one abstract method | That the method's name is nice |
| That it does not have zero abstract methods | Anything at run time |
And why you should always use it on your single-method interfaces, even though it is optional:
- It documents the intent. Whoever reads it knows it is meant for lambdas.
- It protects the contract. Without the annotation, a colleague can add a second abstract method and the error will show up in the twenty places where you used lambdas, with confusing messages. With it, the error shows up in the interface, with the exact reason.
- It costs one line.
Mind the asymmetry: an interface without the annotation but with a single abstract method does accept lambdas. The annotation enables nothing, it only verifies.
RunnableandComparatorworked with lambdas from day one of Java 8.
- The
java.util.function package: the full catalogue
java.util.function package: the full catalogueJava 8 introduced java.util.function with more than forty general-purpose functional interfaces. There is no need to memorise them: it is enough to understand the six families and their naming logic.
| Interface | Abstract method | Receives | Returns | What it is for |
|---|---|---|---|---|
Function<T,R> |
R apply(T t) |
1 value | 1 value | Transforming one value into another |
BiFunction<T,U,R> |
R apply(T t, U u) |
2 values | 1 value | Transforming two values into one |
Consumer<T> |
void accept(T t) |
1 value | nothing | Consuming: printing, storing, notifying |
BiConsumer<T,U> |
void accept(T t, U u) |
2 values | nothing | Consuming two values |
Supplier<T> |
T get() |
nothing | 1 value | Producing a value on demand |
Predicate<T> |
boolean test(T t) |
1 value | boolean |
Deciding: filtering, validating |
BiPredicate<T,U> |
boolean test(T t, U u) |
2 values | boolean |
Deciding about two values |
UnaryOperator<T> |
T apply(T t) |
1 value of type T | value of type T | Transforming without changing type |
BinaryOperator<T> |
T apply(T a, T b) |
2 values of type T | value of type T | Combining two into one of the same type |
The naming system is completely regular, and understanding it is worth more than memorising the table:
Biin front = receives two arguments (BiFunction,BiConsumer,BiPredicate).Operator= a special case ofFunctionwhere input and output are of the same type.UnaryOperator<String>is literally aFunction<String,String>with a shorter name.- The method's verb gives away the family:
applytransforms,acceptconsumes,getproduces,testdecides.
About the angle brackets: Function<Material, String> means "a function that receives a Material and returns a String". Here you are only using existing generic types; writing your own is lesson 10-01.
flowchart LR
A["Supplier<T><br/>nothing -> T"] --> B["Function<T,R><br/>T -> R"]
B --> C["Predicate<T><br/>T -> boolean"]
B --> D["Consumer<T><br/>T -> nothing"]
- The primitive variants and autoboxing
Alongside the nine generic interfaces, the package includes dozens of primitive variants: IntPredicate, ToDoubleFunction<T>, IntSupplier, DoubleConsumer, IntUnaryOperator, ToIntBiFunction<T,U>...
Why do they exist? Because of autoboxing, which you studied in 01-04.
Generic types do not accept primitives: there is no Function<int, int>. You would have to write Function<Integer, Integer>, and then every call involves two hidden conversions:
// WITH autoboxing: Integer -> int -> calculation -> int -> Integer
Function<Integer, Integer> twice = n -> n * 2;
Integer result = twice.apply(21);
// 1. unboxing of 21 (Integer) to int
// 2. calculation
// 3. boxing of 42 (int) to Integer -> an object is created// WITHOUT autoboxing: pure int
IntUnaryOperator fastTwice = n -> n * 2;
int fast = fastTwice.applyAsInt(21); // zero objects createdIn a loop of a million iterations, the first version creates a million Integer objects that the collector will have to clean up. The second creates none.
The naming rules, also regular:
| Prefix | Meaning | Example |
|---|---|---|
Int, Long, Double at the start |
The argument is primitive | IntPredicate: boolean test(int) |
To + type |
The result is primitive | ToDoubleFunction<T>: double applyAsDouble(T) |
Type + To + type |
Both are primitive | IntToDoubleFunction: double applyAsDouble(int) |
Applied to BiblioTech:
// A material's fine at 20 days: receives an object, returns a primitive double
ToDoubleFunction<Material> fineAt20 = m -> m.calculateFine(20);
double total = fineAt20.applyAsDouble(dvd); // without creating any Double
// Checking whether a term is valid: receives an int, returns a boolean
IntPredicate validTerm = days -> days > 0 && days <= 30;
System.out.println(validTerm.test(15)); // truePractical tip: in ordinary business code, use the generic versions; they are more readable. Turn to the primitive ones when working with large volumes or in hot loops. And know that the Streams API uses the primitive ones heavily (IntStream, mapToDouble); you will see that in 10-04.
- The main interfaces applied to BiblioTech
Let us now replace the interfaces of your own from 04-05 with the standard ones, and see the four families in action.
Predicate<T>: deciding. It directly replaces MaterialFilter.
import java.util.function.Predicate;
Predicate<Material> available = m -> m.isAvailable();
Predicate<Material> isBook = m -> m instanceof Book;
Predicate<Material> shortTerm = m -> m.getLoanDays() <= 7;
System.out.println(available.test(effectiveJava)); // true
System.out.println(isBook.test(refactoringDvd)); // falseFunction<T,R>: transforming.
import java.util.function.Function;
Function<Material, String> toTitle = m -> m.getTitle();
Function<Material, String> toLabel = m -> m.getType() + " / " + m.getReference();
Function<Employee, String> toInitials = e -> e.getInitials();
System.out.println(toLabel.apply(effectiveJava)); // Book / 978-0000000001
System.out.println(toInitials.apply(marta)); // M.R.Consumer<T>: consuming without returning anything.
import java.util.function.Consumer;
Consumer<Material> print = m -> System.out.println(" " + m.describe());
Consumer<Material> lend = m -> m.lend();
for (Material m : catalog) {
print.accept(m);
}Supplier<T>: producing on demand.
import java.util.function.Supplier;
Supplier<Employee> defaultEmployee = () -> new Employee("Unassigned", "EMP-000");
Supplier<String> timestamp = () -> "day " + System.currentTimeMillis() / 86_400_000;
Employee e = (assigned != null) ? assigned : defaultEmployee.get();The point of Supplier is lazy evaluation: the object is not created until get() is called. If creating the default value were expensive, with a Supplier you only pay that cost when it is genuinely needed.
BiFunction<T,U,R> and BinaryOperator<T>.
import java.util.function.BiFunction;
import java.util.function.BinaryOperator;
// Two inputs of different types, one output of a third type
BiFunction<Material, Integer, Double> fineAt = (m, days) -> m.calculateFine(days);
System.out.printf("%.2f EUR%n", fineAt.apply(refactoringDvd, 20)); // 8.50 EUR
// Two inputs and one output, all of the same type
BinaryOperator<Material> theMoreExpensive = (a, b) ->
a.calculateFine(20) >= b.calculateFine(20) ? a : b;
System.out.println(theMoreExpensive.apply(effectiveJava, refactoringDvd).getTitle());UnaryOperator<T>: transforming without changing type.
import java.util.function.UnaryOperator;
UnaryOperator<String> normalise = t -> t.trim().toUpperCase();
System.out.println(normalise.apply(" effective java ")); // EFFECTIVE JAVA
- Composing functions:
andThen and compose
andThen and composeHere is where things get truly powerful. The java.util.function interfaces come with default methods that combine two functions into a third.
Function offers two, and the difference is only in the order:
| Method | Meaning | Execution order |
|---|---|---|
f.andThen(g) |
First f, then g |
g(f(x)) |
f.compose(g) |
First g, then f |
f(g(x)) |
Function<Material, String> toTitle = m -> m.getTitle();
Function<String, String> toUpper = t -> t.toUpperCase();
Function<String, String> inQuotes = t -> "\"" + t + "\"";
// andThen: reads left to right, like a pipeline
Function<Material, String> label = toTitle.andThen(toUpper).andThen(inQuotes);
System.out.println(label.apply(effectiveJava)); // "EFFECTIVE JAVA"
// compose: reads right to left
Function<Material, String> same = inQuotes.compose(toUpper).compose(toTitle);
System.out.println(same.apply(effectiveJava)); // "EFFECTIVE JAVA"flowchart LR
A["Material"] -->|"toTitle"| B["Effective Java"]
B -->|"toUpper"| C["EFFECTIVE JAVA"]
C -->|"inQuotes"| D["\"EFFECTIVE JAVA\""]
Use andThen almost always. It reads in the order things happen, which is how the reader thinks. compose exists out of fidelity to mathematical notation (f ∘ g), and in code it usually confuses.
Consumer also has andThen, with the difference that both consumers receive the original value (neither returns anything to chain):
Consumer<Material> logIt = m -> System.out.println("LOG: " + m.getReference());
Consumer<Material> show = m -> System.out.println(" " + m.describe());
Consumer<Material> both = logIt.andThen(show);
both.accept(effectiveJava);
- Composing predicates:
and, or, negate
and, or, negatePredicate offers three combination methods corresponding to the logical operators of 01-05:
| Method | Equivalent to | Description |
|---|---|---|
p.and(q) |
p && q |
Meets both. Short-circuits: if p is false, q is not evaluated |
p.or(q) |
p || q |
Meets either. Short-circuits too |
p.negate() |
!p |
Does not meet |
Predicate.not(p) |
!p |
Same as negate(), in static form (Java 11) |
Predicate.isEqual(x) |
x.equals(...) |
Equality predicate, static |
Predicate<Material> available = m -> m.isAvailable();
Predicate<Material> isBook = m -> m instanceof Book;
Predicate<Material> expensive = m -> m.calculateFine(20) > 5.0;
// Combinations, each on one line
Predicate<Material> availableBook = isBook.and(available);
Predicate<Material> onLoan = available.negate();
Predicate<Material> expensiveOrOnLoan = expensive.or(onLoan);
Predicate<Material> cheapFreeBook = isBook.and(expensive.negate()).and(available);Applied to the BiblioTech catalogue, with the method count(Predicate<Material>):
public int count(Predicate<Material> criterion) {
int n = 0;
for (Material m : materials) {
if (criterion.test(m)) { n++; }
}
return n;
}System.out.println("Available books: " + catalog.count(availableBook));
System.out.println("On loan: " + catalog.count(onLoan));
System.out.println("Cheap free books: " + catalog.count(cheapFreeBook));The underlying advantage: the three base predicates (isBook, available, expensive) are written once and from them come as many combinations as you like, each with its own name and testable separately. It is the same leap that functions make over copied code, applied to conditions.
A detail about Predicate.not versus negate:
Predicate<Material> notAvailable1 = available.negate(); // instance method
Predicate<Material> notAvailable2 = Predicate.not(available); // static, Java 11
Predicate<Material> notAvailable3 = Predicate.not(Material::isAvailable); // referenceThe third form is the most common in modern code, and it uses a method reference: that arrives in section 9.
- Composing comparators:
comparing, thenComparing, reversed
comparing, thenComparing, reversedThis is the application that will most improve your BiblioTech code. Remember the composite comparator from 04-05:
// Before: block body with an intermediate condition
Comparator<Material> byTypeAndTitle = (a, b) -> {
int byType = a.getType().compareTo(b.getType());
if (byType != 0) {
return byType;
}
return a.getTitle().compareToIgnoreCase(b.getTitle());
};Comparator offers a set of methods that reduces it to one line:
| Method | What it does |
|---|---|
Comparator.comparing(f) |
Creates a comparator that compares by the key f extracts |
Comparator.comparingInt(f) |
The same, with an int key (no autoboxing). Also comparingDouble, comparingLong |
c.thenComparing(f) |
Tie-breaker: if c gives equality, compare by f |
c.reversed() |
Reverses the order |
Comparator.naturalOrder() |
The type's natural order (that of compareTo) |
Comparator.reverseOrder() |
Natural order reversed |
c.nullsFirst(c2) / nullsLast(c2) |
Places the nulls at the beginning or at the end |
import java.util.Comparator;
// One line, and it reads like a sentence
Comparator<Material> byTypeAndTitle =
Comparator.comparing((Material m) -> m.getType())
.thenComparing(m -> m.getTitle());
// Highest fine first
Comparator<Material> byFineDesc =
Comparator.comparingDouble((Material m) -> m.calculateFine(20)).reversed();
// Three levels: by term ascending, then by type, then by title
Comparator<Material> full =
Comparator.comparingInt((Material m) -> m.getLoanDays())
.thenComparing(m -> m.getType())
.thenComparing(m -> m.getTitle());
Arrays.sort(catalog, full);Applied to the catalogue:
Material[] data = {
new Dvd("Refactoring Live", "DVD-0007", 95),
new Book("Effective Java", "Joshua Bloch", "978-0000000001", 2018),
new Magazine("Java Magazine", "REV-2024-03", 42, "Monthly"),
new Book("Design Patterns", "Erich Gamma", "978-0000000002", 1994),
new Book("Refactoring", "Martin Fowler", "978-0000000003", 1999)
};
Arrays.sort(data, byTypeAndTitle);
for (Material m : data) {
System.out.printf(" %-10s %s%n", m.getType(), m.getTitle());
}Book Design Patterns Book Effective Java Book Refactoring DVD Refactoring Live Magazine Java Magazine
Two important warnings.
First, the position of reversed() matters. c1.thenComparing(c2).reversed() reverses the whole composite criterion; c1.reversed().thenComparing(c2) reverses only the first one. They are not the same.
Second, the type annotation in the first comparing. Notice that it is written (Material m) -> ... instead of m -> .... The reason is that the compiler infers the lambda's type from the target type, and in Comparator.comparing(...) chained with .thenComparing(...) the inference does not always get there. The alternative — and the cleanest one — is to declare the variable's type or use a method reference:
// With the variable's type declared, the lambda no longer needs annotating
Comparator<Material> c1 = Comparator.comparing(Material::getType)
.thenComparing(Material::getTitle);That Material::getType is exactly what comes next.
- Method references: the four forms
When a lambda does nothing more than call an existing method, the method reference expresses the same thing without noise. The syntax is Target::methodName, without parentheses and without arguments.
| Form | Syntax | Equivalent lambda | Example |
|---|---|---|---|
| 1. Static method | Class::method |
x -> Class.method(x) |
Integer::parseInt |
| 2. Instance method of a specific object | object::method |
x -> object.method(x) |
receipt::print |
| 3. Instance method of an arbitrary object of the type | Class::method |
x -> x.method() |
Material::getTitle |
| 4. Constructor | Class::new |
x -> new Class(x) |
Book::new |
Forms 1 and 3 are written the same way (Class::method) and the compiler tells them apart according to whether the method is static or an instance one. It is the main source of confusion, so let us go through them one by one.
Form 1: static method. The lambda's argument is passed to the method.
Function<String, Integer> toInt1 = s -> Integer.parseInt(s); // lambda
Function<String, Integer> toInt2 = Integer::parseInt; // reference
System.out.println(toInt2.apply("42") + 1); // 43
// Another example from the domain
BiFunction<Double, Double, Double> min = Math::min;
System.out.println(min.apply(8.50, 20.0)); // 8.5Form 2: instance method of a specific object. The object is already fixed; the lambda's argument goes to the method.
ConsoleReceipt receipt = new ConsoleReceipt();
Consumer<Loan> print1 = l -> receipt.printReceipt(l); // lambda
Consumer<Loan> print2 = receipt::printReceipt; // reference
print2.accept(loan);A detail worth knowing: the reference captures the object at the moment it is created. If you later reassign receipt to another object, print2 will keep using the original. It is consistent with the capture rule of 04-05.
Form 3: instance method of an arbitrary object of the type. It is the most used and the hardest at first. Here the lambda's argument becomes the receiver of the call.
Function<Material, String> toTitle1 = m -> m.getTitle(); // lambda
Function<Material, String> toTitle2 = Material::getTitle; // reference
// ^^^^^^^^ the parameter becomes 'm'
Predicate<Material> free1 = m -> m.isAvailable();
Predicate<Material> free2 = Material::isAvailable;
Comparator<String> alphabetical = String::compareToIgnoreCase;
// equivalent to: (a, b) -> a.compareToIgnoreCase(b)
// the FIRST argument is the receiver, the rest are the parametersThat last line explains the whole mechanism: in Class::method over an instance method, the first argument of the functional interface is the object the call is made on, and the others are the method's parameters.
Form 4: constructor.
Supplier<Employee> newAnonymous = () -> new Employee("Unassigned", "EMP-000");
// With a one-argument constructor
Function<String, StringBuilder> toBuffer = StringBuilder::new;
// With a multi-argument constructor, using a functional interface of your own
@FunctionalInterface
interface BookCreator {
Book create(String title, String author, String isbn, int year);
}
BookCreator creator1 = (t, a, i, y) -> new Book(t, a, i, y); // lambda
BookCreator creator2 = Book::new; // reference
Book newBook = creator2.create("Refactoring", "Martin Fowler", "978-0000000003", 1999);
System.out.println(newBook.describe());Book::new automatically picks the constructor whose signature fits the functional interface's method. Since Book has two constructors (one with four parameters and one with five), the BookCreator interface with four parameters determines which one is used.
Now, the comparators of section 8 with references:
Comparator<Material> byTitle = Comparator.comparing(Material::getTitle);
Comparator<Material> byTerm = Comparator.comparingInt(Material::getLoanDays);
Comparator<Material> byTypeTitle = Comparator.comparing(Material::getType)
.thenComparing(Material::getTitle);
Comparator<Material> byTypeDesc = byTypeTitle.reversed();That is the line promised at the start of the lesson: sort by type, break ties by title and reverse the lot, in text that reads like an English sentence.
- When the reference is more readable than the lambda
Not always. These criteria will save you code-review arguments.
Use it when the lambda only delegates:
m -> m.getTitle() → Material::getTitle // better
s -> Integer.parseInt(s) → Integer::parseInt // better
l -> receipt.printReceipt(l) → receipt::printReceipt // better
(a, b) -> a.compareTo(b) → String::compareTo // betterDo not force it when there is logic, however small:
// There is an operation: the lambda is mandatory and also clearer
m -> m.calculateFine(20)
m -> m.getTitle().toUpperCase()
m -> !m.isAvailable()Beware of losing parameter names. Compare:
// Reference: concise, but it does not say what is being compared
Arrays.sort(catalog, Comparator.comparing(Material::getTitle));
// Lambda: explicitly says there are two materials and how they relate
Arrays.sort(catalog, (a, b) -> a.getTitle().compareToIgnoreCase(b.getTitle()));Here the reference wins, because comparing(Material::getTitle) reads as "comparing by the title". But in cases with several arguments of the same type, the lambda with explicit names can be clearer.
Be consistent within a single expression. Mixing styles in a chain makes it hard to follow:
// BAD: a mixture of styles
Comparator.comparing(Material::getType).thenComparing(m -> m.getTitle())
// GOOD
Comparator.comparing(Material::getType).thenComparing(Material::getTitle)
- Receiving behaviour as a parameter:
LoanManager
LoanManagerLet us close with the complete application: a class that receives behaviour from outside in order to decide who to notify, what text to send and how to send it, without knowing any of the three things.
package com.nexussoftware.bibliotech.service;
import com.nexussoftware.bibliotech.domain.Loan;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
/**
* Coordinates the sending of loan notices.
*
* <p>It does not decide who is notified (a Predicate says so), nor what text
* is sent (a Function says so), nor through which channel it is sent (a Consumer
* says so). It only orchestrates.</p>
*/
public class LoanManager {
private final Loan[] loans; // provisional array: collections in module 5
public LoanManager(Loan[] loans) {
this.loans = loans.clone();
}
/**
* Sends one notice per loan meeting the criterion.
*
* @param criterion decides WHICH loans are notified
* @param composer decides WHAT text is sent
* @param channel decides HOW it is sent
* @return number of notices sent
*/
public int sendNotices(Predicate<Loan> criterion,
Function<Loan, String> composer,
Consumer<String> channel) {
int sent = 0;
for (Loan l : loans) {
if (criterion.test(l)) {
channel.accept(composer.apply(l));
sent++;
}
}
return sent;
}
/** Counts the loans meeting a criterion. */
public int count(Predicate<Loan> criterion) {
int n = 0;
for (Loan l : loans) {
if (criterion.test(l)) { n++; }
}
return n;
}
}And its usage, composing criteria out of simple parts:
// --- Base criteria, each with a name explaining its intent ---
Predicate<Loan> overdue = Loan::isOverdue;
Predicate<Loan> returned = Loan::isReturned;
Predicate<Loan> highFine = l -> l.calculateFine() > 5.0;
// --- Composite criteria ---
Predicate<Loan> overduePending = overdue.and(returned.negate());
Predicate<Loan> urgent = overduePending.and(highFine);
// --- Composers ---
Function<Loan, String> brief = l -> String.format(
"[%s] %s - %d days late, %.2f EUR",
l.getReference(), l.getMaterialTitle(),
l.calculateDaysLate(), l.calculateFine());
Function<Loan, String> indented = brief.andThen(t -> " " + t);
// --- Channels ---
Consumer<String> toConsole = System.out::println; // reference, form 2
Consumer<String> withPrefix = t -> System.out.println("NOTICE> " + t);
Consumer<String> both = toConsole.andThen(withPrefix); // both at once
// --- Usage ---
LoanManager manager = new LoanManager(loans);
System.out.println("=== All overdue and pending ===");
int n1 = manager.sendNotices(overduePending, indented, toConsole);
System.out.println("=== Only the urgent ones, through two channels ===");
int n2 = manager.sendNotices(urgent, brief, both);
System.out.printf("Sent: %d normal, %d urgent%n", n1, n2);
System.out.println("Overdue and pending: " + manager.count(overduePending));=== All overdue and pending === [LN-0001] Effective Java - 5 days late, 1.25 EUR [LN-0002] Refactoring Live - 17 days late, 8.50 EUR [LN-0003] Java Magazine - 13 days late, 1.30 EUR === Only the urgent ones, through two channels === [LN-0002] Refactoring Live - 17 days late, 8.50 EUR NOTICE> [LN-0002] Refactoring Live - 17 days late, 8.50 EUR Sent: 3 normal, 1 urgent
Look back at what LoanManager does not know: it does not know what an urgent loan is, it does not know what text is sent, it does not know whether the channel is the console, an email or a file. It only knows how to walk and coordinate. Changing any of the three decisions does not touch its code, and testing it in module 11 will be trivial: you pass it a Consumer that accumulates the texts into an array and check the result, without capturing System.out.
Reminder: all of this becomes even more compact with the Streams API, where
filter(criterion).map(composer).forEach(channel)replaces the whole loop. Streams is lesson 10-04; the previous step, the one you have just taken, is understanding that behaviour is passed as data.
Common Mistakes and Tips
Miscounting the abstract methods. The default, static, private and redeclared public Object methods do not count. That is why Comparator, which declares compare and equals, is still functional.
Putting parentheses in a method reference. Material::getTitle() does not compile. The reference does not invoke: it describes which method to invoke later.
Confusing form 1 with form 3. Class::method means "call the static one" if the method is static, and "call this method on the first argument" if it is an instance method. If you write Material::calculateFine expecting form 3, remember that calculateFine(int) takes a parameter, so the functional interface would need two arguments: the material and the days.
Method reference with fixed arguments. There is no way to write "a reference to calculateFine with 20". For that you need a lambda: m -> m.calculateFine(20).
Swapping andThen and compose. f.andThen(g) runs f first. f.compose(g) runs g first. When in doubt, use andThen.
reversed() in the wrong place. It reverses everything accumulated up to that point in the chain, not only the last criterion.
Inference failures in Comparator.comparing. If the chain does not compile, declare the variable's type (Comparator<Material> c = ...) or use a method reference. It almost always solves the problem.
Invisible autoboxing. Function<Integer,Integer> in a loop of millions of iterations creates millions of objects. Use IntUnaryOperator or ToDoubleFunction<T> when performance matters.
Tip: name your composite predicates. overdue.and(returned.negate()) is correct but opaque. Assign it to a variable called overduePending and the business code reads itself.
Tip: reuse the JDK interfaces. Before writing your own functional interface, check whether Predicate, Function, Consumer or Supplier will do. You get the composition methods for free and any Java programmer understands your signature instantly. Write your own only when the domain name adds clarity (RateRule says more than BiFunction<Material,Integer,Double>) or when the signature fits no standard one.
Exercises
Exercise 1: a catalogue of composite criteria
Starting from the Catalog with find(Predicate<Material>) and count(Predicate<Material>), define five base predicates as public static final constants (available, is a book, short term, high fine at 20 days, title starting with a given letter) and build with them, using and, or and negate, at least four named composite criteria. Show the results over the BiblioTech catalogue.
Exercise 2: the four method references
Write a program using one reference of each of the four forms over the BiblioTech domain:
- Static: converting a string
"20"into anintfor the elapsed days. - Of a specific object: printing a receipt with
ConsoleReceipt. - Of an arbitrary object: extracting a material's title.
- Of a constructor: creating an
Employeefrom a name and an identifier.
Write next to each one its equivalent lambda in a comment.
Exercise 3: a fully parameterised report
Write a method static void report(Material[] materials, Predicate<Material> filter, Comparator<Material> order, Function<Material,String> format, Consumer<String> output) that filters, sorts and displays. Call it three times with different combinations: (a) books only, by title, brief format, to the console; (b) all of them, by fine descending, format with the fine, to the console with a prefix; (c) the short-term ones, by type and title, full format, accumulating into a StringBuilder printed at the end.
Solutions
Solution 1
package com.nexussoftware.bibliotech.service;
import com.nexussoftware.bibliotech.domain.*;
import java.util.function.Predicate;
public class CatalogCriteria {
// ---------- Base predicates: simple, reusable parts ----------
public static final Predicate<Material> AVAILABLE = Material::isAvailable;
public static final Predicate<Material> IS_BOOK = m -> m instanceof Book;
public static final Predicate<Material> SHORT_TERM = m -> m.getLoanDays() <= 7;
public static final Predicate<Material> HIGH_FINE = m -> m.calculateFine(20) > 5.0;
/** Predicate factory: returns a different one depending on the letter. */
public static Predicate<Material> startsWith(char letter) {
return m -> !m.getTitle().isEmpty()
&& Character.toUpperCase(m.getTitle().charAt(0))
== Character.toUpperCase(letter);
}
// ---------- Composite criteria: they read like sentences ----------
public static final Predicate<Material> AVAILABLE_BOOK =
IS_BOOK.and(AVAILABLE);
public static final Predicate<Material> ON_LOAN =
AVAILABLE.negate();
public static final Predicate<Material> URGENT =
HIGH_FINE.or(SHORT_TERM.and(AVAILABLE.negate()));
public static final Predicate<Material> CHEAP_FREE_BOOK =
IS_BOOK.and(HIGH_FINE.negate()).and(AVAILABLE);
public static void main(String[] args) {
Material[] data = {
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)
};
data[0].lend(); // Effective Java goes on loan
data[4].lend(); // and so does the DVD
Catalog catalog = new Catalog(data);
System.out.println("Available books: " + catalog.count(AVAILABLE_BOOK));
System.out.println("On loan: " + catalog.count(ON_LOAN));
System.out.println("Urgent: " + catalog.count(URGENT));
System.out.println("Cheap and free books: " + catalog.count(CHEAP_FREE_BOOK));
System.out.println("Starting with 'R': " + catalog.count(startsWith('R')));
System.out.println("\nAvailable books starting with 'D':");
for (Material m : catalog.find(AVAILABLE_BOOK.and(startsWith('D')))) {
System.out.println(" " + m.describe());
}
}
}Available books: 2 On loan: 2 Urgent: 1 Cheap and free books: 2 Starting with 'R': 2 Available books starting with 'D': Book "Design Patterns" (ref. 978-0000000002) - Erich Gamma, 1994
Three design details. First, AVAILABLE uses a method reference to an arbitrary object (Material::isAvailable), form 3: the material passed to test will be the receiver of the call. Second, startsWith(char) is not a constant but a predicate factory: a method returning behaviour, something that only makes sense now that functions are values. Third, the composite criteria read like business phrases (CHEAP_FREE_BOOK) and each can be tested separately in JUnit without touching the catalogue.
Solution 2
package com.nexussoftware.bibliotech;
import com.nexussoftware.bibliotech.domain.*;
import com.nexussoftware.bibliotech.presentation.ConsoleReceipt;
import java.util.function.*;
public class ReferencesDemo {
/** A functional interface of our own for the two-argument constructor. */
@FunctionalInterface
interface EmployeeCreator {
Employee create(String name, String identifier);
}
public static void main(String[] args) {
// ---- FORM 1: STATIC method ----
// Equivalent lambda: s -> Integer.parseInt(s)
Function<String, Integer> toInt = Integer::parseInt;
int days = toInt.apply("20");
System.out.println("1. Elapsed days read: " + days);
// ---- FORM 4: CONSTRUCTOR (used first because the employee is needed) ----
// Equivalent lambda: (n, id) -> new Employee(n, id)
EmployeeCreator createEmployee = Employee::new;
Employee marta = createEmployee.create("Marta Ruiz", "EMP-001");
System.out.println("4. Employee created: " + marta.getName()
+ " (" + marta.getInitials() + ")");
Book effectiveJava = new Book("Effective Java", "Joshua Bloch",
"978-0000000001", 2018);
Loan loan = new Loan(effectiveJava, marta, 100);
// ---- FORM 3: instance method of an ARBITRARY OBJECT of the type ----
// Equivalent lambda: m -> m.getTitle()
// The argument passed to apply will be the RECEIVER of getTitle()
Function<Material, String> toTitle = Material::getTitle;
System.out.println("3. Title extracted: " + toTitle.apply(effectiveJava));
// ---- FORM 2: instance method of a SPECIFIC OBJECT ----
// Equivalent lambda: l -> receipt.printReceipt(l)
// The 'receipt' object is captured; the argument goes to the method
ConsoleReceipt receipt = new ConsoleReceipt();
Consumer<Loan> print = receipt::printReceipt;
loan.registerReturn(days);
System.out.println("2. Receipt printed via a reference to a specific object:");
print.accept(loan);
}
}1. Elapsed days read: 20 4. Employee created: Marta Ruiz (M.R.) 3. Title extracted: Effective Java 2. Receipt printed via a reference to a specific object: ======================================== RETURN RECEIPT - BIBLIOTECH Reference: LN-0001 Material: Effective Java Employee: Marta Ruiz Days late: 5 days Fine: 1.25 EUR Severity: MINOR ========================================
The key to telling form 2 from form 3 lies in what is to the left of ::: if it is a variable (receipt), the object is fixed and the argument goes to the method; if it is a type name (Material), the argument becomes the object the call is made on.
Solution 3
package com.nexussoftware.bibliotech.service;
import com.nexussoftware.bibliotech.domain.*;
import java.util.Arrays;
import java.util.Comparator;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
public class ParameterisedReport {
/**
* Filters, sorts, formats and delivers. The four decisions are parameters:
* this method knows none of them.
*/
public static void report(Material[] materials,
Predicate<Material> filter,
Comparator<Material> order,
Function<Material, String> format,
Consumer<String> output) {
// 1. Filter (no collections: maximum-size array and a final trim)
Material[] selection = new Material[materials.length];
int n = 0;
for (Material m : materials) {
if (filter.test(m)) { selection[n++] = m; }
}
selection = Arrays.copyOf(selection, n);
// 2. Sort
Arrays.sort(selection, order);
// 3. Format and deliver
for (Material m : selection) {
output.accept(format.apply(m));
}
}
public static void main(String[] args) {
Material[] catalog = {
new Dvd("Refactoring Live", "DVD-0007", 95),
new Book("Effective Java", "Joshua Bloch", "978-0000000001", 2018),
new Magazine("Java Magazine", "REV-2024-03", 42, "Monthly"),
new Book("Design Patterns", "Erich Gamma", "978-0000000002", 1994),
new Book("Refactoring", "Martin Fowler", "978-0000000003", 1999)
};
// ---------- (a) Books only, by title, brief format, to the console ----------
System.out.println("=== (a) Books by title ===");
report(catalog,
m -> m instanceof Book,
Comparator.comparing(Material::getTitle),
Material::getTitle, // reference, form 3
System.out::println); // reference, form 2
// ---------- (b) All of them, by fine descending, with a prefix ----------
System.out.println("=== (b) All by fine descending ===");
report(catalog,
m -> true,
Comparator.comparingDouble((Material m) -> m.calculateFine(20)).reversed(),
m -> String.format("%-24s %6.2f EUR", m.getTitle(), m.calculateFine(20)),
t -> System.out.println(" > " + t));
// ---------- (c) Short term, by type and title, accumulating ----------
System.out.println("=== (c) Short term, accumulated ===");
StringBuilder accumulator = new StringBuilder();
report(catalog,
m -> m.getLoanDays() <= 7,
Comparator.comparing(Material::getType)
.thenComparing(Material::getTitle),
m -> String.format("%s | %s | term %d days | rate %.2f EUR%n",
m.getType(), m.getTitle(),
m.getLoanDays(), m.getDailyRate()),
accumulator::append); // reference, form 2
System.out.print(accumulator);
}
}=== (a) Books by title === Design Patterns Effective Java Refactoring === (b) All by fine descending === > Refactoring Live 8.50 EUR > Java Magazine 1.30 EUR > Effective Java 1.25 EUR > Design Patterns 1.25 EUR > Refactoring 1.25 EUR === (c) Short term, accumulated === DVD | Refactoring Live | term 3 days | rate 0.50 EUR Magazine | Java Magazine | term 7 days | rate 0.10 EUR
The remarkable thing about case (c): the output does not go to the console but to a StringBuilder, and the change cost one method reference (accumulator::append). The report method has not changed and could not tell the difference. That is exactly the property that will make module 11's tests trivial: you swap the Consumer for one that accumulates and check the text, without capturing System.out.
And notice the type annotation in (b): (Material m) -> m.calculateFine(20) carries it because comparingDouble followed by .reversed() does not always infer the type from the argument. In (a) and (c) it is not needed, because the method references (Material::getTitle) carry the type with them.
Conclusion
You now have the complete vocabulary of programming with behaviour in Java. You know that a functional interface is one declaring exactly one abstract method, and that default, static, private and redeclared public Object methods do not count towards that tally — which is why Comparator, declaring compare and equals, is still functional. You always use @FunctionalInterface on your single-method interfaces, knowing that it enables nothing but verifies, and that its value lies in making the error appear in the interface rather than in the twenty places using it with lambdas.
You know the java.util.function catalogue and, better than the list, its logic: Function transforms, Consumer consumes, Supplier produces, Predicate decides; the Bi prefix adds a second argument and Operator is the Function whose input and output are of the same type. And you understand why the dozens of primitive variants exist: to avoid autoboxing, which in a loop of a million iterations means a million objects for the collector to clean up.
You have mastered composition, which is where these pieces stop being curiosities and become design: andThen and compose for chaining functions; and, or and negate for building complex business criteria out of simple, named predicates; and Comparator.comparing().thenComparing().reversed(), which turned that six-line composite comparator with an intermediate condition into one line that reads like a sentence. And you have mastered the four forms of method reference with the criterion for telling them apart: what is to the left of :: — a variable or a type name — determines whether the object is fixed or whether the argument becomes the receiver.
Above all, you have written code that receives behaviour as a parameter: a LoanManager that does not know what an urgent loan is, nor what text is sent, nor through which channel, and which nevertheless does exactly what it is asked on every call. Changing any of the three decisions does not touch its code, and testing it will be trivial. Remember that all of this becomes even more compact with the Streams API, where filter().map().forEach() replaces the whole loop: that is lesson 10-04, and you will reach it with the concept already understood.
One last matter remains outstanding in the module, and it has been dragging on since module 3. classifySeverity still returns the strings "MINOR" and "SEVERE", compared with equals throughout the project; Loan.Incident stores its severity as a String it validates by hand in the constructor; and nothing stops someone writing "severe" in lower case, or "CATASTROPHIC", and the compiler will not say a word until the failure turns up in production. Besides, you have written equals, hashCode and toString by hand in every data class, the same twenty lines three times over. In lesson 04-07, Enums and Records, you close the module with the two tools that solve both things: enums, which turn those strings into a closed set of values with type safety, exhaustiveness in the switch and even behaviour of their own per constant; and records, which generate on their own the constructor, the accessors and the three Object methods that cost you so much effort to write in 03-09.
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
