Predefined Functional interfaces
-
Last Updated: August 10, 2023
-
By: javahandson
-
Series
Learn Java in a easy way
Predefined functional interfaces in Java live in the java.util.function package. Learn Predicate, Function, Consumer, and Supplier with simple, hands-on examples.
Java 8 handed us lambda expressions. A lambda always needs an interface to attach itself to, and that interface must declare exactly one abstract method. We call such an interface a functional interface.
So do you write a brand new interface every single time you want a lambda? That would get tiring fast. The Java team felt the same way. They shipped a whole package of ready-made ones instead.
These ready-made interfaces sit in java.util.function. The package holds 43 of them. That number sounds scary at first, but relax. Four of them cover nearly everything you will ever write.
Those four are Predicate, Function, Consumer, and Supplier. Learn these four properly and the other 39 stop looking mysterious. Most of them just repeat the same four ideas with a small twist.
Here is the nice part. You probably use them already. Every call to filter, map, or forEach on a stream hands a lambda to one of these interfaces.
We start with what makes an interface functional. Then we take the four core interfaces one at a time, with a small program for each. Here is the plan:
You only need a rough feel for lambda syntax before starting. If that part still feels shaky, our guide to lambda expressions in Java 8 walks through the basics first.
Before touching the ready-made ones, we should nail the idea behind them. The rule fits in a single line, and everything else follows from it.
A functional interface declares exactly one abstract method. That single method gives the lambda a shape to fill. The compiler matches your lambda against it and checks the types for you.
Here is a tiny one we could write by hand:
@FunctionalInterface
public interface Greeter {
String greet(String name); // the one abstract method
}
// A lambda now fits this shape
Greeter greeter = name -> "Hello " + name;
System.out.println(greeter.greet("Suraj")); // Output: Hello SurajWhy only one method? Think about it. If the interface had two abstract methods, the compiler could not tell which one your lambda meant to implement.
The @FunctionalInterface annotation stays optional. It simply asks the compiler to guard the rule. Add a second abstract method later and your build breaks right away, which beats finding out at the call site.
Default methods and static methods do not break the rule either. The compiler counts abstract methods only. That freedom matters, because the JDK interfaces below pack in several handy default methods.
Picture a world without java.util.function. Every team writes its own Checker, Validator, Converter, and Printer interface. They all mean the same four things, yet none of them fit together.
The JDK removes that mess. It names the shapes once, so every library speaks the same language. Look at what you gain:
That last point pays off more than people expect. We will see it when we chain two predicates together with a single call.
All of them live in java.util.function, which arrived with Java 8. You import them like any other class. Nothing extra to add to your build file.
Do not let the count of 43 worry you. Sort them into families and the picture clears up:
Master the four core shapes and the rest read like small variations. Let us start with the simplest of them.
Predicate answers a yes-or-no question about a value. Is this number even? Did this student clear the cutoff? Does this string start with the letter A?
Predicate takes a value of type T and hands back a boolean. Its single abstract method carries the name test:
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t); // abstract method
}The type parameter T means Predicate works with any type. Integer, String, or your own Student class all slot straight in.
Let us write a check for even numbers. The lambda takes a number and compares its remainder against zero.
package com.javahandson.predefined;
import java.util.function.Predicate;
public class Test {
public static void main(String[] args) {
Predicate<Integer> isEven = number -> number % 2 == 0;
System.out.println(isEven.test(10)); // Output: true
System.out.println(isEven.test(7)); // Output: false
}
}Here T stands for Integer. We pass an Integer to test, and the lambda body decides the answer. Notice how the variable name reads like the question it answers.
Nothing runs when you create the predicate. The lambda body waits, quietly, until some code calls test. That delay matters, and Supplier leans on the same trick later.
Predicate shines with your own classes too. We need a small Student class for the next few examples:
package com.javahandson.predefined;
public class Student {
private String name;
private int marks;
public Student(String name, int marks) {
this.name = name;
this.marks = marks;
}
public String getName() {
return name;
}
public int getMarks() {
return marks;
}
}Now we ask a question about each student. Did they score above 400?
package com.javahandson.predefined;
import java.util.function.Predicate;
public class Test {
public static void main(String[] args) {
Predicate<Student> scoredAbove400 = student -> student.getMarks() > 400;
Student[] students = {
new Student("Suraj", 450),
new Student("Iqbal", 350)
};
for (Student student : students) {
System.out.println(student.getName() + " above 400 : " + scoredAbove400.test(student));
}
}
}
// Output:
// Suraj above 400 : true
// Iqbal above 400 : falseThis time T stands for Student, our own class. The lambda reaches into the object, reads the marks, and returns the verdict. One predicate now serves every student in the array.
Real checks rarely stay simple. You want students above 400 whose name starts with S. Do you cram both conditions into one lambda?
You could, but Predicate offers something cleaner. Three default methods let you build big conditions out of small ones:
package com.javahandson.predefined;
import java.util.function.Predicate;
public class Test {
public static void main(String[] args) {
Predicate<Student> scoredAbove400 = student -> student.getMarks() > 400;
Predicate<Student> nameStartsWithS = student -> student.getName().startsWith("S");
Student suraj = new Student("Suraj", 450);
System.out.println(scoredAbove400.and(nameStartsWithS).test(suraj)); // Output: true
System.out.println(scoredAbove400.or(nameStartsWithS).test(suraj)); // Output: true
System.out.println(scoredAbove400.negate().test(suraj)); // Output: false
}
}Each method reads exactly as it sounds. Here they are in plain words:
Two extras round out the set. The static method Predicate.isEqual compares a value against a fixed target. Java 11 added Predicate.not, which negates a predicate you pass in.
Predicate always answers true or false. Function lifts that limit. It takes one value and returns another value of any type you like.
Function carries two type parameters. T marks the input type and R marks the result type:
@FunctionalInterface
public interface Function<T, R> {
R apply(T t); // abstract method
}T and R may match or differ. Integer in and Integer out works fine. So does Student in and String out.
Start with the simplest conversion of all. A number goes in, its square comes out.
package com.javahandson.predefined;
import java.util.function.Function;
public class Test {
public static void main(String[] args) {
Function<Integer, Integer> square = number -> number * number;
System.out.println(square.apply(6)); // Output: 36
}
}Both T and R stand for Integer here. The lambda multiplies the number by itself and returns the result. Simple, but it shows the shape clearly.
Now let the input and output types differ. We feed a Student in and pull a grade String out:
package com.javahandson.predefined;
import java.util.function.Function;
public class Test {
public static void main(String[] args) {
Function<Student, String> toGrade = student -> {
if (student.getMarks() > 400) {
return "Grade A Distinction";
}
return "Grade B First Class";
};
Student[] students = {
new Student("Suraj", 450),
new Student("Iqbal", 350)
};
for (Student student : students) {
System.out.println(student.getName() + " : " + toGrade.apply(student));
}
}
}
// Output:
// Suraj : Grade A Distinction
// Iqbal : Grade B First ClassT stands for Student and R stands for String. The lambda body spans several lines, so it needs braces and an explicit return. Single-expression lambdas skip both.
Function ships two default methods for stitching steps together. They differ only in running order, and that difference trips up plenty of people.
package com.javahandson.predefined;
import java.util.function.Function;
public class Test {
public static void main(String[] args) {
Function<Integer, Integer> doubled = number -> number * 2;
Function<Integer, Integer> addTen = number -> number + 10;
// andThen: double first, then add ten
System.out.println(doubled.andThen(addTen).apply(5)); // Output: 20
// compose: add ten first, then double
System.out.println(doubled.compose(addTen).apply(5)); // Output: 30
}
}Read andThen from left to right. It runs the first function, then feeds that result into the next one.
Read compose from right to left. It runs the argument first, then passes that result into the original function.
Stuck on which to use? Pick andThen. It matches how we read English, and confusing code helps nobody.
Sometimes you want an action, not an answer. Print a line. Save a record. Send an email. Consumer covers exactly that case.
Consumer swallows a value and returns nothing. Its abstract method carries the name accept and a void return type:
@FunctionalInterface
public interface Consumer<T> {
void accept(T t); // abstract method
}The name fits well. A consumer eats the value and keeps quiet about it.
Watch the difference from Function. This lambda calculates a square and prints it, yet hands nothing back:
package com.javahandson.predefined;
import java.util.function.Consumer;
public class Test {
public static void main(String[] args) {
Consumer<Integer> printSquare = number -> {
int result = number * number;
System.out.println("Square of " + number + " is : " + result);
};
int[] numbers = {2, 3, 5};
for (int number : numbers) {
printSquare.accept(number);
}
}
}
// Output:
// Square of 2 is : 4
// Square of 3 is : 9
// Square of 5 is : 25The square never leaves the lambda. Printing happens inside, so the caller receives no value. Swap Consumer for Function only when the caller actually needs that result.
Consumer handles your own classes just as happily:
package com.javahandson.predefined;
import java.util.function.Consumer;
public class Test {
public static void main(String[] args) {
Consumer<Student> printName = student ->
System.out.println("Student name : " + student.getName());
Student[] students = {
new Student("Suraj", 450),
new Student("Iqbal", 350)
};
for (Student student : students) {
printName.accept(student);
}
}
}
// Output:
// Student name : Suraj
// Student name : IqbalOne line, no braces, no return. The lambda body sits as a single statement, which keeps things tidy.
Consumer offers a default andThen method as well. It runs both consumers on the same input, one after the other:
Consumer<Student> printName = student -> System.out.println("Name : " + student.getName());
Consumer<Student> printMarks = student -> System.out.println("Marks : " + student.getMarks());
printName.andThen(printMarks).accept(new Student("Suraj", 450));
// Output:
// Name : Suraj
// Marks : 450Notice the key detail. Both consumers receive the original student, because a Consumer produces no result to pass along. Function.andThen behaves differently, since each step feeds the next.
Supplier flips Consumer on its head. It accepts no input, yet it produces a value whenever you ask.
The abstract method carries the name get, takes zero arguments, and returns a T:
@FunctionalInterface
public interface Supplier<T> {
T get(); // abstract method
}Because get takes nothing, the lambda starts with an empty pair of brackets.
A clock reading makes a natural supplier. Ask twice and you may see two different answers:
package com.javahandson.predefined;
import java.time.LocalDate;
import java.util.function.Supplier;
public class Test {
public static void main(String[] args) {
Supplier<LocalDate> today = () -> LocalDate.now();
System.out.println("Today is : " + today.get());
}
}
// Output:
// Today is : 2026-08-09Here T stands for LocalDate. The empty brackets show that get wants no arguments. We favour LocalDate over the old Date class, since java.time replaced it back in Java 8.
A supplier can build something bigger than a single value. This one assembles a whole list:
package com.javahandson.predefined;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
public class Test {
public static void main(String[] args) {
Supplier<List<Integer>> evenNumbers = () -> {
List<Integer> list = new ArrayList<>();
for (int i = 1; i <= 20; i++) {
if (i % 2 == 0) {
list.add(i);
}
}
return list;
};
System.out.println(evenNumbers.get());
}
}
// Output:
// [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]The loop builds the list and the lambda returns it. Call get twice and the loop runs twice, handing back a fresh list each time.
So why wrap a value in a lambda instead of just building it? One word: timing.
A plain value gets built the moment you write it. A Supplier waits until somebody calls get. When the value costs real time or memory, that delay saves you plenty.
// Builds the fallback even when the Optional already holds a value String a = optionalName.orElse(buildExpensiveDefault()); // Builds the fallback only when the Optional sits empty String b = optionalName.orElseGet(() -> buildExpensiveDefault());
The first line always calls buildExpensiveDefault, even when nobody needs the answer. The second line skips that call whenever the Optional already holds a name. Same result, far less wasted effort.
We have met all four now. Seeing them together makes the pattern jump out.
| Interface | Method | Takes | Returns | Typical use |
|---|---|---|---|---|
| Predicate<T> | test | One T | boolean | Check a condition |
| Function<T, R> | apply | One T | One R | Convert or calculate |
| Consumer<T> | accept | One T | void | Print, save, send |
| Supplier<T> | get | Nothing | One T | Create or fetch on demand |
Look at the Takes and Returns columns alone. Every combination of input and output appears exactly once. That symmetry explains why four interfaces stretch so far.
Two quick questions settle the choice almost every time. Does your lambda need an input? Does the caller need a result?
Keep that little table in your head. It answers the choice faster than any documentation search.
Four down, thirty-nine to go. Do not panic. Those remaining interfaces mostly repeat the same shapes with a tweak.
Predicate<Integer> works, but it hides a cost. Every int must become an Integer object first, then unwrap again inside the lambda. That autoboxing dance wastes time in a tight loop.
Primitive variants dodge the whole problem:
| Interface | Method | Boxed equivalent |
|---|---|---|
| IntPredicate | boolean test(int) | Predicate<Integer> |
| IntFunction<R> | R apply(int) | Function<Integer, R> |
| ToIntFunction<T> | int applyAsInt(T) | Function<T, Integer> |
| IntConsumer | void accept(int) | Consumer<Integer> |
| IntSupplier | int getAsInt() | Supplier<Integer> |
Long and double get the same treatment, so LongPredicate and DoubleFunction exist too. The naming stays predictable throughout.
IntPredicate isEven = number -> number % 2 == 0; // no boxing anywhere System.out.println(isEven.test(10)); // Output: true
Some functions return the same type they receive. Uppercasing a String gives back a String. Doubling an int gives back an int.
Writing Function<String, String> each time feels clumsy, so Java offers a shorthand:
// UnaryOperator<T> extends Function<T, T>
UnaryOperator<String> shout = text -> text.toUpperCase();
System.out.println(shout.apply("java")); // Output: JAVA
// BinaryOperator<T> extends BiFunction<T, T, T>
BinaryOperator<Integer> add = (a, b) -> a + b;
System.out.println(add.apply(3, 4)); // Output: 7Both act as convenience names rather than new ideas. UnaryOperator extends Function, and BinaryOperator extends BiFunction, so every default method still applies.
What about a lambda that needs two inputs? The Bi family covers that ground with BiPredicate, BiFunction, and BiConsumer.
No BiSupplier exists, though. A supplier takes no arguments at all, so a two-argument version makes no sense.
Those interfaces deserve their own space, and we cover them in the follow-up article on predefined functional interfaces with 2 input arguments.
Here comes the payoff. These four interfaces power the Stream API, so learning them unlocks streams at the same time.
Look at the signature of Stream.filter and you meet an old friend:
Stream<T> filter(Predicate<? super T> predicate);
// So this lambda is simply a Predicate<Integer>
List<Integer> evens = numbers.stream()
.filter(number -> number % 2 == 0)
.collect(Collectors.toList());That lambda inside filter creates a Predicate. The stream calls test on every element and keeps the ones answering true.
Stream.map asks for a Function, which makes sense. Mapping turns one value into another:
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
List<String> names = students.stream()
.map(student -> student.getName())
.collect(Collectors.toList());The stream calls apply on each student and gathers the returned names. Our guide to mapping in Java 8 streams digs deeper into this one.
forEach performs an action per element and returns nothing, which describes Consumer precisely:
void forEach(Consumer<? super T> action); students.forEach(student -> System.out.println(student.getName()));
The same method exists on every Collection, so you can call it straight on a List without opening a stream.
Optional leans on Supplier for its lazy fallbacks, and Stream.generate builds an endless stream from one:
String name = optionalName.orElseGet(() -> "Guest"); // Supplier<String> Stream<Double> randoms = Stream.generate(() -> Math.random()); // Supplier<Double>
Map.computeIfAbsent wants a Function. Optional.map wants a Function too. Spot the pattern once and the whole library starts feeling familiar.
Five traps catch beginners again and again. A minute here saves an hour of head scratching later.
Plenty of people default to Function for everything. It compiles, sure, but the intent blurs.
Returning a Boolean from a Function works technically. Predicate says the same thing more clearly, and it brings and, or, and negate along for free.
This one bites early. You write a Consumer, then try to grab a result:
Consumer<Integer> square = number -> number * number; int result = square.accept(5); // compile error: void cannot convert to int
The accept method returns void, full stop. Need that square back in the caller? Switch to Function<Integer, Integer> instead.
We saw earlier how f.andThen(g) runs f first, while f.compose(g) runs g first. Both compile happily and both return a Function.
The compiler stays silent, so the wrong choice slips into production as a quiet arithmetic bug. Test any chained function with real numbers before trusting it.
Predicate<Integer> inside a loop over a million ints boxes a million times. Each box allocates an object, and the garbage collector cleans up after every one.
IntPredicate removes that cost entirely. For small collections nobody notices, but hot loops repay the switch. Our article on wrapper classes and autoboxing explains what happens underneath.
A lambda may read local variables from around it, but only effectively final ones. Reassign such a variable and the compiler objects:
int count = 0;
Consumer<Integer> counter = number -> count++; // compile error
// Use an array or an AtomicInteger when you truly need mutable state
int[] tally = {0};
Consumer<Integer> safeCounter = number -> tally[0]++; // compilesInstance fields and static fields escape this rule, since only local variables face the restriction.
Time to put all four together. One small program, four interfaces, each doing the job it fits best.
We want a tiny report of distinction holders. The task splits into four clean steps:
package com.javahandson.predefined;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
public class ReportApp {
public static void main(String[] args) {
Supplier<List<Student>> source = () -> {
List<Student> list = new ArrayList<>();
list.add(new Student("Suraj", 450));
list.add(new Student("Iqbal", 350));
list.add(new Student("Meera", 410));
return list;
};
Predicate<Student> distinction = student -> student.getMarks() > 400;
Function<Student, String> toLine =
student -> student.getName() + " scored " + student.getMarks();
Consumer<String> printer = line -> System.out.println(line);
for (Student student : source.get()) {
if (distinction.test(student)) {
printer.accept(toLine.apply(student));
}
}
}
}
// Output:
// Suraj scored 450
// Meera scored 410Follow the flow once and the design clicks. The supplier hands over three students. The predicate drops Iqbal, whose 350 falls short of the cutoff.
Suraj and Meera continue. The function shapes each into a sentence, then the consumer prints it. Four interfaces, four responsibilities, zero overlap.
The same logic reads even better as a stream:
source.get().stream()
.filter(distinction) // Predicate
.map(toLine) // Function
.forEach(printer); // ConsumerNotice what changed. We pass the very same variables straight into filter, map, and forEach. Stream methods speak these interfaces natively, so no rewriting happens at all.
A: They are functional interfaces that already ship inside the JDK, so you never write them yourself. Each one declares a single abstract method, which lets a lambda expression slot right in. The java.util.function package holds 43 of them, and Predicate, Function, Consumer, and Supplier cover most everyday work.
A: The java.util.function package, added in Java 8. A few other functional interfaces live elsewhere, such as Runnable, Callable, and Comparator, and lambdas work with those too.
A: Predicate takes one argument and always returns a boolean through its test method. Function takes one argument and returns any type you choose through its apply method. Use Predicate for a yes-or-no check, because it also brings the and, or, and negate helpers.
A: Pick Consumer when the caller wants no return value. Printing, logging, and saving all fit that shape, and the accept method returns void. Pick Function whenever the caller needs the computed result back.
A: A Supplier runs its lambda body only when some code calls get. Wrapping an expensive value in a Supplier therefore delays that cost until the moment of real need. Optional.orElseGet shows the benefit clearly, since it skips the fallback entirely when a value already exists.
A: No. Any interface with exactly one abstract method works with a lambda, annotation or not. The annotation asks the compiler to enforce that rule, so adding a second abstract method fails the build immediately.
A: Yes, as many as it likes. Only abstract methods count toward the limit of one. Predicate proves the point with default methods and, or, and negate, plus the static method isEqual.
A: f.andThen(g) runs f first and passes its result into g. f.compose(g) reverses that order, running g first and feeding the result into f. Both return a new Function and neither one changes the originals.
A: They are shorthand names for functions whose input and output share a type. UnaryOperator extends Function of T to T, and BinaryOperator extends BiFunction of T, T to T. Both inherit every default method from their parent interface.
A: Predicate of Integer forces autoboxing on every call, so each int turns into an Integer object first. IntPredicate takes a raw int and skips that step. Across a large loop the primitive version allocates far less memory and runs faster.
Let us wrap up what we covered. Predefined functional interfaces give lambdas a ready-made shape to fill, and they all sit in java.util.function.
Four of them carry the load. Predicate answers true or false through test. Function converts one value into another through apply.
Consumer takes a value and returns nothing through accept. Supplier takes nothing and returns a value through get. Input and output between them, that pairing decides your choice.
Default methods add real power on top. Predicate combines with and, or, and negate, while Function chains through andThen and compose.
The rest of the package repeats these shapes. Primitive variants skip boxing, operator names shorten the common case, and the Bi family accepts two arguments.
Best of all, this knowledge carries straight into streams. filter wants a Predicate, map wants a Function, and forEach wants a Consumer. Learn the four, and a big slice of modern Java opens up.