Lambda Expressions & Functional Interfaces
java lambda expressions functional interface Predicate Function Consumer Supplier method reference effectively final
Introduction
In the previous lesson, you learned about the Java Streams API, which introduced a declarative and functional way of processing collections.
You may have noticed code like this:
names.stream().filter(name -> name.startsWith("J")).forEach(System.out::println);
What does this mean?
name -> name.startsWith("J")
This is called a Lambda Expression.
Before Java 8, developers had to create anonymous classes for simple behaviors, resulting in verbose code.
Java 8 introduced Lambda Expressions and Functional Interfaces to simplify programming and bring functional programming concepts into Java.
Lambda expressions are widely used in:
- Spring Boot
- Streams API
- Microservices
- CompletableFuture
- Event Handling
- Collections Sorting
- Executors
- Kafka Consumers
- REST APIs
Today, almost every enterprise Java application uses lambda expressions.
In this lesson, you'll learn:
- What is a Lambda Expression?
- Why Lambdas?
- Lambda Syntax
- Functional Interfaces
- @FunctionalInterface
- Built-in Functional Interfaces
- Predicate
- Function
- Consumer
- Supplier
- UnaryOperator
- BinaryOperator
- Method References
- Variable Capture
- Effectively Final Variables
- Real-world Examples
- Best Practices
- Common Mistakes
- Hands-on Exercises
- Professional Interview Questions
What is a Lambda Expression?
A Lambda Expression is a concise way to represent an anonymous function.
It allows you to pass behavior as data.
General Syntax:
(parameters) -> expression
Or
(parameters) -> {// statements}
Example:
(a, b) -> a + b
Meaning:
- Input →
a,b - Output →
a + b
Why Lambda Expressions?
Before Java 8:
Runnable runnable = new Runnable() {@Overridepublic void run() {System.out.println("Running");}};
Java 8:
Runnable runnable =() -> System.out.println("Running");
Much cleaner and easier to read.
Benefits:
- Less Boilerplate
- Better Readability
- Functional Programming
- Easy Parallel Processing
- Better Stream Integration
Lambda Syntax
No Parameters
() -> System.out.println("Hello");
One Parameter
name -> System.out.println(name);
Parentheses are optional for a single parameter.
Multiple Parameters
(a, b) -> a + b;
Multiple Statements
(name) -> {System.out.println(name);return name.length();};
Functional Interface
A Functional Interface contains exactly one abstract method.
Example:
@FunctionalInterfaceinterface Calculator {int add(int a, int b);}
Implementation using Lambda:
Calculator calculator =(a, b) -> a + b;System.out.println(calculator.add(10,20));
Output:
30
@FunctionalInterface Annotation
@FunctionalInterfaceinterface Greeting {void sayHello();}
Benefits:
- Compiler validation
- Prevents multiple abstract methods
- Improves readability
Built-in Functional Interfaces
Java provides several commonly used functional interfaces.
PredicateFunctionConsumerSupplierUnaryOperatorBinaryOperator
These reside in the java.util.function package.
Predicate<T>
Represents a condition.
Method:
boolean test(T value)
Example:
Predicate<Integer> even =number -> number % 2 == 0;System.out.println(even.test(10));
Output:
true
Common Uses:
- Filtering
- Validation
- Business Rules
Function<T,R>
Transforms one value into another.
Method:
R apply(T input)
Example:
Function<String,Integer> length =text -> text.length();System.out.println(length.apply("Java"));
Output:
4
Consumer<T>
Consumes a value without returning anything.
Method:
void accept(T value)
Example:
Consumer<String> printer =System.out::println;printer.accept("Hello");
Output:
Hello
Supplier<T>
Produces data without taking input.
Method:
T get()
Example:
Supplier<Double> random =Math::random;System.out.println(random.get());
UnaryOperator<T>
Accepts and returns the same type.
UnaryOperator<Integer> square =n -> n * n;System.out.println(square.apply(5));
Output:
25
BinaryOperator<T>
Accepts two parameters of the same type and returns the same type.
BinaryOperator<Integer> sum =Integer::sum;System.out.println(sum.apply(20,30));
Output:
50
Method References
Method references provide an even shorter syntax.
Instead of:
names.forEach(name ->System.out.println(name));
Use:
names.forEach(System.out::println);
Types of Method References:
Class::staticMethodobject::instanceMethodClass::instanceMethodClass::new
Constructor Reference
Supplier<List<String>> list =ArrayList::new;
Equivalent to:
() -> new ArrayList<>();
Variable Capture
A lambda can use variables from its surrounding scope.
Example:
String prefix = "Hello";Consumer<String> consumer =name -> System.out.println(prefix + " " + name);
Effectively Final Variables
Captured variables cannot be modified after initialization.
Valid:
int age = 20;Consumer<String> consumer =name -> System.out.println(age);
Invalid:
age++;Consumer<String> consumer =name -> System.out.println(age);
Compilation Error.
Lambda with Streams
List<String> names =List.of("Java","Spring","Kafka");names.stream().filter(name -> name.startsWith("J")).map(String::toUpperCase).forEach(System.out::println);
Output:
JAVA
Real-World Example: Employee Filtering
employees.stream().filter(emp -> emp.getSalary() > 100000).forEach(System.out::println);
Real-World Example: Sorting Products
products.sort((a,b) ->Double.compare(a.getPrice(),b.getPrice()));
Real-World Example: Email Notification
Consumer<String> emailService =email ->System.out.println("Sending Email to " + email);emailService.accept("user@example.com");
Lambda vs Anonymous Class
| Lambda | Anonymous Class |
|---|---|
| Concise | Verbose |
| Functional Interface Only | Any Interface/Class |
| No separate anonymous type | Creates anonymous class |
| Better readability | More boilerplate |
Best Practices
Use Method References
Prefer:
System.out::println
when it improves readability.
Keep Lambdas Short
Complex business logic should be moved to separate methods.
Prefer Built-in Functional Interfaces
Reuse Predicate, Function, Consumer, and Supplier instead of creating custom interfaces unnecessarily.
Avoid Side Effects
Lambda expressions should preferably be stateless and avoid modifying shared variables.
Use Meaningful Parameter Names
Improve readability by choosing descriptive names.
Common Mistakes
Using Lambdas with Non-Functional Interfaces
Lambdas require exactly one abstract method.
Writing Large Lambdas
Large blocks reduce readability.
Modifying Captured Variables
Only effectively final variables can be captured.
Ignoring Method References
Method references often make code simpler.
Creating Custom Interfaces Unnecessarily
Many requirements are already covered by the built-in functional interfaces.
Hands-on Exercise
Create a Java program that:
- Creates a custom functional interface named
Calculator. - Implements it using lambda expressions.
- Uses
Predicateto filter even numbers. - Uses
Functionto convert names to uppercase. - Uses
Consumerto print employee details. - Uses
Supplierto generate random numbers. - Uses
UnaryOperatorto calculate squares. - Uses
BinaryOperatorto calculate sums. - Uses method references with forEach().
- Processes employee objects using Streams and lambda expressions.
Summary
Lambda expressions bring functional programming capabilities to Java by allowing behavior to be passed as data. Combined with functional interfaces, method references, and the Streams API, lambdas enable concise, expressive, and maintainable code. They are foundational to modern Java development and are extensively used in Spring Boot, reactive programming, and enterprise applications.
Key Takeaways
- Lambda expressions represent anonymous functions.
- Functional interfaces contain exactly one abstract method.
- @FunctionalInterface enables compiler validation.
- Predicate performs boolean tests.
- Function transforms data.
- Consumer consumes data without returning a value.
- Supplier provides data without input.
- UnaryOperator and BinaryOperator specialize common transformations.
- Method references improve readability.
- Captured variables must be effectively final.
- Lambdas integrate seamlessly with the Streams API.
Professional Interview Questions
1What is a Lambda Expression in Java?
Professional Answer
A lambda expression is a concise representation of an anonymous function that can be passed as an implementation of a functional interface. It enables functional programming in Java, reduces boilerplate code, and improves readability, particularly when working with the Streams API, event handling, and asynchronous programming.
Follow-up Questions
- Which Java version introduced lambda expressions?
- Can lambdas be used with any interface?
Interview Tip: Remember: Lambda = Anonymous Function + Functional Interface.
2What is a Functional Interface?
Professional Answer
A functional interface is an interface that contains exactly one abstract method. It may also include default, static, or private methods. Functional interfaces serve as the target types for lambda expressions and method references. The @FunctionalInterface annotation helps the compiler enforce this rule.
Follow-up Questions
- Can a functional interface contain default methods?
- Name some built-in functional interfaces.
Interview Tip: Common built-in functional interfaces include: Predicate, Function, Consumer, Supplier.
3What is the difference between Predicate, Function, Consumer, and Supplier?
Professional Answer
Predicate evaluates a condition and returns a boolean value. Function transforms an input into an output. Consumer accepts an input and performs an action without returning a result. Supplier produces and returns a value without accepting any input. These interfaces form the foundation of functional programming in Java and are widely used with the Streams API.
Follow-up Questions
- Which interface is commonly used with filter()?
- Which interface powers map()?
- Which interface is useful for lazy object creation?
Interview Tip: A quick way to remember: Predicate → Test, Function → Transform, Consumer → Consume, Supplier → Supply.