Topic 117 of 119 14. Additional Java Topics Interactive Tutorial

Java Functional Interfaces and Lambda Syntax: Predicate, Consumer, Function, Supplier

9 mins read Last updated: 22 Aug 2026 17,241 views

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 returns boolean

  • Consumer<T> — accepts something and returns nothing

  • Function<T, R> — accepts one value and produces another

  • Supplier<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:

Main.java
Run Online
Runnable task = new Runnable() {
    @Override public void run() {
        System.out.println("Running");
    }
};

Lambda version:

Main.java
Run Online
Runnable task =() -> {
    System.out.println("Running");
};

For a single statement, it can be shorter:

Main.java
Run Online
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:

Main.java
Run Online
(parameters) -> expression

or:

Main.java
Run Online
(parameters) -> {
    statements;
}

For example:

The three important parts are:

Main.java
Run Online
(int a, int b) -> a + b parameters body

Lambda With One Parameter

Parentheses can usually be omitted when there is a single implicitly typed parameter.

Instead of:

Both represent the same basic lambda structure.


Lambda With Multiple Parameters

Multiple parameters require parentheses:

You can also explicitly declare the parameter types:

You generally should not mix explicit and implicit parameter types:

Main.java
Run Online
// Incorrect
(int a, b) -> a + b

Lambda With No Parameters

When there are no parameters, use empty parentheses:

Main.java
Run Online
() -> System.out.println("Hello");

This is commonly seen with Runnable:

Main.java
Run Online
Runnable task =() -> System.out.println("Running");
task.run();

Lambda With Multiple Statements

If the lambda contains multiple statements, use braces:

Main.java
Run Online
(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:

Main.java
Run Online
Predicate<Integer> positive = x -> x > 0;

Here the target type is:

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:

The four most important basic interfaces are:

InterfaceInputOutputMain Method
Predicate<T>Tbooleantest()
Consumer<T>Tvoidaccept()
Function<T,R>TRapply()
Supplier<T>NoneTget()

A simple way to remember them is:

Main.java
Run Online
Predicate → asks a questionConsumer → consumes a valueFunction → transforms a valueSupplier → supplies a value

Predicate<T>

Predicate<T> represents a condition or test.

Its main abstract method is:

Example:

Main.java
Run Online
Predicate<Integer> isEven = number -> number % 2 == 0;
System.out.println(isEven.test(10));
System.out.println(isEven.test(7));

Output:

A predicate always answers a yes/no-style question.


Predicate Example With Strings

Main.java
Run Online
Predicate<String> isLong = text -> text.length() > 5;
System.out.println(isLong.test("Java"));
System.out.println(isLong.test("Programming"));

Output:

This kind of logic is commonly used when filtering collections.


Predicate With Stream API

For example:

Main.java
Run Online
List<Integer> numbers = List.of(10, 15, 20, 25, 30);
numbers.stream() .filter(n -> n > 20) .forEach(System.out::println);

Output:

The lambda passed to filter() behaves like a predicate.

Conceptually:

Main.java
Run Online
number↓Predicate↓true / false↓keep or reject

Combining Predicates

Predicates provide useful default methods such as:

  • and()

  • or()

  • negate()

Example:

Main.java
Run Online
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:

The number must satisfy both conditions.


Predicate OR

Main.java
Run Online
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:

Main.java
Run Online
Predicate<Integer> even = n -> n % 2 == 0;
Predicate<Integer> odd = even.negate();
System.out.println(odd.test(7));

Output:


Consumer<T>

Consumer<T> represents an operation that accepts a value but does not return a result.

Its main method is:

Example:

Main.java
Run Online
Consumer<String> printer = text -> System.out.println(text);
printer.accept("Hello Java");

Output:

Think of Consumer as:

There is no returned value.


Consumer Example

Main.java
Run Online
Consumer<Integer> display = number -> System.out.println("Number: " + number);
display.accept(100);

Output:

This 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().

Main.java
Run Online
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:

Main.java
Run Online
names.forEach(System.out::println);

Combining Consumers

Consumers provide an andThen() method.

Main.java
Run Online
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:

The 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:

Example:

Main.java
Run Online
Function<String, Integer> length = text -> text.length();
System.out.println(length.apply("Java"));

Output:

Think of Function as:

Main.java
Run Online
Input → Transformation → Output

Function Example

Convert a string to uppercase:

Main.java
Run Online
Function<String, String> upper = text -> text.toUpperCase();
System.out.println(upper.apply("java"));

Output:

The input and output types can be different.

Main.java
Run Online
Function<String, Integer> length = text -> text.length();

Here:


Function With Stream map()

The map() operation in streams commonly uses function-like behavior.

Main.java
Run Online
List<String> names = List.of("Amit", "Riya", "Rahul");
List<Integer> lengths = names.stream() .map(name -> name.length()) .toList();
System.out.println(lengths);

Output:

The lambda transforms every name into its length.

Conceptually:

Main.java
Run Online
"Amit" → 4"Riya" → 4"Rahul" → 5

Function Composition

Functions can be combined using methods such as:

  • andThen()

  • compose()

Example:

Main.java
Run Online
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:

Output:


Supplier<T>

Supplier<T> represents something that produces a value without receiving an input.

Its main method is:

Example:

Main.java
Run Online
Supplier<String> message =() -> "Hello Java";
System.out.println(message.get());

Output:

Think of a supplier as:

Main.java
Run Online
No input → Produce a value

Supplier Example With Current Time

Main.java
Run Online
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:

Main.java
Run Online
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:

Main.java
Run Online
PredicateInput → Question → booleanConsumerInput → Action → nothingFunctionInput → Transform → outputSupplierNothing → Produce → output

Example:

Main.java
Run Online
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.

Main.java
Run Online
List<Integer> salaries = List.of(25000, 45000, 60000, 80000);

Predicate — Filter

Main.java
Run Online
Predicate<Integer> highSalary = salary -> salary >= 50000;

Function — Transform

Main.java
Run Online
Function<Integer, Integer> bonus = salary -> salary + 5000;

Consumer — Display

Main.java
Run Online
Consumer<Integer> print = salary -> System.out.println("Salary: " + salary);

Supplier — Generate Default Salary

Main.java
Run Online
Supplier<Integer> defaultSalary =() -> 30000;

These four interfaces solve different kinds of problems.


Combining Them With Streams

Main.java
Run Online
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:

Main.java
Run Online
Updated: 65000Updated: 85000

Conceptually:

Main.java
Run Online
List↓Predicate↓Filter↓Function↓Transform↓Consumer↓Action

This 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:

Main.java
Run Online
@FunctionalInterfaceinterface StringProcessor {
    String process(String text);
}

Use it with a lambda:

Main.java
Run Online
StringProcessor processor = text -> text.toUpperCase();
System.out.println( processor.process("java"));

Output:

A custom functional interface is useful when the operation has domain-specific meaning.


Functional Interface With @FunctionalInterface

The annotation:

tells the compiler that the interface is intended to have exactly one abstract method.

For example:

Main.java
Run Online
@FunctionalInterfaceinterface Calculator {
    int calculate(int a, int b);
}

If another abstract method is added:

Main.java
Run Online
@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:

Main.java
Run Online
@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:

Main.java
Run Online
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:

Main.java
Run Online
IntPredicateIntConsumerIntFunction<R>IntSupplier

There are also variants for long and double.

Example:

Main.java
Run Online
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:

Example:

Main.java
Run Online
BiFunction<Integer, Integer, Integer> add =(a, b) -> a + b;
System.out.println(add.apply(10, 20));

Output:

Other useful interfaces include:

Main.java
Run Online
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

Main.java
Run Online
UnaryOperator<Integer> square = n -> n * n;
System.out.println(square.apply(5));

Output:

It is conceptually:

BinaryOperator

Main.java
Run Online
BinaryOperator<Integer> add =(a, b) -> a + b;
System.out.println(add.apply(10, 20));

Output:

It is conceptually:


Lambda Variable Scope

A lambda can access local variables when they are final or effectively final.

Main.java
Run Online
String prefix = "Hello";
Consumer<String> printer = name -> System.out.println(prefix + " " + name);
printer.accept("Tanmoy");

Output:

But changing the local variable after it is captured is not allowed:

Main.java
Run Online
String prefix = "Hello";
Consumer<String> printer = name -> System.out.println(prefix + name);
// prefix = "Hi"; // Error

The 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:

Main.java
Run Online
class Person {
    private String name = "Tanmoy";
    void show() {
        Runnable task =() -> System.out.println(this.name);
        task.run();
    }
}

Inside the lambda:

refers 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:

Main.java
Run Online
names.forEach(name -> System.out.println(name));

you can write:

Main.java
Run Online
names.forEach(System.out::println);

Common forms include:

Main.java
Run Online
object::instanceMethodClassName::staticMethodClassName::instanceMethodClassName::new

Example:

Main.java
Run Online
Function<String, Integer> length = String::length;

This is equivalent in behavior to:

Main.java
Run Online
Function<String, Integer> length = text -> text.length();

Lambda vs Anonymous Class

Anonymous class:

Main.java
Run Online
Runnable task = new Runnable() {
    @Override public void run() {
        System.out.println("Running");
    }
};

Lambda:

Main.java
Run Online
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:

Main.java
Run Online
// What type is this?
x -> x * 2;

A lambda needs a target functional-interface type or another supported target context.

For example:

Main.java
Run Online
Function<Integer, Integer> doubleValue = x -> x * 2;

Mistake 2: Returning a Value From Consumer

This is incorrect:

Main.java
Run Online
Consumer<Integer> consumer = x -> x * 2;

A Consumer returns void.

Use Function if you need a result:

Main.java
Run Online
Function<Integer, Integer> function = x -> x * 2;

Mistake 3: Returning Boolean From the Wrong Interface

If the main purpose is a condition:

Main.java
Run Online
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:

Main.java
Run Online
Supplier<Integer> supplier = x -> x * 2;

A supplier accepts no input.

Correct:

Main.java
Run Online
Supplier<Integer> supplier =() -> 100;

Mistake 5: Using Function When a More Specific Interface Exists

This works:

Main.java
Run Online
Function<Integer, Boolean> even = n -> n % 2 == 0;

But this communicates the intention better:

Main.java
Run Online
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:

Main.java
Run Online
names.forEach(System.out::println);

can be clearer than:

Main.java
Run Online
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

Main.java
Run Online
Predicate<T> T → boolean test()Consumer<T> T → void accept()Function<T, R> T → R apply()Supplier<T>() → T get()

Additional interfaces:

Main.java
Run Online
BiPredicate<T, U>BiConsumer<T, U>BiFunction<T, U, R>UnaryOperator<T>BinaryOperator<T>IntPredicateIntConsumerIntFunction<R>IntSupplier

Frequently 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.

Main.java
Run Online
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.

Main.java
Run Online
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.

Main.java
Run Online
Function<String, Integer> length = text -> text.length();

What is Supplier used for?

Supplier<T> produces a value without receiving an input.

Main.java
Run Online
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.

Main.java
Run Online
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.

UnaryOperator<T> requires the same type:

What is the difference between Supplier and Consumer?

A supplier produces a value without input:

A consumer receives a value and performs an action:

Graphics and Design Ideas

Graphic 1 — Four Core Functional Interfaces

Architecture Diagram
              java.util.function
                     │
       ┌─────────────┼─────────────┐
↓

↓

↓
Predicate      Consumer       Function
       │             │             │
↓

↓

↓
T → boolean    T → void       T → R
                     │
                     │
                  Supplier
↓
() → T

This is the most important visual for the page.


Graphic 2 — Easy Memory Trick

Main.java
Run Online
PREDICATE"Is it valid?"↓true / falseCONSUMER"Do something with it"↓voidFUNCTION"Change it"↓new valueSUPPLIER"Give me one"↓new value

This visual is ideal for beginners.


Graphic 3 — Stream Pipeline

Architecture Diagram
        List<Integer>
              │
↓
┌───────────┐
        │ Predicate │
        │   filter  │
        └─────┬─────┘
↓
Filtered
           values
              │
↓
┌───────────┐
        │  Function │
        │    map    │
        └─────┬─────┘
↓
Transformed
           values
              │
↓
┌───────────┐
        │  Consumer │
        │  forEach  │
        └───────────┘

Graphic 4 — Lambda Anatomy

Architecture Diagram
        (a, b)  ->  a + b
          │          │
          │          └── Lambda body
          │
          └── Parameters

Show three examples:

Main.java
Run Online
() -> "Hello"x -> x * 2(a, b) -> a + b

Graphic 5 — Interface Comparison Table

Architecture Diagram
┌───────────┬───────────────┬────────────┐
│ 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+
Main.java
Run Online
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));
        }
    }
}

Common Java Technical Interview Questions & Answers

Interview Prep
Q1

What are the four core functional interface categories in java.util.function?

Predicate (tests condition), Consumer (accepts input with no return), Function (transforms input to output), and Supplier (generates output with no input).
Q2

Can a functional interface contain default and static methods?

Yes, it can contain any number of default and static methods as long as it has exactly one abstract method.

Key Takeaways, Best Practices & Clean Code Rules

Java Best Practice & Production Rules

Use standard functional interfaces from java.util.function rather than inventing custom single-method interfaces.