Runnable task = new Runnable() {
@Override public void run() {
System.out.println("Running");
}
};Java Functional Interfaces and Lambda Syntax: Predicate, Consumer, Function, Supplier
Direct Answer & Core Concept
Modern Java makes it possible to treat behavior like data. Instead of always creating a separate class and method for a small operation, you can pass behavior directly using lambda expressions.
This programming style became especially important with Java 8, which introduced lambda expressions and the java.util.function package.
Four commonly used functional interfaces are:
Predicate<T>— tests something and returnsbooleanConsumer<T>— accepts something and returns nothingFunction<T, R>— accepts one value and produces anotherSupplier<T>— produces a value without accepting an input
Understanding these interfaces is essential for working with the Stream API, collections, callbacks, filtering, mapping, and modern Java code.
Detailed Explanation & Mechanics
What Is a Lambda Expression?
A lambda expression provides an implementation of a functional interface without requiring a traditional anonymous class.
Traditional anonymous class:
Lambda version:
Runnable task =() -> {
System.out.println("Running");
};For a single statement, it can be shorter:
Runnable task =() -> System.out.println("Running");The lambda represents behavior that can be passed around and executed later.
Basic Lambda Syntax
The general syntax is:
(parameters) -> expressionor:
(parameters) -> {
statements;
}For example:
(int a, int b) -> a + bThe three important parts are:
(int a, int b) -> a + b parameters bodyLambda With One Parameter
Parentheses can usually be omitted when there is a single implicitly typed parameter.
x -> x * 2Instead of:
(x) -> x * 2Both represent the same basic lambda structure.
Lambda With Multiple Parameters
Multiple parameters require parentheses:
(a, b) -> a + bYou can also explicitly declare the parameter types:
(int a, int b) -> a + bYou generally should not mix explicit and implicit parameter types:
// Incorrect
(int a, b) -> a + bLambda With No Parameters
When there are no parameters, use empty parentheses:
() -> System.out.println("Hello");This is commonly seen with Runnable:
Runnable task =() -> System.out.println("Running");
task.run();Lambda With Multiple Statements
If the lambda contains multiple statements, use braces:
(a, b) -> {
int result = a + b;
System.out.println(result);
return result;
}When a block lambda returns a value, an explicit return is normally required.
Lambda Expression and Target Type
A lambda does not have a useful standalone interface type by itself.
Its type is determined by the context in which it is used.
For example:
Predicate<Integer> positive = x -> x > 0;Here the target type is:
Predicate<Integer>Therefore Java knows that x is an Integer and the lambda must return a boolean.
The java.util.function Package
Java provides many standard functional interfaces in:
java.util.functionThe four most important basic interfaces are:
| Interface | Input | Output | Main Method |
|---|---|---|---|
Predicate<T> | T | boolean | test() |
Consumer<T> | T | void | accept() |
Function<T,R> | T | R | apply() |
Supplier<T> | None | T | get() |
A simple way to remember them is:
Predicate → asks a questionConsumer → consumes a valueFunction → transforms a valueSupplier → supplies a valuePredicate<T>
Predicate<T> represents a condition or test.
Its main abstract method is:
boolean test(T value)Example:
Predicate<Integer> isEven = number -> number % 2 == 0;
System.out.println(isEven.test(10));
System.out.println(isEven.test(7));Output:
truefalseA predicate always answers a yes/no-style question.
Predicate Example With Strings
Predicate<String> isLong = text -> text.length() > 5;
System.out.println(isLong.test("Java"));
System.out.println(isLong.test("Programming"));Output:
falsetrueThis kind of logic is commonly used when filtering collections.
Predicate With Stream API
For example:
List<Integer> numbers = List.of(10, 15, 20, 25, 30);
numbers.stream() .filter(n -> n > 20) .forEach(System.out::println);Output:
2530The lambda passed to filter() behaves like a predicate.
Conceptually:
number↓Predicate↓true / false↓keep or rejectCombining Predicates
Predicates provide useful default methods such as:
and()or()negate()
Example:
Predicate<Integer> greaterThan10 = n -> n > 10;
Predicate<Integer> even = n -> n % 2 == 0;
Predicate<Integer> condition = greaterThan10.and(even);
System.out.println(condition.test(20));Output:
trueThe number must satisfy both conditions.
Predicate OR
Predicate<Integer> small = n -> n < 10;
Predicate<Integer> large = n -> n > 100;
Predicate<Integer> condition = small.or(large);The condition is true when either predicate is true.
Predicate Negation
Use negate() to reverse a predicate:
Predicate<Integer> even = n -> n % 2 == 0;
Predicate<Integer> odd = even.negate();
System.out.println(odd.test(7));Output:
trueConsumer<T>
Consumer<T> represents an operation that accepts a value but does not return a result.
Its main method is:
void accept(T value)Example:
Consumer<String> printer = text -> System.out.println(text);
printer.accept("Hello Java");Output:
Hello JavaThink of Consumer as:
Input → ActionThere is no returned value.
Consumer Example
Consumer<Integer> display = number -> System.out.println("Number: " + number);
display.accept(100);Output:
Number: 100This is useful for printing, logging, sending notifications, updating state, or performing another action.
Consumer With forEach
One of the most common uses of a Consumer is forEach().
List<String> names = List.of("Amit", "Riya", "Rahul");
names.forEach(name -> System.out.println(name));The lambda receives each element and performs an action.
Method reference syntax can make this shorter:
names.forEach(System.out::println);Combining Consumers
Consumers provide an andThen() method.
Consumer<String> first = text -> System.out.println("First: " + text);
Consumer<String> second = text -> System.out.println("Second: " + text);
Consumer<String> combined = first.andThen(second);
combined.accept("Java");Output:
First: JavaSecond: JavaThe first consumer executes before the second.
Function<T, R>
Function<T, R> represents a transformation.
It accepts one value of type T and returns a value of type R.
Its main method is:
R apply(T value)Example:
Function<String, Integer> length = text -> text.length();
System.out.println(length.apply("Java"));Output:
4Think of Function as:
Input → Transformation → OutputFunction Example
Convert a string to uppercase:
Function<String, String> upper = text -> text.toUpperCase();
System.out.println(upper.apply("java"));Output:
JAVAThe input and output types can be different.
Function<String, Integer> length = text -> text.length();Here:
String → IntegerFunction With Stream map()
The map() operation in streams commonly uses function-like behavior.
List<String> names = List.of("Amit", "Riya", "Rahul");
List<Integer> lengths = names.stream() .map(name -> name.length()) .toList();
System.out.println(lengths);Output:
[4, 4, 5]The lambda transforms every name into its length.
Conceptually:
"Amit" → 4"Riya" → 4"Rahul" → 5Function Composition
Functions can be combined using methods such as:
andThen()compose()
Example:
Function<Integer, Integer> doubleValue = x -> x * 2;
Function<Integer, Integer> addTen = x -> x + 10;
Function<Integer, Integer> result = doubleValue.andThen(addTen);
System.out.println(result.apply(5));Calculation:
5 × 2 = 1010 + 10 = 20Output:
20Supplier<T>
Supplier<T> represents something that produces a value without receiving an input.
Its main method is:
T get()Example:
Supplier<String> message =() -> "Hello Java";
System.out.println(message.get());Output:
Hello JavaThink of a supplier as:
No input → Produce a valueSupplier Example With Current Time
Supplier<LocalDateTime> now =() -> LocalDateTime.now();
System.out.println(now.get());Every call to get() can produce a new value.
This is one reason suppliers are useful for lazy or deferred value creation.
Supplier for Default Values
A supplier can be used when a value should be created only when required.
For example:
Supplier<List<String>> listSupplier =() -> new ArrayList<>();
List<String> list = listSupplier.get();The supplier describes how to create the value.
Predicate vs Consumer vs Function vs Supplier
The easiest comparison is:
PredicateInput → Question → booleanConsumerInput → Action → nothingFunctionInput → Transform → outputSupplierNothing → Produce → outputExample:
Predicate<Integer> p = n -> n > 10;
Consumer<Integer> c = n -> System.out.println(n);
Function<Integer, String> f = n -> "Number: " + n;
Supplier<Integer> s =() -> 100;Practical Example Using All Four
Consider a list of employee salaries.
List<Integer> salaries = List.of(25000, 45000, 60000, 80000);Predicate — Filter
Predicate<Integer> highSalary = salary -> salary >= 50000;Function — Transform
Function<Integer, Integer> bonus = salary -> salary + 5000;Consumer — Display
Consumer<Integer> print = salary -> System.out.println("Salary: " + salary);Supplier — Generate Default Salary
Supplier<Integer> defaultSalary =() -> 30000;These four interfaces solve different kinds of problems.
Combining Them With Streams
List<Integer> salaries = List.of(25000, 45000, 60000, 80000);
salaries.stream() .filter(salary -> salary >= 50000) .map(salary -> salary + 5000) .forEach(salary -> System.out.println("Updated: " + salary));Output:
Updated: 65000Updated: 85000Conceptually:
List↓Predicate↓Filter↓Function↓Transform↓Consumer↓ActionThis pattern is one of the most important foundations of modern Java programming.
Custom Functional Interface
You are not limited to the interfaces provided by Java.
You can create your own:
@FunctionalInterfaceinterface StringProcessor {
String process(String text);
}Use it with a lambda:
StringProcessor processor = text -> text.toUpperCase();
System.out.println( processor.process("java"));Output:
JAVAA custom functional interface is useful when the operation has domain-specific meaning.
Functional Interface With @FunctionalInterface
The annotation:
@FunctionalInterfacetells the compiler that the interface is intended to have exactly one abstract method.
For example:
@FunctionalInterfaceinterface Calculator {
int calculate(int a, int b);
}If another abstract method is added:
@FunctionalInterfaceinterface Calculator {
int calculate(int a, int b);
int multiply(int a, int b);
}the compiler reports an error because the interface is no longer functional.
Default and Static Methods in Functional Interfaces
A functional interface can still contain default and static methods.
For example:
@FunctionalInterfaceinterface Printer {
void print(String text);
default void printTwice(String text) {
print(text);
print(text);
} static void info() {
System.out.println("Printer interface");
}
}The important rule is that it must still have only one abstract method.
Primitive Functional Interfaces
The standard interfaces such as:
Function<Integer, Integer>use wrapper types because Java generics work with reference types rather than primitive type arguments.
For performance-sensitive code, Java provides primitive-specialized interfaces.
Examples include:
IntPredicateIntConsumerIntFunction<R>IntSupplierThere are also variants for long and double.
Example:
IntPredicate even = number -> number % 2 == 0;
System.out.println(even.test(10));These can avoid some boxing and unboxing overhead.
Bi-Functional Interfaces
Java also provides interfaces that work with two inputs.
For example:
BiFunction<T, U, R>Example:
BiFunction<Integer, Integer, Integer> add =(a, b) -> a + b;
System.out.println(add.apply(10, 20));Output:
30Other useful interfaces include:
BiPredicate<T, U>BiConsumer<T, U>UnaryOperator and BinaryOperator
Java provides specialized functional interfaces for operations where input and output types are the same.
UnaryOperator
UnaryOperator<Integer> square = n -> n * n;
System.out.println(square.apply(5));Output:
25It is conceptually:
T → TBinaryOperator
BinaryOperator<Integer> add =(a, b) -> a + b;
System.out.println(add.apply(10, 20));Output:
30It is conceptually:
T + T → TLambda Variable Scope
A lambda can access local variables when they are final or effectively final.
String prefix = "Hello";
Consumer<String> printer = name -> System.out.println(prefix + " " + name);
printer.accept("Tanmoy");Output:
Hello TanmoyBut changing the local variable after it is captured is not allowed:
String prefix = "Hello";
Consumer<String> printer = name -> System.out.println(prefix + name);
// prefix = "Hi"; // ErrorThe same effectively-final concept appears with local and anonymous classes.
Lambda and this
A lambda does not create a new this context in the same way an anonymous class does.
For example:
class Person {
private String name = "Tanmoy";
void show() {
Runnable task =() -> System.out.println(this.name);
task.run();
}
}Inside the lambda:
this.namerefers to the enclosing Person instance.
This differs from an anonymous inner class, where this refers to the anonymous class instance.
Method References
Java also provides method references as a shorter form for certain lambdas.
Instead of:
names.forEach(name -> System.out.println(name));you can write:
names.forEach(System.out::println);Common forms include:
object::instanceMethodClassName::staticMethodClassName::instanceMethodClassName::newExample:
Function<String, Integer> length = String::length;This is equivalent in behavior to:
Function<String, Integer> length = text -> text.length();Lambda vs Anonymous Class
Anonymous class:
Runnable task = new Runnable() {
@Override public void run() {
System.out.println("Running");
}
};Lambda:
Runnable task =() -> System.out.println("Running");For a functional interface, the lambda is usually much more concise.
However, anonymous classes can still be useful when you need a more explicit class body, additional state, or behavior that does not fit naturally into a lambda.
Common Mistakes
Mistake 1: Forgetting the Functional Interface
This does not work:
// What type is this?
x -> x * 2;A lambda needs a target functional-interface type or another supported target context.
For example:
Function<Integer, Integer> doubleValue = x -> x * 2;Mistake 2: Returning a Value From Consumer
This is incorrect:
Consumer<Integer> consumer = x -> x * 2;A Consumer returns void.
Use Function if you need a result:
Function<Integer, Integer> function = x -> x * 2;Mistake 3: Returning Boolean From the Wrong Interface
If the main purpose is a condition:
Predicate<Integer> positive = x -> x > 0;is clearer than using a general Function<Integer, Boolean>.
Mistake 4: Passing an Argument to Supplier
This is incorrect:
Supplier<Integer> supplier = x -> x * 2;A supplier accepts no input.
Correct:
Supplier<Integer> supplier =() -> 100;Mistake 5: Using Function When a More Specific Interface Exists
This works:
Function<Integer, Boolean> even = n -> n % 2 == 0;But this communicates the intention better:
Predicate<Integer> even = n -> n % 2 == 0;Choosing the appropriate functional interface makes code easier to understand.
Best Practices
Use Predicate for Conditions
If the result is fundamentally true or false, prefer Predicate.
Use Consumer for Side Effects
Printing, logging, updating an object, or sending an event are common consumer operations.
Use Function for Transformations
Use it when one value needs to become another value.
Use Supplier for Value Creation
Use it when the caller requests a value but does not provide an input.
Keep Lambdas Short
A lambda containing many lines can become difficult to read.
If the logic becomes substantial, consider extracting it into a named method or class.
Prefer Method References When They Improve Readability
For example:
names.forEach(System.out::println);can be clearer than:
names.forEach(name -> System.out.println(name));Avoid Unnecessary Boxing
For performance-sensitive numeric operations, consider primitive-specialized interfaces such as IntPredicate and IntConsumer.
Quick Cheat Sheet
Predicate<T> T → boolean test()Consumer<T> T → void accept()Function<T, R> T → R apply()Supplier<T>() → T get()Additional interfaces:
BiPredicate<T, U>BiConsumer<T, U>BiFunction<T, U, R>UnaryOperator<T>BinaryOperator<T>IntPredicateIntConsumerIntFunction<R>IntSupplierFrequently Asked Questions
What is a functional interface in Java?
A functional interface is an interface with exactly one abstract method. It can be implemented using a lambda expression or method reference.
What is the purpose of @FunctionalInterface?
It tells the compiler that the interface is intended to be functional. The compiler can detect accidental addition of another abstract method.
What is Predicate used for?
Predicate<T> is used for conditions that return boolean.
Predicate<Integer> even = n -> n % 2 == 0;What is Consumer used for?
Consumer<T> accepts a value and performs an action without returning a result.
Consumer<String> print = text -> System.out.println(text);What is Function used for?
Function<T,R> transforms an input of type T into an output of type R.
Function<String, Integer> length = text -> text.length();What is Supplier used for?
Supplier<T> produces a value without receiving an input.
Supplier<Double> random =() -> Math.random();Can a functional interface have default methods?
Yes. It can have default and static methods as long as it still has only one abstract method.
Can a lambda have multiple statements?
Yes.
x -> {
int result = x * 2;
return result;
}Can lambda expressions modify local variables?
A lambda can capture local variables only when they are final or effectively final. It cannot modify a captured local variable in a way that breaks that requirement.
Are Predicate, Consumer, Function, and Supplier interfaces?
Yes. They are standard functional interfaces provided in the java.util.function package.
What is the difference between Function and UnaryOperator?
Function<T,R> can have different input and output types.
T → RUnaryOperator<T> requires the same type:
T → TWhat is the difference between Supplier and Consumer?
A supplier produces a value without input:
() → TA consumer receives a value and performs an action:
T → voidGraphics and Design Ideas
Graphic 1 — Four Core Functional Interfaces
java.util.function
│
┌─────────────┼─────────────┐
↓
↓
↓
Predicate Consumer Function
│ │ │
↓
↓
↓
T → boolean T → void T → R
│
│
Supplier
↓
() → TThis is the most important visual for the page.
Graphic 2 — Easy Memory Trick
PREDICATE"Is it valid?"↓true / falseCONSUMER"Do something with it"↓voidFUNCTION"Change it"↓new valueSUPPLIER"Give me one"↓new valueThis visual is ideal for beginners.
Graphic 3 — Stream Pipeline
List<Integer>
│
↓
┌───────────┐
│ Predicate │
│ filter │
└─────┬─────┘
↓
Filtered
values
│
↓
┌───────────┐
│ Function │
│ map │
└─────┬─────┘
↓
Transformed
values
│
↓
┌───────────┐
│ Consumer │
│ forEach │
└───────────┘Graphic 4 — Lambda Anatomy
(a, b) -> a + b
│ │
│ └── Lambda body
│
└── ParametersShow three examples:
() -> "Hello"x -> x * 2(a, b) -> a + bGraphic 5 — Interface Comparison Table
┌───────────┬───────────────┬────────────┐
│ Interface │ Input │ Output │
├───────────┼───────────────┼────────────┤
│ Predicate │ T │ boolean │
│ Consumer │ T │ void │
│ Function │ T │ R │
│ Supplier │ none │ T │
└───────────┴───────────────┴────────────┘A clean infographic like this can work well near the beginning of the article as a quick reference.
Runnable Code Example: Java Functional Interfaces and Lambda Syntax
Java 17+import java.util.function.*;
public class FunctionalInterfaceDemo {
public static void main(String[] args) {
Predicate<String> isLong = s -> s.length() > 5;
Function<String, Integer> getLength = String::length;
Consumer<String> printer = s -> System.out.println("Consumed: " + s);
Supplier<Double> randomSupplier = Math::random;
String word = "Enterprise";
if (isLong.test(word)) {
printer.accept("Length is " + getLength.apply(word));
}
}
}