7.1 Functional Interfaces and Lambda Syntax Rules
Key Takeaways
A functional interface adheres to the Single Abstract Method (SAM) contract; default, static, and public Object methods (such as equals, hashCode, and toString) do not count against the single abstract method total.
The @FunctionalInterface annotation is informative and triggers compile-time verification, but an interface satisfying the SAM rule is functional regardless of whether it is annotated.
Lambda parameter lists require parentheses when zero, two or more, or explicitly typed parameters are specified; parentheses are optional strictly for a single untyped parameter (x -> ...).
The var keyword in lambda parameter lists follows an all-or-nothing rule: it cannot be mixed with explicit types or untyped parameters, requires parentheses, and enables parameter-level annotations.
Lambdas capture local variables only if they are final or effectively final; lambdas share the enclosing lexical scope and cannot declare parameters that shadow enclosing local variables.
7.1 Functional Interfaces and Lambda Syntax Rules
Functional programming in Java builds upon object-oriented foundations by treating executable behavior as data. Introduced in Java 8 and refined in subsequent releases through Java 17, lambda expressions provide concise syntax for instantiating single-method interfaces. For the Oracle Java SE 17 Developer certification (1Z0-829), you must master the structural requirements of functional interfaces, every syntactic variation of lambda expressions, the strict rules governing var parameter declarations, and the mechanics of variable capture and lexical scoping.
The Single Abstract Method (SAM) Contract
A functional interface is defined in the Java Language Specification (JLS §9.8) as an interface that contains exactly one abstract method. This single method establishes the target contract for lambda expressions and method references.
The @FunctionalInterface Annotation
The @FunctionalInterface annotation documents the developer's intent and instructs javac to validate that the annotated interface fulfills the SAM contract at compile time:
@FunctionalInterface
public interface StringTransformer {
String transform(String input); // Single abstract method (SAM)
}
Key rules regarding @FunctionalInterface include:
- Informative, not mandatory: Any interface with exactly one abstract method is a functional interface by definition, even without
@FunctionalInterface. - Compile-time validation: If an interface annotated with
@FunctionalInterfacecontains zero abstract methods or more than one abstract method, the compiler generates a fatal error:Unexpected @FunctionalInterface annotation.
Methods That Do Not Count Against the SAM Total
Interfaces often contain multiple method declarations. The compiler ignores the following categories when tallying abstract methods:
- Default Methods: Methods declared with the
defaultkeyword provide a concrete body and do not count as abstract. - Static Methods: Static interface methods belong to the interface itself, contain a concrete body, and do not count as abstract.
- Private and Private Static Methods: Introduced in Java 9 for helper logic within default methods, private interface methods contain a body and do not count as abstract.
- Public Methods Inherited from
java.lang.Object: An abstract method declared in an interface that matches the signature of any public method injava.lang.Object(equals(Object),hashCode(), ortoString()) does not count against the SAM count. Because every class in Java inherits concrete implementations of these methods fromjava.lang.Object, any concrete implementation of the interface is guaranteed to provide them.
@FunctionalInterface
public interface ComplexValidator<T> {
// 1. The Single Abstract Method
boolean validate(T item);
// 2. Default methods do not affect SAM status
default ComplexValidator<T> invert() {
return item -> !validate(item);
}
// 3. Static helper methods do not affect SAM status
static <T> ComplexValidator<T> acceptAll() {
return item -> true;
}
// 4. Public Object method declarations do not count as abstract
@Override
boolean equals(Object obj);
@Override
String toString();
}
Warning
The Protected Object Method Trap
The exception to the Object method exclusion rule applies strictly to public methods (equals, hashCode, toString). The methods clone() and finalize() are protected in java.lang.Object. If an interface declares Object clone();, it does count as an abstract method because implementing classes do not inherit a public implementation from Object! An interface with void run(); and Object clone(); has two abstract methods and is NOT a functional interface.
Lambda Expression Syntax Rules
A lambda expression consists of three distinct parts: a parameter list, the arrow token (->), and a body.
Parameter List Variations
The syntax for the parameter list depends on the number of parameters and whether their types are explicitly declared or inferred by the compiler:
| Parameter Variant | Syntax Example | Notes & Compiler Rules |
|---|---|---|
| Zero parameters | () -> System.out.println("Done") | Parentheses () are strictly mandatory. |
| Single inferred parameter | s -> s.length() / (s) -> s.length() | Parentheses are optional only when types are omitted and there is exactly one parameter. |
| Single explicitly typed parameter | (String s) -> s.length() | Parentheses are strictly mandatory. String s -> s.length() fails compilation. |
| Multiple inferred parameters | (a, b) -> a + b | Parentheses are strictly mandatory. |
| Multiple typed parameters | (int a, int b) -> a + b | Parentheses are strictly mandatory. All parameters must have explicit types. |
// VALID PARAMETER SYNTAX
Runnable r1 = () -> System.out.println("Running");
Consumer<String> c1 = s -> System.out.println(s);
Consumer<String> c2 = (s) -> System.out.println(s);
Consumer<String> c3 = (String s) -> System.out.println(s);
BiFunction<Integer, Integer, Integer> adder = (a, b) -> a + b;
// COMPILE-TIME ERRORS
// Consumer<String> err1 = String s -> System.out.println(s); // Missing ()
// BiFunction<Integer, Integer, Integer> err2 = (int a, b) -> a + b; // Cannot mix typed & inferred
Expression Body vs. Block Body
A lambda body can be structured as a single expression or a block of statements enclosed in curly braces ({}):
// Expression body: implicit return, no braces, no semicolons
BinaryOperator<Integer> addExpr = (a, b) -> a + b;
// Block body: explicit return required for non-void returns, braces and semicolons mandatory
BinaryOperator<Integer> addBlock = (a, b) -> {
int sum = a + b;
return sum;
};
Exam questions frequently present subtle syntax corruptions in lambda bodies:
(a, b) -> return a + b;Compile Error:returnkeyword is forbidden in an expression body without braces.(a, b) -> { a + b; }Compile Error: Expressiona + bis not a valid standalone statement, and non-void functional interfaces require areturnkeyword inside a block body.(a, b) -> { return a + b }Compile Error: Semicolon is missing after thereturnstatement inside the block body.
Using var in Lambda Parameters
Java 11 introduced the ability to declare formal lambda parameters using the var keyword for local variable type inference:
BiFunction<String, String, String> concat = (var s1, var s2) -> s1 + s2;
When using var in lambda parameter declarations, three strict rules apply:
- All-or-Nothing Rule: You cannot mix
varwith explicitly typed parameters or untyped parameters. - Mandatory Parentheses: Any parameter list declaring
varmust be enclosed in parentheses, even for a single parameter. - Annotation Support: The primary rationale for
varin lambdas is applying type annotations without explicitly writing lengthy generic types.
// VALID
Consumer<String> v1 = (var s) -> System.out.println(s);
BiConsumer<String, Integer> v2 = (var a, var b) -> System.out.println(a + b);
BiConsumer<String, Integer> v3 = (@Deprecated var a, var b) -> System.out.println(a + b);
// COMPILE ERRORS
// Consumer<String> err1 = var s -> System.out.println(s); // Missing parentheses
// BiConsumer<String, Integer> err2 = (var a, Integer b) -> {}; // Cannot mix var with explicit type
// BiConsumer<String, Integer> err3 = (var a, b) -> {}; // Cannot mix var with inferred type
// Consumer<String> err4 = @Deprecated s -> {}; // Cannot annotate inferred parameter
Variable Capture and Lexical Scoping
Lambdas can reference variables defined outside their parameter lists. However, the Java compiler enforces strict scoping and mutability constraints.
The Effectively Final Rule
A local variable declared outside a lambda and referenced within its body must be final or effectively final:
- Effectively final: A variable whose value is initialized once and never modified thereafter anywhere within its enclosing scope.
- If a local variable is modified after its initialization, or if the lambda attempts to modify the variable, compilation fails.
public void processRecords() {
int baseMultiplier = 10; // Effectively final
int runningTotal = 0; // Mutated later -> NOT effectively final
Function<Integer, Integer> calc = x -> x * baseMultiplier; // Compiles cleanly
// IntUnaryOperator accumulator = x -> runningTotal += x; // COMPILE ERROR: cannot mutate local var
runningTotal = 100;
// Function<Integer, Integer> calc2 = x -> x * runningTotal; // COMPILE ERROR: runningTotal is not effectively final
}
Accessing and Mutating Fields
Unlike local method variables, instance fields and static fields are NOT required to be effectively final. A lambda expression can freely read and mutate instance variables of the enclosing class and static variables of any accessible class:
public class Accumulator {
private int total = 0; // Instance field
private static int count = 0; // Static field
public void execute() {
Consumer<Integer> adder = val -> {
this.total += val; // Perfectly legal: mutates instance field
count++; // Perfectly legal: mutates static field
};
adder.accept(50);
}
}
Variable Shadowing and Lexical Scope
A lambda expression does not introduce a new lexical scope. It shares the same scope as the enclosing method block. Consequently:
- A lambda parameter cannot shadow a local variable declared in the enclosing method.
- A lambda parameter can shadow an instance or static field of the enclosing class.
- Within a lambda body,
thisrefers to the instance of the enclosing class, not the lambda itself.
public class ScopeExample {
private String name = "Field";
public void test() {
String localStr = "Local";
// COMPILE ERROR: Variable 'localStr' is already defined in the scope
// Consumer<String> c1 = localStr -> System.out.println(localStr);
// LEGAL: 'name' shadows the instance field 'this.name'
Consumer<String> c2 = name -> {
System.out.println(name); // Refers to lambda parameter
System.out.println(this.name); // Refers to instance field
};
}
}
Warning
this in Lambdas vs. Anonymous Classes
In an anonymous inner class, the this keyword refers to the instance of the anonymous inner class itself. In a lambda expression, this refers to the enclosing instance of the class that lexically surrounds the lambda. If a lambda appears in a static context (such as a public static void main method), using this inside the lambda causes a compile-time error: non-static variable this cannot be referenced from a static context.
Consider the following interface definitions:
interface ContractA {
void execute();
default void reset() {}
static void log() {}
String toString();
boolean equals(Object obj);
}
interface ContractB {
void execute();
Object clone();
}
interface ContractC {
default void execute() {}
}
interface ContractD {
void execute();
void stop();
}
Which of these interfaces qualifies as a valid functional interface that compiles successfully when annotated with @FunctionalInterface?
ContractB and ContractD
ContractA and ContractB
ContractA only
ContractA and ContractC
Which of the following lambda expressions fails to compile due to a syntax violation?
BiFunction<Integer, Integer, Integer> f = (var a, var b) -> a + b;
BiFunction<Integer, Integer, Integer> f = (var a, int b) -> a + b;
Function<String, Integer> f = (String s) -> { return s.length(); };
Function<String, String> f = s -> s.strip();
Consider the following class:
public class Calculator {
private int offset = 10;
public void compute() {
int multiplier = 2;
int adjustment = 5;
java.util.function.IntUnaryOperator op = x -> {
offset++;
return x * multiplier + adjustment;
};
adjustment = 15;
System.out.println(op.applyAsInt(4));
}
}
What is the result of attempting to compile and execute this class?
Compilation fails because offset++ attempts to mutate an instance field inside a lambda.
Compilation fails because multiplier is not explicitly declared with the final keyword.
Compilation fails because adjustment is modified after being referenced in a lambda, violating the effectively final rule.
The code compiles and prints 27.
Consider the following code snippet:
public class ScopeTracker {
int count = 100;
public void runTracker() {
int count = 50;
java.util.function.Consumer<Integer> action = count -> {
System.out.println(count);
};
action.accept(25);
}
}
What is the outcome of compiling and running this code?
Compilation fails: lambda parameter count redeclares a local
The code compiles and prints 50, the local variable
The code compiles and prints 100, the value of the instance field
The code compiles and prints 25, the lambda argument
Sections you finish are checked off in the contents.