Skip to main content

Java Functional Interfaces: Function, Predicate, Supplier and Consumer Explained

What a lambda really is, how to pick the right interface from java.util.function by shape, why primitive versions exist, and the five compile errors every beginner meets. Every program was compiled and run on Java 25 and JDK 27.

You are reading a stream pipeline, or a Comparator, or a Spring configuration class, and the line ends in something like s -> s.isEmpty(). The code works, but you cannot say what type that expression is. Then you try to write one yourself, the compiler answers Object is not a functional interface or local variables referenced from a lambda expression must be final or effectively final, and the error message is no help at all. Everything in that paragraph has one explanation. A lambda is a short way to write an object of an interface that has exactly one abstract method, and the JDK ships 43 ready-made interfaces of that kind in java.util.function. Once you know five of them by shape, and the four or five ways that lambdas fail to compile, the streams and collectors code in Java Streams API: The Complete Reference Guide and Java Streams API Deep Dive + Collectors Cookbook reads as vocabulary rather than magic. This page is that prerequisite, written for someone meeting the idea for the first time.
Versions and how this was checked. Java 25 LTS (Temurin 25.0.4.1) and JDK 27 (released 15 September 2026). Retrieved 22 September 2026. Every program on this page was compiled and run on both, and the output was identical apart from the version line. Every compiler error quoted is javac’s own text, and the interface count and every stream method signature were read from the running JDK with reflection.

There is no companion repository for this page. The programs are short and are quoted in full below, so each code block links to the Javadoc for the interface it uses rather than to a repository file. Nothing here was benchmarked: where performance comes up, the page says what the compiler does and stops there.

A functional interface is an interface with one abstract method

An interface is a list of methods a class promises to have. An abstract method is one with a name and types but no body. Before Java 8, if you wanted to pass a small piece of behaviour to a method, such as “what to do when the button is clicked”, you wrote an anonymous class that implemented the interface and filled in its one method. A lambda is the short form of exactly that, and it works whenever the interface has one abstract method. Such an interface is called a functional interface. The example below defines one and uses it three ways. The methods twice and polite have bodies, so they are not abstract and do not count: Function works the same way, with one abstract method (apply) and several default ones (andThen, compose).
@FunctionalInterface
interface Greeter {
    String greet(String name);                                   // the one abstract method

    default Greeter twice() { return n -> greet(greet(n)); }     // default methods do not count

    static Greeter polite() { return n -> "Dear " + n; }         // static methods do not count
}
Greeter g = name -> "Hello, " + name;
System.out.println(g.greet("Ankur"));
System.out.println(g.twice().greet("Ankur"));
System.out.println(Greeter.polite().greet("Ankur"));
Running it prints this. The code is the Shape01 program, and the transcript is what the JDK printed; any functional interface, such as Predicate, is used the same way:
Hello, Ankur
Hello, Hello, Ankur
Dear Ankur
The picture shows what happened on the first line. The lambda supplied only a body. Everything else, the method name greet, the parameter type, and the return type, came from the interface the lambda was assigned to. That interface is called the target type.
A lambda is only the body; the interface supplies the name and the types The lambda name -> “Hello, ” + name no name, no declared types The target type Greeter String greet(String name) An object Greeter g = … g.greet(“Ankur”) runs the body The compiler reads the target type from the left of the equals sign, or from the parameter of the method the lambda is passed to.
Two rules decide whether an interface counts, and both are in JLS 9.8, Functional Interfaces. Default and static methods are ignored. And so is any abstract method that just re-declares a public method of Object, such as equals. The second rule is why Comparator, which lists equals next to compare, is still a functional interface. This helper reads the abstract methods of a type from the running JDK:
static String abstractMethods(Class<?> type) {
    return Arrays.stream(type.getMethods())
            .filter(m -> Modifier.isAbstract(m.getModifiers()))
            .map(Method::getName)
            .sorted()
            .toList()
            .toString();
}
System.out.println("Greeter    abstract methods: " + abstractMethods(Greeter.class));
System.out.println("Runnable   abstract methods: " + abstractMethods(Runnable.class));
System.out.println("Comparator abstract methods: " + abstractMethods(Comparator.class));
System.out.println("Iterable   abstract methods: " + abstractMethods(Iterable.class));
(Comparator is not in java.util.function but is the most familiar functional interface in the JDK; see the Comparator Javadoc.)
Greeter    abstract methods: [greet]
Runnable   abstract methods: [run]
Comparator abstract methods: [compare, equals]
Iterable   abstract methods: [iterator]
The @FunctionalInterface annotation is optional. It changes nothing about how the interface behaves; it asks the compiler to reject the interface if it ever gains a second abstract method, which is the error shown later on this page. Write it on your own interfaces so that a colleague adding a method finds out at compile time rather than at every call site.

The compiler works out a lambda’s type from where it lands

A lambda has no type of its own. It gets one from the place it is written: the type on the left of an equals sign, the parameter type of the method it is passed to, or a cast. The same text can therefore mean different things in different places, and can mean nothing at all where there is no place to read a type from. This is the part beginners find strangest, so here are both sides of it. On the first side, the identical expression s -> s.length() becomes a Function in one line and a ToIntFunction in the next, and () -> 42 becomes a Supplier and a Callable. Each object then has the method name its interface says:
// The same text on the right-hand side, three different interfaces on the left.
Function<String, Integer> f = s -> s.length();
ToIntFunction<String> g = s -> s.length();
System.out.println("Function.apply           : " + f.apply("abc"));
System.out.println("ToIntFunction.applyAsInt : " + g.applyAsInt("abc"));

Supplier<Integer> supplier = () -> 42;
Callable<Integer> callable = () -> 42;
System.out.println("Supplier.get             : " + supplier.get());
System.out.println("Callable.call            : " + callable.call());

Object o = (Runnable) () -> System.out.println("ran");
((Runnable) o).run();
Function.apply           : 3
ToIntFunction.applyAsInt : 3
Supplier.get             : 42
Callable.call            : 42
ran
On the other side, take the target away and the compiler has nothing to go on. Both of these fail, and the messages are the ones to recognise (JLS 15.27, Lambda Expressions has the rules):
var f = s -> s.length();
Object o = () -> "hello";
## E5_var.java
E5_var.java:3: error: cannot infer type for local variable f
        var f = s -> s.length();
            ^
  (lambda expression needs an explicit target-type)
1 error

## E7_object_target.java
E7_object_target.java:3: error: incompatible types: Object is not a functional interface
        Object o = () -> "hello";
                   ^
1 error
var needs a value whose type it can work out, and a lambda has none until a target gives it one. Object is not a functional interface, so nothing about it says what the lambda’s parameters or body should be. In both cases the fix is to name the interface: Function<String, Integer> f = s -> s.length();.

Five shapes cover almost everything

There are 43 interfaces in java.util.function on Java 25 and 27 (counted below). You do not need to memorise them, because they are variations on a few shapes. A shape is just what goes in and what comes out. Ask those two questions about the lambda you want to write, and the name follows. The tree below is the same question sequence as the table under it.
Which interface? Answer the questions from the top Does it return a value? Does it take an input? Does it take an input? no yes Runnable nothing in or out Consumer BiConsumer for two no yes Supplier Callable if it throws Is the result a boolean? no yes Predicate BiPredicate for two Same type as the input? yes no UnaryOperator BinaryOperator for two Function BiFunction for two yes no
Start at the top. If the lambda returns nothing, it is a Runnable when it also takes nothing and a Consumer when it takes something. If it returns a value and takes nothing, it is a Supplier. If it takes something and returns a boolean, it is a Predicate. If it returns the same type it was given, it is an operator. Otherwise it is a Function. Consumer, Predicate and Function each have a two-argument version whose name starts with Bi, and UnaryOperator has BinaryOperator.
You need to…UseIts one methodExample lambda
Run something; nothing in, nothing outRunnablerun()() -> log("done")
Produce a value from nothingSupplier<T>get()() -> "hello"
Produce a value, and it may throw a checked exceptionCallable<V>call()() -> 7
Use a value; no resultConsumer<T>accept(t)s -> System.out.println(s)
Ask a yes/no question about a valuePredicate<T>test(t)s -> s.isBlank()
Convert a value into another typeFunction<T, R>apply(t)s -> s.length()
Transform a value, keeping its typeUnaryOperator<T>apply(t)s -> s.toUpperCase()
Combine two values of one type into that typeBinaryOperator<T>apply(a, b)(a, b) -> a + b
Two inputs of different types, any resultBiFunction<T, U, R>apply(t, u)(s, n) -> s.repeat(n)
Two inputs, yes/no or no resultBiPredicate, BiConsumertest(t, u), accept(t, u)(k, v) -> v > 0
Put two values in orderComparator<T>compare(a, b)(a, b) -> a.length() - b.length()
Notice that the method name changes with the interface: run, get, accept, test, apply, call, compare. That is the single most confusing thing about the package, and it has no rule behind it beyond the interface’s own name, so it helps to remember the verb along with the shape. UnaryOperator and BinaryOperator have no method of their own; they inherit apply from Function and BiFunction and only fix the types to be equal. All ten shapes below are one program (the java.util.function package summary lists the rest):
Supplier<String> supplier = () -> "hello";                       // nothing in, one value out
Consumer<String> consumer = s -> System.out.println("consumed " + s);   // one in, nothing out
Predicate<String> predicate = s -> s.isBlank();                  // one in, boolean out
Function<String, Integer> function = s -> s.length();            // one in, another type out
UnaryOperator<String> unary = s -> s.toUpperCase();              // one in, same type out
BinaryOperator<Integer> binary = (a, b) -> a + b;                // two of a type in, same type out
BiFunction<String, Integer, String> bi = (s, n) -> s.repeat(n);  // two different types in, one out
Runnable runnable = () -> System.out.println("ran");             // nothing in, nothing out
Callable<Integer> callable = () -> 7;                            // like Supplier, may throw
Comparator<String> comparator = (a, b) -> a.length() - b.length(); // two in, an int out
System.out.println("Supplier       -> " + supplier.get());
consumer.accept("x");
System.out.println("Predicate      -> " + predicate.test("  "));
System.out.println("Function       -> " + function.apply("four"));
System.out.println("UnaryOperator  -> " + unary.apply("shout"));
System.out.println("BinaryOperator -> " + binary.apply(2, 3));
System.out.println("BiFunction     -> " + bi.apply("ab", 3));
runnable.run();
System.out.println("Callable       -> " + callable.call());
System.out.println("Comparator     -> " + comparator.compare("aa", "b"));
Its output (the other members of the family are in the java.util.function package summary):
Supplier       -> hello
consumed x
Predicate      -> true
Function       -> 4
UnaryOperator  -> SHOUT
BinaryOperator -> 5
BiFunction     -> ababab
ran
Callable       -> 7
Comparator     -> 1

Small functions combine: andThen, compose, negate

The reason these are interfaces with default methods, and not just bare lambdas, is that you can build a big function out of small ones. Function has andThen and compose, which chain two functions in opposite orders; Predicate has and, or and negate. The words are easy to mix up. a.andThen(b) means a first, then b. a.compose(b) means b first, then a. The picture runs the same pair both ways on the number 5.
andThen and compose run the same two functions in opposite orders plusOne.andThen(timesTwo) 5 input plusOne x + 1 6 in between timesTwo x * 2 12 result plusOne.compose(timesTwo) 5 input timesTwo x * 2 10 in between plusOne x + 1 11 result a.andThen(b) means a, then b. a.compose(b) means b, then a.
The two results differ, 12 against 11, because adding one and doubling do not commute. Getting these the wrong way round is one of the quietest bugs in functional code, since both compile and both return a number. The program that produced the figures (Function) also shows the predicate combinators and the comparator chain:
Function<Integer, Integer> plusOne = x -> x + 1;
Function<Integer, Integer> timesTwo = x -> x * 2;

System.out.println("plusOne.andThen(timesTwo).apply(5) = " + plusOne.andThen(timesTwo).apply(5));   // (5 + 1) * 2
System.out.println("plusOne.compose(timesTwo).apply(5) = " + plusOne.compose(timesTwo).apply(5));   // (5 * 2) + 1
System.out.println("Function.identity().apply(\"same\")  = " + Function.<String>identity().apply("same"));
Predicate<String> isEmpty = String::isEmpty;
Predicate<String> startsWithA = s -> s.startsWith("a");
Predicate<String> longWord = s -> s.length() > 3;
System.out.println("isEmpty.negate().test(\"\")           = " + isEmpty.negate().test(""));
System.out.println("startsWithA.and(longWord).test(\"ant\")   = " + startsWithA.and(longWord).test("ant"));
System.out.println("startsWithA.or(longWord).test(\"ant\")    = " + startsWithA.or(longWord).test("ant"));
System.out.println("Predicate.not(String::isBlank).test(\" \") = " + Predicate.not(String::isBlank).test(" "));
(Function and Predicate document every combinator.) The comparator chain is the same idea applied to sorting:
List<String> words = new ArrayList<>(List.of("pear", "fig", "apple", "kiwi", "date"));
words.sort(Comparator.comparing(String::length).thenComparing(Comparator.naturalOrder()));
System.out.println("by length, then A-Z: " + words);
words.sort(Comparator.comparing(String::length).reversed());
System.out.println("longest first      : " + words);

BinaryOperator<String> longer = BinaryOperator.maxBy(Comparator.comparing(String::length));
System.out.println("maxBy length(\"fig\", \"apple\") = " + longer.apply("fig", "apple"));
(Comparator Javadoc.) Here is everything those three blocks printed:
plusOne.andThen(timesTwo).apply(5) = 12
plusOne.compose(timesTwo).apply(5) = 11
Function.identity().apply("same")  = same
isEmpty.negate().test("")           = false
startsWithA.and(longWord).test("ant")   = false
startsWithA.or(longWord).test("ant")    = true
Predicate.not(String::isBlank).test(" ") = false
by length, then A-Z: [fig, date, kiwi, pear, apple]
longest first      : [apple, date, kiwi, pear, fig]
maxBy length("fig", "apple") = apple
Two things to notice. Predicate.not exists so that you can negate a method reference, which negate() cannot do because a method reference has no type until it lands somewhere. And Function.identity() is the function that returns its argument, which is more useful than it sounds: it is what you pass to Collectors.toMap when the element is its own key.

Method references are lambdas that already have a name

When a lambda does nothing except call one existing method, you can write the method’s name instead: String::length instead of s -> s.length(). The result is the same kind of object, with the same target-typing rule. There are four forms, and the only difficulty is which arguments go where, so the example labels each one (JLS 15.13, Method Reference Expressions is the rulebook). The constructor form shows the target type doing real work: a class with two constructors, and the interface on the left picks which one runs.
static class Tag {
    final String label;

    Tag() { this("none"); }

    Tag(String label) { this.label = label; }

    @Override
    public String toString() { return "Tag(" + label + ")"; }
}
// 1. static method
Function<String, Integer> parse = Integer::parseInt;
// 2. instance method of an object you already have (the receiver is captured)
String greeting = "hello";
Supplier<Integer> lengthOfGreeting = greeting::length;
// 3. instance method of the first argument (the receiver arrives as the parameter)
Function<String, Integer> length = String::length;
BiFunction<String, String, Boolean> startsWith = String::startsWith;
// 4. constructor: the interface decides which constructor
Supplier<Tag> noArgs = Tag::new;
Function<String, Tag> withLabel = Tag::new;
(Each line is a Function, Supplier or BiFunction, chosen to match the method it points at.) Printing them:
System.out.println("Integer::parseInt        -> " + parse.apply("42"));
System.out.println("greeting::length         -> " + lengthOfGreeting.get());
System.out.println("String::length           -> " + length.apply("four"));
System.out.println("String::startsWith       -> " + startsWith.apply("hello", "he"));
System.out.println("Tag::new as Supplier     -> " + noArgs.get());
System.out.println("Tag::new as Function     -> " + withLabel.apply("urgent"));
Function<Integer, Integer> viaLambda = Refs06::twice;
System.out.println("Refs06::twice            -> " + viaLambda.apply(21));
The output (Supplier, BiFunction):
Integer::parseInt        -> 42
greeting::length         -> 5
String::length           -> 4
String::startsWith       -> true
Tag::new as Supplier     -> Tag(none)
Tag::new as Function     -> Tag(urgent)
Refs06::twice            -> 42
Read the four forms this way. Integer::parseInt is static, so its arguments arrive as the lambda’s arguments. greeting::length is a method on an object you already hold, so the object is fixed and the lambda takes nothing. String::length and String::startsWith name an instance method through its type, so the first argument of the lambda becomes the receiver: startsWith takes two arguments in the BiFunction because the string being tested is one of them. And Tag::new is a constructor; with Supplier the no-argument constructor ran, and with Function<String, Tag> the one-argument constructor ran.
  • The four forms and how the compiler chooses between overloads: JLS 15.13, Method Reference Expressions.
  • Why Comparator.comparing(String::length) compiles where comparing(s -> s.length()) may not: the error section below.

The primitive versions exist to avoid boxing

Java has two kinds of number: primitives such as int, which are plain values, and their wrapper objects such as Integer. Generic types cannot hold primitives, so Function<Integer, Integer> works on wrapper objects. When it is called with an int, the value is put into an Integer (boxed), and inside the lambda the arithmetic needs the plain int back (unboxed). The picture shows what the compiler generates inside the lambda body in each case; the two blocks after it are the source, then the compiler’s own listing.
What the compiler puts inside the lambda body Function<Integer, Integer> plusOne = x -> x + 1 Integer x an object intValue() unbox + 1 the real work Integer.valueOf() box again Integer an object IntUnaryOperator plusOne = x -> x + 1 int x a plain value + 1 the real work int a plain value
This is the source of the two lambdas (Function, IntUnaryOperator):
static void boxedVersion() {
    Function<Integer, Integer> plusOne = x -> x + 1;
    plusOne.apply(1);
}

static void primitiveVersion() {
    IntUnaryOperator plusOne = x -> x + 1;
    plusOne.applyAsInt(1);
}
And this is what javap -c -p printed for the bodies the compiler generated from them. The boxed version calls intValue to unbox its argument and valueOf to box its result; the primitive version is four instructions on plain integers.
  private static int lambda$primitiveVersion$0(int);
    Code:
         0: iload_0
         1: iconst_1
         2: iadd
         3: ireturn
  private static java.lang.Integer lambda$boxedVersion$0(java.lang.Integer);
    Code:
         0: aload_0
         1: invokevirtual #161                // Method java/lang/Integer.intValue:()I
         4: iconst_1
         5: iadd
         6: invokestatic  #11                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
         9: areturn
Per the Integer.valueOf Javadoc, values from −128 to 127 always come from a cache, and outside that range the method may create a new object. This page did not measure what the extra calls cost. The primitive interfaces remove that work; whether it matters depends on how often the code runs. That is why java.util.function has 34 interfaces beyond the nine generic ones. The program below reads the package straight from the running JDK. The count is on its first line:
interfaces in java.util.function: 43
(Source: the Boxing05 program reads the package with the JDK’s jrt: file system; the java.util.function package summary lists the same interfaces.) The 34 follow one naming scheme, so learn the scheme rather than the list. Every name below appears in that program’s output:
PatternWhat it takes and givesintlongdouble
XFunction<R>a primitive in, an object outIntFunctionLongFunctionDoubleFunction
ToXFunction<T>an object in, a primitive outToIntFunctionToLongFunctionToDoubleFunction
ToXBiFunction<T, U>two objects in, a primitive outToIntBiFunctionToLongBiFunctionToDoubleBiFunction
XUnaryOperatora primitive in, the same primitive outIntUnaryOperatorLongUnaryOperatorDoubleUnaryOperator
XBinaryOperatortwo primitives in, the same primitive outIntBinaryOperatorLongBinaryOperatorDoubleBinaryOperator
XPredicatea primitive in, a boolean outIntPredicateLongPredicateDoublePredicate
XConsumera primitive in, nothing outIntConsumerLongConsumerDoubleConsumer
ObjXConsumer<T>an object and a primitive in, nothing outObjIntConsumerObjLongConsumerObjDoubleConsumer
XSuppliernothing in, a primitive outIntSupplierLongSupplierDoubleSupplier
XToYFunctionone primitive kind in, another outIntToLongFunction, IntToDoubleFunctionLongToIntFunction, LongToDoubleFunctionDoubleToIntFunction, DoubleToLongFunction
Three gaps are worth knowing. There is no char, short, byte or float version of anything, so those go through int or double or through the generic interface. There is no IntBiFunction; two ints in and an object out is not a shape the package provides, and you write your own (see below) or box. And the only boolean entry is BooleanSupplier. You will meet the primitive interfaces mostly through primitive streams, where IntStream.map takes an IntUnaryOperator and mapToInt takes a ToIntFunction. The last section confirms those signatures from the JDK.

What is really happening inside a lambda

It is tempting to picture a lambda as shorthand for an anonymous class, and that picture is nearly right for reading code and wrong in two places that matter. First, this means something different. Inside an anonymous class, this is the anonymous object; inside a lambda, this is whatever it was outside, because a lambda has no identity of its own. Second, the compiler does not generate a class file for each lambda. The program below shows both, using the same Runnable once each way (Function for the second half):
void show() {
    Runnable lambda = () -> System.out.println("lambda:    this is " + this.getClass().getName());
    Runnable anonymous = new Runnable() {
        @Override
        public void run() {
            System.out.println("anonymous: this is " + this.getClass().getName());
        }
    };
    lambda.run();
    anonymous.run();
}
Function<String, Integer> f = s -> s.length();
Class<?> c = f.getClass();
System.out.println("lambda class is hidden   : " + c.isHidden());
System.out.println("lambda class is synthetic: " + c.isSynthetic());
System.out.println("implements Function      : " + Function.class.isAssignableFrom(c));
System.out.println("name starts with Inside07: " + c.getName().startsWith("Inside07$$Lambda"));
This is what it printed (the second half of the program uses Function):
lambda:    this is Inside07
anonymous: this is Inside07$1
lambda class is hidden   : true
lambda class is synthetic: true
implements Function      : true
name starts with Inside07: true
The lambda’s this is the enclosing Inside07; the anonymous class’s is Inside07$1, a class of its own. The lambda’s class is hidden: the JDK creates it while the program runs, and it has no name you can refer to in source code. The reason is visible in the bytecode. Where an anonymous class would appear as new, the compiler emits a single invokedynamic instruction, and that instruction names a bootstrap method that builds the object the first time the line runs:
         1: invokedynamic #7,  0              // InvokeDynamic #0:run:(LInside07;)Ljava/lang/Runnable;
  #125 = MethodHandle       6:#126        // REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
(Both lines are from javap on the compiled program; see the LambdaMetafactory Javadoc for what the bootstrap method does.) You never need to write any of this. What it explains is the two differences above, and why the language promises only what JLS 15.27, Lambda Expressions says about a lambda’s behaviour and nothing about the class that implements it.

Five compile errors, and what each one is telling you

Almost every problem people have with lambdas is one of the five below, and each message names a rule that the earlier sections explained. Every message is javac’s own text from the JDK, captured by compiling one small file per error. Two more, cannot infer type for local variable and Object is not a functional interface, were in the section on target types. 1. “local variables referenced from a lambda expression must be final or effectively final.” A lambda copies the values of the local variables it uses at the moment it is created; it does not share the variable itself. If the variable could change afterwards, the lambda and the surrounding code would disagree about its value, so the language forbids it. “Effectively final” means a variable that is never reassigned, whether or not it says final.
int count = 0;
List.of("a", "b", "c").forEach(x -> count++);
(Consumer is the target here: forEach takes one.)
## E1_capture.java
E1_capture.java:6: error: local variables referenced from a lambda expression must be final or effectively final
        List.of("a", "b", "c").forEach(x -> count++);
                                            ^
1 error
The fix is not to mutate. Count with stream().count(), or sum with reduce. If you truly need a counter that a lambda can update, hold it in an object such as an AtomicInteger, because the reference to the object is what stays fixed (AtomicInteger Javadoc):
// 1. Counting inside a lambda: a mutable holder, or better, do not mutate at all.
List<String> items = List.of("a", "b", "c");
AtomicInteger count = new AtomicInteger();
items.forEach(x -> count.incrementAndGet());
System.out.println("counted with AtomicInteger : " + count.get());
System.out.println("counted without mutation   : " + items.stream().count());
2. “unreported exception IOException; must be caught or declared to be thrown.” None of the interfaces in java.util.function declares a throws clause, so a lambda targeting one of them cannot throw a checked exception. It is the ordinary checked-exception rule, applied to a method (apply) that you did not write and cannot change.
Function<Path, String> read = p -> Files.readString(p);
(Function is the target; its apply declares no checked exceptions.)
## E2_checked.java
E2_checked.java:7: error: unreported exception IOException; must be caught or declared to be thrown
        Function<Path, String> read = p -> Files.readString(p);
                                                           ^
1 error
The choices are to catch inside the lambda, to use Callable where the API accepts one (its call may throw), or to write a small functional interface of your own whose method does declare throws, with an adapter. The last is shown in the section after this one. 3. “cannot find symbol… method length()… variable s of type Object”. The message looks wrong, since s is obviously a String. It is the target-typing rule biting. Comparator.comparing(s -> s.length()) on its own can be typed from the list it sorts, but adding .reversed() makes the comparing(...) call a receiver rather than an argument, so it no longer has a target to read the type from, and T is inferred as Object.
words.sort(Comparator.comparing(s -> s.length()).reversed());
(Comparator Javadoc.)
## E3_reversed.java
E3_reversed.java:8: error: cannot find symbol
        words.sort(Comparator.comparing(s -> s.length()).reversed());
                                              ^
  symbol:   method length()
  location: variable s of type Object
1 error
Two fixes work: give the lambda parameter a type, or use a method reference, which carries its own type. comparingInt avoids the problem and the boxing (Comparator Javadoc):
// 2. Comparator.comparing(...).reversed(): give the lambda a type, or use a method reference.
List<String> words = new ArrayList<>(List.of("pear", "fig", "apple"));
words.sort(Comparator.comparing((String s) -> s.length()).reversed());
System.out.println("typed lambda + reversed    : " + words);
words.sort(Comparator.comparing(String::length).reversed());
System.out.println("method reference + reversed: " + words);
words.sort(Comparator.comparingInt(String::length));
System.out.println("comparingInt               : " + words);
Output (Comparator Javadoc):
typed lambda + reversed    : [apple, pear, fig]
method reference + reversed: [apple, pear, fig]
comparingInt               : [fig, pear, apple]
4. “Unexpected @FunctionalInterface annotation… multiple non-overriding abstract methods found”. This is the annotation doing its job. The interface has two abstract methods, so it is not functional, and both the annotation and any lambda assigned to it are errors:
@FunctionalInterface
interface Pair {
    int first();
    int second();
}
(Every functional interface in the java.util.function package summary has exactly one.)
## E4_two_methods.java
E4_two_methods.java:2: error: Unexpected @FunctionalInterface annotation
    @FunctionalInterface
    ^
  Pair is not a functional interface
    multiple non-overriding abstract methods found in interface Pair
E4_two_methods.java:9: error: incompatible types: Pair is not a functional interface
        Pair p = () -> 1;
                 ^
    multiple non-overriding abstract methods found in interface Pair
2 errors
5. “reference to run is ambiguous”. When a method is overloaded with two different functional interfaces that both fit the lambda’s shape, the compiler has no way to choose. Supplier and Callable both take nothing and return a value, so () -> "hello" fits either.
static <T> T run(Supplier<T> s) { return s.get(); }

static <T> T run(Callable<T> c) throws Exception { return c.call(); }

String s = run(() -> "hello");
(Supplier and Callable are the two overloads.)
## E6_ambiguous.java
E6_ambiguous.java:10: error: reference to run is ambiguous
        String s = run(() -> "hello");
                   ^
  both method <T#1>run(Supplier<T#1>) in E6_ambiguous and method <T#2>run(Callable<T#2>) in E6_ambiguous match
  where T#1,T#2 are type-variables:
    T#1 extends Object declared in method <T#1>run(Supplier<T#1>)
    T#2 extends Object declared in method <T#2>run(Callable<T#2>)
1 error
Cast the lambda to the interface you mean (Supplier), or give the two methods different names. If you are designing an API, prefer different names; overloading on functional interfaces is what causes this.
// 3. Ambiguous overloads: say which one you mean.
String viaSupplier = run((Supplier<String>) () -> "supplier");
String viaCallable = run((Callable<String>) () -> "callable");
cast to Supplier           : supplier
cast to Callable           : callable

Writing your own functional interface

The JDK’s interfaces cover the common shapes, and it is worth using them when they fit, because every Java developer already knows them and they come with combinators such as andThen for free. Write your own in three situations: when you need three or more inputs (BiFunction is the largest), when the method must declare a checked exception, or when a domain name reads far better than Function<Order, Invoice>, as in InvoiceBuilder. The first two are below. Annotate with @FunctionalInterface, as in Function.
@FunctionalInterface
interface TriFunction<A, B, C, R> {
    R apply(A a, B b, C c);
}

@FunctionalInterface
interface ThrowingFunction<T, R> {
    R apply(T t) throws Exception;

    static <T, R> Function<T, R> unchecked(ThrowingFunction<T, R> f) {
        return t -> {
            try {
                return f.apply(t);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        };
    }
}
ThrowingFunction solves error 2 from the last section. Its method may throw anything, and unchecked adapts it to an ordinary Function by wrapping the exception, so the adapted lambda can be passed to any API that expects one (Function). Using both:
TriFunction<Integer, Integer, Integer, Integer> volume = (w, h, d) -> w * h * d;
System.out.println("TriFunction volume(2,3,4) = " + volume.apply(2, 3, 4));

Path file = Files.createTempFile("fi-demo", ".txt");
Files.writeString(file, "contents");
Function<Path, String> read = ThrowingFunction.unchecked(Files::readString);
System.out.println("ThrowingFunction read     = " + read.apply(file));
Files.delete(file);
try {
    read.apply(file);
} catch (RuntimeException e) {
    System.out.println("after delete              = " + e.getCause().getClass().getSimpleName());
}
TriFunction volume(2,3,4) = 24
ThrowingFunction read     = contents
after delete              = NoSuchFileException
The last line shows the cost of the adapter: the failure now arrives as a RuntimeException and the real exception, NoSuchFileException, is its cause. That is a design decision, not a free lunch. Wrapping is reasonable when the caller cannot do anything useful with the checked exception anyway, and it is a bad idea when the caller is expected to handle it.

Where you will meet them in the Streams API

This page was the prerequisite; here is what it buys. Every operation in a stream pipeline takes one of the shapes above, and once you can name the shape you can predict the lambda. The list below is not copied from documentation: a program asked the running JDK for the parameter types of the methods, and printed them (Function, Predicate).
Stream.filter(Predicate)
Stream.map(Function)
Stream.mapToInt(ToIntFunction)
Stream.flatMap(Function)
Stream.forEach(Consumer)
Stream.anyMatch(Predicate)
Stream.sorted(Comparator)
Stream.generate(Supplier)
Stream.reduce(BinaryOperator)
IntStream.map(IntUnaryOperator)
IntStream.filter(IntPredicate)
Collectors.toMap(Function, Function, BinaryOperator)
Collectors.groupingBy(Function)
Collectors.partitioningBy(Predicate)
Read down the list. filter and anyMatch ask a yes/no question, so they take a Predicate. map and flatMap convert, so they take a Function. forEach uses a value and returns nothing, so it takes a Consumer. generate produces from nothing, so it takes a Supplier. reduce combines two of a type into that type, which makes it a BinaryOperator. sorted puts two values in order, a Comparator. toMap takes two Functions, one for the key and one for the value, and a BinaryOperator for what to do when two elements produce the same key, which is the shape behind the IllegalStateException that Java Streams API Deep Dive + Collectors Cookbook explains. The full API, operation by operation, is in Java Streams API: The Complete Reference Guide.

FAQs

Is a lambda the same as an anonymous class?

No. They behave the same for most code, but this means the enclosing object inside a lambda and the anonymous object inside an anonymous class, and the JDK compiles a lambda to a single invokedynamic instruction rather than a class file of its own. The output for both is in the section on what happens inside a lambda.

Do I have to write @FunctionalInterface?

No. Any interface with exactly one abstract method can be the target of a lambda whether or not it carries the annotation. The annotation makes the compiler check that the interface stays that way, which is the fourth error above.

How many functional interfaces does the JDK have?

java.util.function has 43 on Java 25 and on JDK 27, counted by reading the package from the running JDK. That does not include ones elsewhere, such as Runnable, Callable and Comparator, or the many in third-party libraries and in Spring.

Can a functional interface be generic, or have default methods?

Yes to both. Function<T, R> is generic, and its andThen and compose are default methods. Only the count of abstract methods matters (JLS 9.8, Functional Interfaces).

When should I use IntPredicate or ToIntFunction instead of Predicate<Integer> or Function<T, Integer>?

When the value is an int, long or double and the API offers the choice, such as mapToInt on a stream. It avoids the box and unbox calls shown in the boxing section. This page did not benchmark the difference, so treat it as removing work the compiler would otherwise emit rather than as a measured speed-up.

What is the difference between Runnable, Callable and Supplier?

Runnable takes nothing and returns nothing. Supplier takes nothing and returns a value. Callable is a Supplier that is allowed to throw a checked exception, and it lives in java.util.concurrent because executors are where you need that.

Conclusion

A lambda is an object of an interface with one abstract method, its type comes from where it is written, and the JDK already has an interface for each shape you are likely to need. The four things to keep are the tree of questions for choosing an interface, andThen against compose, the fact that Integer and int versions are different interfaces, and the five error messages. With those in hand, Java Streams API: The Complete Reference Guide and Java Streams API Deep Dive + Collectors Cookbook stop being lists of methods and become the same five shapes in different places.
Should you write your own functional interfaces? Rarely. Reach for a JDK interface first, and write your own only for three or more inputs, a checked exception, or a domain name that will be read many times. Do not write a custom interface whose only difference from Function is its name, since it costs you every combinator and makes readers stop and look it up. And do not adapt checked exceptions to unchecked ones everywhere by reflex: wrapping hides a failure the caller may need to handle.

Further Reading

No Comments yet!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.