What Are Functional Interfaces in Java 8? Consumer, Predicate & Supplier Explained

If you write Java day in and day out, chances are you’ve used forEach() or filter() from the Stream API without thinking twice. It sounds simple: just pass a lambda and get an output. But there is a question of what is happening behind the scenes. Why does one lambda “do something” while another “decides something”? And from where the value came as an output when no input is given.
That “magic” relies on special types called functional interfaces, which define a single behavior that lambdas can plug into.
What is a Functional Interface?
A functional interface in Java is defined as an interface that has one abstract method. It is the main role in it and includes static and default methods. But still, it only allows one abstract method, and that's what makes it compatible with lambda expressions and references.
Java 8 didn’t just add Streams; it changed how many of us write code. Lambdas let you pass behavior around like data, and functional interfaces act as the type that behavior “fits into.”
Here’s the quick model most developers use:
- Consumer — takes an input, performs some work, and returns nothing
- Predicate — takes an input and returns true or false
- Supplier — takes no input and returns a value
You’ll see them everywhere: logging a value, validating something, generating objects on demand, filtering lists, running callbacks, and more.
Java also offers the optional @FunctionalInterface annotation. It's a smart barrier, but you don't need it. If someone later tries to add a second abstract method, the compiler will complain, and you’ll avoid quietly breaking lambda usage.
Consumer Interface
Consumer is one of the basic Java 8 functional interfaces. It is beneficial when you want to get some result, play with it, and don't return anything.
It provides two methods.
void accept(T t);
default Consumer<T> andThen(Consumer<? super T> after);
accept() is the abstract method. There’s also a default method named andThen(). The accept() method takes a generic type input, and its return type is void.
Consumer Interface Example
Assume we have a class named ConsumerDemo that uses the Consumer functional interface. In the example below, accept() prints the given number. Then we call it from the main method.
public class ConsumerDemo implements Consumer<Integer> {
@Override
public void accept(Integer integer) {
System.out.println(integer);
}
public static void main(String[] args) {
ConsumerDemo demo = new ConsumerDemo();
demo.accept(2);
}
}
This works, but it's a bit much to write a full class only to print a value. A lambda expression can be used to write the same idea.
public class ConsumerDemo {
public static void main(String[] args) {
Consumer<Integer> consumer = (integer) -> System.out.println(integer);
consumer.accept(2);
}
}
Here, there’s no need to implement the interface or override the method in a separate class. You just write the behavior directly.
If this feels new, it usually helps to look at a few more java 8 functional interfaces first, because they all follow the same “single abstract method” pattern. Now let’s switch the context for a moment and look at the forEach() method.
forEach Documentation
Based on how forEach() is defined, it doesn’t return anything, and it takes a Consumer as the argument. That’s why code like this works:
public class ConsumerDemo {
public static void main(String[] args) {
Consumer<Integer> consumer = (integer) -> System.out.println(integer);
List<Integer> list = List.of(1, 2, 3, 4);
list.forEach(consumer);
}
}
But a Consumer is basically “the thing your lambda represents”, so you don’t have to store it in a variable.
You can directly pass the lambda into forEach().
public class ConsumerDemo {
public static void main(String[] args) {
List<Integer> list = List.of(1, 2, 3, 4);
list.forEach((integer) -> System.out.println(integer));
}
}
And that’s the familiar forEach() usage most people recognize.
The reason this works so smoothly is that the Java 8 stream consumer (Consumer functional interface) is what forEach() expects under the hood. Your lambda just plugs into that contract.
Predicate Functional Interface
It’s commonly used when filtering collections, validating input, or applying conditions that return a Boolean value. It’s part of the java.util.function package and was introduced in functional programming in Java 8. It’s commonly used with streams, especially in .filter() operations.
Functional Method
boolean test(T t);
This is the abstract method that takes a value and gives you back a Boolean true or false, based on your condition.
There are also other methods available in the interface:
- and()
- or()
- negate()
- isEqual()
These help you combine or manipulate your predicate logic without writing extra if-else code.
Basic Example (Interface Implementation)
Let’s start the old-school way by creating a class that implements Predicate.
import java.util.function.Predicate;
class PredicateDemo implements Predicate<Integer> {
@Override
public boolean test(Integer integer) {
return integer % 2 == 0;
}
public static void main(String[] args) {
PredicateDemo demo = new PredicateDemo();
System.out.println(demo.test(5));
}
}
Output:
false
Here, the class checks whether the given number is even.
Same Check Using Lambda
Now let’s simplify the same thing using a lambda expression. This is where functional programming in Java 8 comes in.
import java.util.function.Predicate;
class PredicateDemo {
public static void main(String[] args) {
Predicate<Integer> predicate = (integer) -> integer % 2 == 0;
System.out.println(predicate.test(5));
}
}
Output:
false
filter() Method With Predicate
Let’s now connect it to Java’s stream API. The .filter() method expects a Predicate. That means you can pass in your logic either via a variable or directly.
import java.util.function.Predicate;
import java.util.List;
class PredicateDemo {
public static void main(String[] args) {
Predicate<Integer> predicate = (integer) -> integer % 2 == 0;
List<Integer> list = List.of(1, 2, 3, 4);
list.stream()
.filter(predicate)
.forEach(number -> System.out.println(number));
}
}
Output:
2
4
The predicate keeps only the even numbers, and forEach() prints them one by one.
Direct Lambda in filter()
You can skip the separate variable and plug the lambda expression directly into filter():
import java.util.List;
class PredicateDemo {
public static void main(String[] args) {
List<Integer> list = List.of(1, 2, 3, 4);
list.stream()
.filter(integer -> integer % 2 == 0)
.forEach(number -> System.out.println(number));
}
}
It will give you the same output but in concise form.
Supplier Interface
Use this interface when you need to produce a value but you don’t want to pass anything in. It exposes one abstract method, get(), and that method returns a generic type T.
T get();
Let’s walk through a few quick examples.
Supplier Interface Example
We’ll stick to the same pattern as before.
First, here’s a SupplierDemo class that implements Supplier<Integer> (java.util.function supplier, ).
class SupplierDemo implements Supplier<Integer> {
@Override
public Integer get() {
return 0;
}
public static void main(String[] args) {
SupplierDemo demo = new SupplierDemo();
System.out.println(demo.get());
}
}
Next, the same thing using a lambda. In practice, you’ll usually see the lambda version because it’s shorter and still clear once you recognize the pattern.
class SupplierDemo {
public static void main(String[] args) {
Supplier<Integer> supplier = () -> 0;
System.out.println(supplier.get());
}
}
Now let’s look at Optional’s orElseGet() in the docs.
The parameter is a Supplier reference, so we can pass a supplier into orElseGet() and let it call get() only when needed.
class SupplierDemo {
public static void main(String[] args) {
Supplier<Integer> supplier = () -> 0;
List<Integer> list = List.of();
int value = list.stream().findAny().orElseGet(supplier);
System.out.println(value);
}
}
If the list has a value, findAny() returns it and the fallback never runs. If the list is empty, orElseGet() executes the supplier to produce the default.
You can also inline the lambda, which is often cleaner.
class SupplierDemo {
public static void main(String[] args) {
// Supplier<Integer> supplier = () -> 0;
List<Integer> list = List.of();
int value = list.stream().findAny().orElseGet(() -> 0);
System.out.println(value);
}
}
That’s it. The same idea shows up all over Streams with methods like filter(), map(), and forEach().
Plenty of other APIs use Consumer, Predicate, and Supplier too, and they behave in the same style. Once you spot the expected return type, you can usually guess which functional interface is being used without checking the import.
Conclusion
Java Stream API arrived with Java 8, and it’s now standard in modern Java codebases. It made functional patterns feel natural in Java, especially when working with collections. The goal here was to explain what these interfaces really do, so the code you read (and write) feels obvious.
If you’re working on enterprise Java projects and need experienced support, our team at Amrood Labs is ready to assist.
Frequently Asked Questions:
Is @FunctionalInterface necessary in Java?
@FunctionalInterface isn’t required, but it’s strongly recommended because it enforces the single-abstract-method rule at compile time. If another abstract method is added accidentally, compilation fails, protecting lambda compatibility and code intent.
What is -> in Java?
In Java, -> is the lambda operator introduced in Java 8. It separates parameters from the lambda body, enabling concise implementations of functional interfaces.
What are the 5 advantages of Java?
Java is platform-independent via the JVM, has a huge ecosystem of libraries, provides strong tooling and community support, includes automatic memory management, and delivers solid performance through JIT compilation.
Should I use CompletableFuture?
Use CompletableFuture when you need asynchronous, non-blocking workflows, such as chaining tasks, combining multiple async calls, or handling results without callbacks.
What are the 4 functional interfaces in Java 8?
The four core Java 8 functional interfaces are Predicate<T> for boolean tests, Function<T,R> for transformations, Consumer<T> for actions with side effects, and Supplier<T> for providing values. They power Streams and lambdas.
What is the Supplier functional interface in Java 8?
Supplier<T> represents a function that takes no arguments and returns a value using get(). It’s useful for lazy evaluation and generating values on demand, such as timestamps, random numbers, or deferred object creation.
What is the functional interface of a Consumer in Java?
Consumer<T> is a functional interface for operations that accept one input and return no result, using accept(T). It’s commonly used with forEach() in collections and streams to print, log, persist, or mutate data.
What is the purpose of Predicate in Java 8?
Predicate<T> is a functional interface used to evaluate conditions and return true or false via test(T). It’s widely used for filtering in Streams, validating inputs, and composing logic using and(), or(), and negate().
What’s the difference between Predicate and Function?
Predicate<T> returns a boolean and is used for checks like filtering or validation. Function<T,R> returns a value and is used for mapping or transformation. In Streams, filter() expects predicates, while map() expects functions.
What’s the difference between Supplier and Consumer?
A Supplier<T> produces values without input, using get(), making it ideal for lazy creation and generators. A Consumer<T> takes input and returns nothing, using accept(T), typically for side effects like printing or storing.
Can a functional interface have multiple methods?
A functional interface can only have one abstract method, but it may still include default and static methods. Methods inherited from Object don’t count as abstract. This keeps lambda compatibility while allowing reusable helper behavior.
