Table of Contents

Predefined Functional interfaces

  • Last Updated: August 10, 2023
  • By: javahandson
  • Series
img

Predefined Functional interfaces

Predefined functional interfaces in Java live in the java.util.function package. Learn Predicate, Function, Consumer, and Supplier with simple, hands-on examples.

1. Introduction

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.

1.1 What This Article Covers

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:

  • What a functional interface means, and why the JDK ships its own
  • Predicate, the one that answers true or false
  • Function, the one that turns one value into another
  • Consumer, the one that takes a value and gives nothing back
  • Supplier, the one that gives a value without taking any
  • Primitive flavours such as IntPredicate, plus UnaryOperator and BinaryOperator
  • Mistakes beginners hit, a full walkthrough, and interview questions

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.

2. What Is a Functional Interface?

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.

2.1 One Abstract Method, One Job

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 Suraj

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

2.2 Why Java Ships Ready-Made Ones

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:

  • Less boilerplate – you skip writing an interface for a job the JDK already named
  • A shared vocabulary – any Java developer reads Predicate and knows the intent instantly
  • Library support – streams, Optional, and collections accept these types directly
  • Free composition – default methods let you glue small pieces into bigger ones

That last point pays off more than people expect. We will see it when we chain two predicates together with a single call.

2.3 Where They Live: The java.util.function Package

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:

  • Four core shapes that we cover here, one for each combination of input and output
  • Primitive flavours such as IntPredicate and DoubleFunction, built to skip boxing
  • Two-argument versions such as BiFunction, for lambdas that take a pair of values
  • Operator shortcuts, namely UnaryOperator and BinaryOperator

Master the four core shapes and the rest read like small variations. Let us start with the simplest of them.

3. The Predicate Interface

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?

3.1 The test Method

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.

  • Takes: one argument of type T
  • Returns: a boolean, true when the condition holds and false otherwise
  • Reach for it when: your lambda checks a condition

3.2 Example: Checking for an Even Number

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.

3.3 Example: Filtering Your Own Objects

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

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

3.4 Joining Predicates With and, or, and negate

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:

  • and – both conditions must hold
  • or – either condition suffices
  • negate – flips the answer around

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.

4. The Function Interface

Predicate always answers true or false. Function lifts that limit. It takes one value and returns another value of any type you like.

4.1 The apply Method

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.

  • Takes: one argument of type T
  • Returns: a value of type R
  • Reach for it when: your lambda converts, calculates, or extracts something

4.2 Example: Squaring a Number

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.

4.3 Example: Turning a Student Into a Grade

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 Class

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

4.4 Chaining With andThen and compose

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.

5. The Consumer Interface

Sometimes you want an action, not an answer. Print a line. Save a record. Send an email. Consumer covers exactly that case.

5.1 The accept Method

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.

  • Takes: one argument of type T
  • Returns: nothing at all, the return type reads void
  • Reach for it when: your lambda performs a side effect such as printing or saving

5.2 Example: Printing Squares

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

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

5.3 Example: Consuming a Student

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

One line, no braces, no return. The lambda body sits as a single statement, which keeps things tidy.

5.4 Running Two Consumers in a Row

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

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

6. The Supplier Interface

Supplier flips Consumer on its head. It accepts no input, yet it produces a value whenever you ask.

6.1 The get Method

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.

  • Takes: no arguments
  • Returns: a value of type T
  • Reach for it when: your lambda creates or fetches something on demand

6.2 Example: Supplying the Current Date

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-09

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

6.3 Example: Supplying a List of Even Numbers

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.

6.4 Why Supplier Saves Work

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.

7. The Four Interfaces Side by Side

We have met all four now. Seeing them together makes the pattern jump out.

7.1 Quick Comparison Table

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.

7.2 How to Pick the Right One

Two quick questions settle the choice almost every time. Does your lambda need an input? Does the caller need a result?

  • One input, and the answer reads true or false → Predicate
  • One input, and the caller wants some other value back → Function
  • An input goes in, yet nothing comes back → Consumer
  • Nothing goes in, yet a value comes back → Supplier

Keep that little table in your head. It answers the choice faster than any documentation search.

8. The Other Variants You Will Meet

Four down, thirty-nine to go. Do not panic. Those remaining interfaces mostly repeat the same shapes with a tweak.

8.1 Primitive Versions Like IntPredicate

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

8.2 UnaryOperator and BinaryOperator

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

Both act as convenience names rather than new ideas. UnaryOperator extends Function, and BinaryOperator extends BiFunction, so every default method still applies.

8.3 Versions That Take Two Arguments

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.

9. Where You Already Use Them

Here comes the payoff. These four interfaces power the Stream API, so learning them unlocks streams at the same time.

9.1 filter Takes a Predicate

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.

9.2 map Takes a Function

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.

9.3 forEach Takes a Consumer

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.

9.4 Optional Takes a Supplier

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.

10. Common Mistakes and Pitfalls

Five traps catch beginners again and again. A minute here saves an hour of head scratching later.

10.1 Reaching for the Wrong Interface

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.

10.2 Expecting a Consumer to Return Something

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.

10.3 Mixing Up andThen and compose

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.

10.4 Boxing Everywhere With Wrapper Types

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.

10.5 Changing a Captured Variable

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]++;   // compiles

Instance fields and static fields escape this rule, since only local variables face the restriction.

11. A Practical Walkthrough

Time to put all four together. One small program, four interfaces, each doing the job it fits best.

11.1 The Problem

We want a tiny report of distinction holders. The task splits into four clean steps:

  • Build the student list on demand, which calls for a Supplier
  • Keep only students above 400, a job for a Predicate
  • Turn each survivor into a printable line, meaning a Function
  • Print every line, handled by a Consumer

11.2 The Code

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 410

11.3 Reading the Output

Follow 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);       // Consumer

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

12. Interview Questions

Q: What are predefined functional interfaces in Java?

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.

Q: Which package holds the predefined functional interfaces?

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.

Q: What is the difference between Predicate and Function?

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.

Q: When should you use Consumer instead of Function?

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.

Q: Why does Supplier count as lazy?

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.

Q: Is the @FunctionalInterface annotation mandatory?

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.

Q: Can a functional interface have default and static methods?

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.

Q: What is the difference between andThen and compose in Function?

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.

Q: What are UnaryOperator and BinaryOperator?

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.

Q: Why does Java provide IntPredicate when Predicate of Integer already works?

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.

13. Conclusion

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.

Further Reading

Leave a Comment