Practice: Lambdas and Functional Interfaces
Five exercises covering what the slides in this module introduced — effectively-final capture, SAM counting, method reference types,
Function.andThen/compose, andPredicateshort-circuiting. Try to answer before opening each explanation.
Exercise 1 — Capturing a loop variable
javaList<Runnable> tasks = new ArrayList<>();
for (int i = 0; i < 3; i++) {
tasks.add(() -> System.out.println("Task " + i));
}Does this compile?
Answer
No — compile error: i is not effectively final.
i is reassigned by i++ on every iteration of the loop, so it's mutated
over its lifetime — the opposite of "assigned exactly once." A lambda can
only capture local variables that are effectively final, so this fails to
compile with a message like "local variables referenced from a lambda
expression must be final or effectively final."
The standard fix is copying the value into a fresh local variable inside the loop body before creating the lambda:
javafor (int i = 0; i < 3; i++) {
int taskNum = i; // a brand-new variable each iteration
tasks.add(() -> System.out.println("Task " + taskNum));
}taskNum is effectively final — even though it's declared inside a
loop that runs multiple times, each iteration gets its own instance of
taskNum, assigned exactly once and never reassigned within that
instance's own scope.
Exercise 2 — Does this satisfy @FunctionalInterface?
java@FunctionalInterface
interface Calculator {
int compute(int a, int b);
default int square(int a) { return compute(a, a); }
static Calculator adder() { return (a, b) -> a + b; }
@Override
String toString();
}Does this compile as a valid functional interface?
Answer
Yes. Only compute(int, int) counts as the single abstract method
(SAM):
squareis adefaultmethod — doesn't count.adderisstatic— doesn't count.toString()is abstract, but it merely re-declares a public method already defined onObject. Every implementation inheritstoString()fromObjectregardless, so it doesn't count toward the SAM total either.
Total abstract methods that matter for the SAM rule: 1
(compute) — valid.
Exercise 3 — Identifying a method reference's type and result
javaBiFunction<String, String, Integer> cmp1 = String::compareTo;
System.out.println(cmp1.apply("apple", "banana"));
String prefix = "Hello";
Function<String, String> cmp2 = prefix::concat;
System.out.println(cmp2.apply(", World"));Which kind of method reference is each one, and what's printed?
Answer
plaintext-1 Hello, World
String::compareTo is an instance method on an arbitrary object of a
particular type (type 3) — the first BiFunction argument becomes the
object the method is called on, and the second becomes compareTo's
parameter. So cmp1.apply("apple", "banana") really runs
"apple".compareTo("banana"). compareTo returns the difference between
the first differing characters' codes — 'a' (97) vs. 'b' (98) — giving
-1.
prefix::concat is an instance method on a particular, already-captured
object (type 2) — it always calls concat on that one specific "Hello"
instance, no matter what. cmp2.apply(", World") runs
"Hello".concat(", World"), giving "Hello, World".
Exercise 4 — andThen vs. compose
javaFunction<Integer, Integer> times2 = n -> n * 2;
Function<Integer, Integer> plus3 = n -> n + 3;
Function<Integer, Integer> a = times2.andThen(plus3);
Function<Integer, Integer> b = times2.compose(plus3);
System.out.println(a.apply(5));
System.out.println(b.apply(5));What's printed on each line?
Answer
plaintext13 16
times2.andThen(plus3) runs times2 first, then feeds its result
into plus3: 5 * 2 = 10, then 10 + 3 = 13.
times2.compose(plus3) runs plus3 first (on the original argument),
then feeds that result into times2: 5 + 3 = 8, then 8 * 2 = 16.
andThen reads left-to-right in the order you called it; compose
reverses it — the function you call compose on runs last, and the
argument to compose runs first.
Exercise 5 — Does Predicate.and() short-circuit?
javaPredicate<String> notNull = s -> s != null;
Predicate<String> isLong = s -> s.length() > 5;
Predicate<String> safe = notNull.and(isLong);
System.out.println(safe.test(null));
System.out.println(safe.test("hi"));Does safe.test(null) throw a NullPointerException?
Answer
No — prints false, no exception.
Predicate.and() short-circuits exactly like &&: it only evaluates the
second predicate if the first one returns true. notNull.test(null)
returns false immediately, so isLong.test(null) — which would throw a
NullPointerException calling .length() on null — is never evaluated
at all.
safe.test("hi") also prints false, but for an ordinary reason this
time: notNull passes ("hi" isn't null), so isLong does run —
"hi".length() is 2, which is not greater than 5.
The general lesson: composing predicates with and/or preserves the
same short-circuit guarantees as &&/||, which is exactly why ordering
a null-check predicate first (as here) is a safe, idiomatic pattern.