Skip to main content

Chapter 8 โ€” Lambdas & Functional Interfaces

Exam Domain: Working with Streams and Lambda Expressions

Key Topics: Lambda expression syntax, deferred execution, Functional Interfaces, SAM (Single Abstract Method) rule, @FunctionalInterface, built-in functional interfaces, primitive-specific functional interfaces, method references, constructor references, variable scopes (final and effectively final capture), and composition convenience methods.


๐ŸŸฆ New Learner: Lambdas, SAM, & Syntax Optionsโ€‹

What is a Lambda Expression?โ€‹

A lambda expression is a block of code passed around using a deferred execution model. It acts like an unnamed method inside an anonymous class, focusing on behaviors and expressions rather than object state.

  • SAM (Single Abstract Method) Rule: A lambda expression implements a functional interface โ€” an interface that contains exactly one abstract method.
  • Context Inference: Java uses surrounding context (variable declarations, method arguments) to infer the types of lambda parameters and the return type.
// Example: Animal record and trait checking interface
public record Animal(String species, boolean canHop, boolean canSwim) {}

@FunctionalInterface
public interface CheckTrait {
boolean test(Animal a);
}

// In client code:
List<Animal> animals = List.of(new Animal("rabbit", true, false));
// We pass a lambda block matching (Animal a) -> boolean
print(animals, a -> a.canHop());

Lambda Syntax Rulesโ€‹

A lambda expression consists of three parts: parameters, the arrow operator (->), and a body.

a -> a.canHop() // โœ… Shortest form (1 inferred parameter, single expression)
(Animal a) -> a.canHop() // โœ… Parentheses required for explicit type
(a, b) -> a.canHop() // โœ… Parentheses required for multiple parameters
(var a) -> a.canHop() // โœ… var parameter allowed (parentheses required)
a -> { return a.canHop(); } // โœ… Block body requires braces, return keyword, and semicolon
() -> true // โœ… Parentheses required for zero parameters

[!WARNING] Parentheses are optional only when there is a single parameter and the type is inferred (not explicitly declared).

// โŒ INVALID SYNTAX EXAMPLES
var invalid = (Animal a) -> a.canHop(); // โŒ DOES NOT COMPILE (var cannot infer type from a lambda directly without target context)
a, b -> a.canHop() // โŒ DOES NOT COMPILE (Missing parentheses for multiple parameters)
a -> { a.canHop(); } // โŒ DOES NOT COMPILE (Block body must return boolean; missing return keyword)
(Animal a) -> { return a.canHop() } // โŒ DOES NOT COMPILE (Missing semicolon inside braces)

๐ŸŸฃ Senior Deep Dive: Object Methods, Primitive Interfaces, Method References, & Scopesโ€‹

Object Methods Exceptionโ€‹

An interface is still a functional interface if it declares abstract methods that match public methods in java.lang.Object. These do not count toward the Single Abstract Method (SAM) count.

  • Reasoning: Since all classes implicitly inherit from Object, any implementation of the interface will always have concrete implementations of these methods.
  • Object signatures to check: public String toString(), public boolean equals(Object), and public int hashCode().
@FunctionalInterface
public interface Dive {
String toString(); // Extracted from Object (ignored in SAM count)
boolean equals(Object o); // Extracted from Object (ignored in SAM count)
int hashCode(); // Extracted from Object (ignored in SAM count)
void dive(); // โœ… The SINGLE abstract method (SAM)
}

[!IMPORTANT] The signature must match the Object signature exactly. If the parameter type differs, it is counted as a new abstract method.

@FunctionalInterface
public interface Hibernate {
boolean equals(Hibernate h); // โŒ Declares equals(Hibernate) instead of equals(Object)
void rest(); // Counted as a second abstract method -> NOT a functional interface!
}

Built-in Functional Interfaces Referenceโ€‹

InterfaceMethodInputsReturn TypeUse Case
Supplier<T>T get()0TSupplying or generating values lazily
Consumer<T>void accept(T t)1 (T)voidPerforming actions on a value (printing, saving)
BiConsumer<T, U>void accept(T t, U u)2 (T, U)voidPerforming actions on two values
Predicate<T>boolean test(T t)1 (T)booleanTesting conditions/filtering
BiPredicate<T, U>boolean test(T t, U u)2 (T, U)booleanTesting conditions on two inputs
Function<T, R>R apply(T t)1 (T)RTransforming an input into another type
BiFunction<T, U, R>R apply(T t, U u)2 (T, U)RTransforming two inputs into another type
UnaryOperator<T>T apply(T t)1 (T)TTransforming a value into the same type
BinaryOperator<T>T apply(T t1, T t2)2 (T, T)TMerging two values of the same type into one

Primitive Functional Interfacesโ€‹

To prevent performance degradation from autoboxing/unboxing wrapper classes (like Double, Integer, Long), Java provides primitive-specific functional interfaces.

1. Boolean Variantโ€‹

  • BooleanSupplier defines boolean getAsBoolean().

2. Double, Int, and Long Variantsโ€‹

These interfaces omit generic parameter declarations when the primitive type is explicitly named.

Generic Shapedouble Equivalentint Equivalentlong EquivalentAbstract Method
Supplier<T>DoubleSupplierIntSupplierLongSuppliergetAsXXX()
Consumer<T>DoubleConsumerIntConsumerLongConsumeraccept()
Predicate<T>DoublePredicateIntPredicateLongPredicatetest()
Function<T, R>DoubleFunction<R>IntFunction<R>LongFunction<R>apply()
UnaryOperator<T>DoubleUnaryOperatorIntUnaryOperatorLongUnaryOperatorapplyAsXXX()
BinaryOperator<T>DoubleBinaryOperatorIntBinaryOperatorLongBinaryOperatorapplyAsXXX()

3. Cross-Type Conversion and Mixed Interfacesโ€‹

  • To-Primitive Functions: ToDoubleFunction<T>, ToIntFunction<T>, ToLongFunction<T>.
  • Bi-To-Primitive Functions: ToDoubleBiFunction<T,U>, ToIntBiFunction<T,U>, ToLongBiFunction<T,U>.
  • Primitive-to-Primitive Functions: DoubleToIntFunction, DoubleToLongFunction, IntToDoubleFunction, IntToLongFunction, LongToDoubleFunction, LongToIntFunction.
  • Object-Primitive Consumers: ObjDoubleConsumer<T>, ObjIntConsumer<T>, ObjLongConsumer<T> (declares accept(T t, primitive value)).

Four Formats of Method Referencesโ€‹

Method references (::) provide a shorthand notation for lambda expressions that only call a single method.

Arity & Parameter Mappingโ€‹

  • Static Methods: Class::staticMethod maps parameters directly to the method arguments.
  • Instance Methods on specific object: instance::method captures the instance reference and maps the lambda parameters directly to method arguments.
  • Instance Methods on arbitrary parameter: Class::instanceMethod uses the first parameter of the lambda as the object instance on which the method is called, and remaining parameters (if any) as arguments.
  • Constructor References: Class::new maps parameters to the matching class constructor.
// Specific instance method reference (captures 'str')
String str = "Zoo";
Predicate<String> lambda1 = s -> str.startsWith(s);
Predicate<String> methodRef1 = str::startsWith; // โœ… Same arity

// Arbitrary instance method reference (uses first parameter as target)
BiPredicate<String, String> lambda2 = (s, p) -> s.startsWith(p);
BiPredicate<String, String> methodRef2 = String::startsWith; // โœ… First param is target, second is argument

Variable Capture Rulesโ€‹

Lambda expressions can reference variables from the enclosing scope only under strict rules:

Scope of VariableAccess from Lambda Body
Instance VariableAlways allowed (can read/write)
Static Class VariableAlways allowed (can read/write)
Local VariableAllowed only if marked final or is effectively final
Method ParameterAllowed only if marked final or is effectively final
Lambda ParameterAlways allowed

[!NOTE] A variable is effectively final if its value is never changed after it is initialized. If you can add the final keyword without causing compilation errors, the variable is effectively final.

public class Crow {
private String color;
public void caw(String name) {
String volume = "loudly";

name = "Caty"; // โŒ name is reassigned, no longer effectively final!

Consumer<String> consumer = s -> {
// System.out.println(name); // โŒ DOES NOT COMPILE (name not effectively final)
System.out.println(volume); // โœ… Compiles (volume is effectively final)
};

// volume = "softly"; // If uncommented, it breaks 'volume' capture above!
}
}

Convenience Methods on FIsโ€‹

These methods chain, compose, or negate functional interfaces:

Predicate<String> egg = s -> s.contains("egg");
Predicate<String> brown = s -> s.contains("brown");

Predicate<String> brownEggs = egg.and(brown); // โœ… Combined check
Predicate<String> nonBrownEggs = egg.and(brown.negate()); // โœ… Negated check
  • Function Composition (andThen vs compose):
    • f.andThen(g) runs f first, then passes the result to g.
    • f.compose(g) runs g first, then passes the result to f.
Function<Integer, Integer> addOne = x -> x + 1;
Function<Integer, Integer> multiplyTwo = x -> x * 2;

System.out.println(addOne.andThen(multiplyTwo).apply(3)); // (3+1)*2 = 8
System.out.println(addOne.compose(multiplyTwo).apply(3)); // (3*2)+1 = 7

๐Ÿšจ Top 10 Exam Trapsโ€‹

Trap 1: redeclaring lambda parametersโ€‹

You cannot declare a parameter in a lambda body with the same name as a local variable in the enclosing method.

public void test(int x) {
// Predicate<Integer> p = x -> x > 5; // โŒ DOES NOT COMPILE (x is already defined in scope)
}

Trap 2: Mixing implicit and explicit parameter typesโ€‹

You cannot mix inferred types, explicit types, or var in the same lambda parameter list.

// (var x, y) -> x + y; // โŒ DOES NOT COMPILE
// (String x, var y) -> x + y; // โŒ DOES NOT COMPILE
(var x, var y) -> x + y; // โœ… Correct

Trap 3: Missing lambda assignment semicolonโ€‹

A lambda statement assigning to a variable must end in a semicolon.

Predicate<String> p = s -> s.isEmpty(); // โœ… Semicolon required

Trap 4: Throwing checked exceptions in Lambdasโ€‹

If a lambda body throws a checked exception, the functional interface's abstract method must declare that exception.

// Runnable r = () -> Thread.sleep(100); // โŒ DOES NOT COMPILE (InterruptedException is checked)
Callable<Void> c = () -> { Thread.sleep(100); return null; }; // โœ… Compiles (Callable throws Exception)

Trap 5: Modifying local variables inside Lambdasโ€‹

Lambdas cannot modify local variables captured from the enclosing context.

int count = 0;
// Runnable r = () -> count++; // โŒ DOES NOT COMPILE (attempts to modify local variable)

Trap 6: Calling negate() directly with ! operatorโ€‹

The logical negation operator ! cannot be applied directly to a Predicate object reference.

Predicate<String> p = String::isEmpty;
// Predicate<String> bad = !p; // โŒ DOES NOT COMPILE
Predicate<String> good = p.negate(); // โœ… Correct

Trap 7: Missing return keywords inside Bracesโ€‹

When braces are used in a lambda body, a return keyword is mandatory if the method returns a value.

// Function<String, Integer> f = s -> { s.length(); }; // โŒ DOES NOT COMPILE
Function<String, Integer> f = s -> { return s.length(); }; // โœ… Correct

Trap 8: Implicitly returning a value inside Bracesโ€‹

Conversely, you cannot return a value in a single-expression lambda if there are no braces.

// Function<String, Integer> f = s -> return s.length(); // โŒ DOES NOT COMPILE

Trap 9: Wrong arity in method referencesโ€‹

Ensure the parameters of the functional interface match the parameters expected by the method reference.

// Supplier<String> s = String::concat; // โŒ DOES NOT COMPILE (concat needs a target and a parameter)
BiFunction<String, String, String> s = String::concat; // โœ… Correct

Trap 10: Re-assigning instance parameters inside loopsโ€‹

Using loop indices inside lambdas violates the effectively final rule.

for (int i = 0; i < 3; i++) {
// Supplier<Integer> s = () -> i; // โŒ DOES NOT COMPILE (i is modified)
}

๐Ÿ”— Spring / Enterprise Relevanceโ€‹

  • Dynamic Specifications: Spring Data JPA Specification<T> allows combining database predicates dynamically using and(), or(), and not() methods.
  • Spring Boot Task Scheduling: TaskScheduler and @Scheduled setups execute tasks asynchronously using Runnable lambdas internally.
  • Reactive Programming: Spring WebFlux (Project Reactor) uses Function and Consumer heavily within map(), flatMap(), and subscribe() operators.