skip to content
everything.functional.functionalInterfaces

functional interfaces

Function, Supplier, Consumer, Predicate and the rest of the zoo.

chapter
chapter 07.02
reading time
7 min read

a functional interface is any interface with exactly one abstract method. that one method is what a lambda becomes.

java.util.function ships 43 of them, but don't panic: it's four shapes (supply, consume, transform, test), times one or two inputs, times a few primitive versions.

the picture

you know how old libraries each shipped their own Callback, Listener, Handler and Transformer, all with one method, all slightly different, none of them interchangeable? java 8 fixed that by standardizing the common shapes once.

think of plug standards. you don't design a new socket for every lamp, you pick the one that matches the pins. here the pins are "what goes in" and "what comes out". pick those two and the interface picks itself.

takes =
returns =
methodR apply(T t)exampleFunction<String, Integer> len = s -> s.length();the JDK uses it inStream.map, Optional.map, Map.computeIfAbsentdefaultsandThen, compose, identitynotethe general case: one thing in, another thing out.
filter by shape, or click a card. circles are inputs, the square is what comes out, the diamond is a boolean, a dashed outline means "nothing". the bottom three live outside java.util.function but have the same job.

the names are verbs once you see them: a Supplier supplies, a Consumer consumes, a Function transforms, a Predicate tests. Bi means two inputs. Operator means the output is the same type as the input.

the one rule

an interface is functional if it has exactly one abstract method. that's the whole definition. things that don't count toward the one:

  • default methods (they have a body).
  • static methods (they have a body too).
  • abstract methods that redeclare a public method of Object, like equals. every object already has those, so a lambda never needs to supply them. that's why Comparator still counts even though it declares boolean equals(Object).
Main.java
@FunctionalInterface
interface Validator {
    boolean check(String s);                       // the one abstract method
 
    default Validator and(Validator other) {       // defaults don't count
        return s -> check(s) && other.check(s);
    }
    static Validator nonBlank() {                  // statics don't count
        return s -> !s.isBlank();
    }
    boolean equals(Object o);                      // Object's methods don't count
}
 
void main() {
    Validator v = Validator.nonBlank().and(s -> s.length() <= 8);
    IO.println(v.check("duke"));
    IO.println(v.check("   "));
    IO.println(v.check("a very long name"));
}
output
true
false
false

@FunctionalInterface is optional. any interface with one abstract method works as a lambda target without it. the annotation is a promise you ask the compiler to enforce: add a second abstract method later and the build breaks right there, instead of in every file that used a lambda.

text
error: Unexpected @FunctionalInterface annotation
  Shout is not a functional interface
    multiple non-overriding abstract methods found in interface Shout

composing them

the interfaces come with default methods that glue them together. the one that trips people up is the order.

predict the output
java
Function<Integer, Integer> plus1 = x -> x + 1;
Function<Integer, Integer> times2 = x -> x * 2;
IO.println(plus1.andThen(times2).apply(5));
Main.java
import java.util.*;
import java.util.function.*;
 
void main() {
    Function<Integer, Integer> plus1 = x -> x + 1;
    Function<Integer, Integer> times2 = x -> x * 2;
    IO.println(plus1.andThen(times2).apply(5));   // times2(plus1(5))
    IO.println(plus1.compose(times2).apply(5));   // plus1(times2(5))
 
    Predicate<String> empty = s -> s.isEmpty();
    Predicate<String> longish = s -> s.length() > 3;
    IO.println(empty.negate().and(longish).test("duke"));
    IO.println(Predicate.not(empty).or(longish).test(""));
 
    Consumer<List<String>> addA = l -> l.add("a");
    var list = new ArrayList<String>();
    addA.andThen(l -> l.add("b")).accept(list);    // both run, in order
    IO.println(list);
 
    BinaryOperator<Integer> max = BinaryOperator.maxBy(Comparator.naturalOrder());
    IO.println(max.apply(3, 9));
}
output
12
11
true
false
[a, b]
9

Predicate.not (java 11) is the one you'll use most, because it reads well inside a stream: .filter(Predicate.not(String::isBlank)).

the primitive ones

the big table looks scary because of primitives. a Function<Integer, Integer> boxes its argument and its result on every call, which in a hot loop means millions of tiny Integer objects. so the JDK ships primitive versions of the common shapes.

in ↓ out →nothingbooleanintlongdoublean object
()·
(T)
(T, U)
(int)
(long)
(double)
(int, int)··
···
(long, long)···
··
(double, double)····
·
(T, int)
·····
(T, long)
·····
(T, double)
·····
interface ToIntFunction<T> { int applyAsInt(T value); }43 interfaces. the dots don't exist.
all 43 interfaces in java.util.function, by shape. rows are what goes in, columns are what comes out. click a name to see its one method. UnaryOperator and BinaryOperator share a cell with the function they specialize.

the naming is regular enough to guess:

Int..., Long..., Double...
the input is that primitive. IntPredicate tests an int.
To...
the output is that primitive. ToIntFunction<T> turns a T into an int.
IntToLong...
both: int in, long out.
Obj...Consumer
an object and a primitive in, nothing out. ObjIntConsumer<T>.
...AsInt
the method name changes when the result is primitive: applyAsInt, getAsInt. the names have to differ, or one class couldn't implement two of them.

notice the dots. there's no IntBiPredicate, no BooleanFunction, nothing for char, byte, short or float. the JDK stopped at the shapes streams actually need. chars travel as ints ("hi".chars() is an IntStream), and for anything else you write a tiny interface of your own.

quick check
you need a lambda that takes two ints and returns their sum, with no boxing. which type?

gotcha: same shape, different type

keylines
  • functional interface = one abstract method. defaults, statics and Object's methods don't count.
  • supply, consume, transform, test. Bi for two inputs, Operator when in and out match.
  • andThen runs left to right, compose runs inside out.
  • primitive versions exist so hot code doesn't box. the naming is mechanical once you see it.
  • same shape isn't the same type. bridge with a method reference.