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.
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:
defaultmethods (they have a body).staticmethods (they have a body too).- abstract methods that redeclare a public method of
Object, likeequals. every object already has those, so a lambda never needs to supply them. that's whyComparatorstill counts even though it declaresboolean equals(Object).
@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"));
}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.
error: Unexpected @FunctionalInterface annotation
Shout is not a functional interface
multiple non-overriding abstract methods found in interface Shoutcomposing them
the interfaces come with default methods that glue them together. the one that trips people up is the order.
Function<Integer, Integer> plus1 = x -> x + 1;
Function<Integer, Integer> times2 = x -> x * 2;
IO.println(plus1.andThen(times2).apply(5));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));
}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 → | nothing | boolean | int | long | double | an object |
|---|---|---|---|---|---|---|
| () | · | |||||
| (T) | ||||||
| (T, U) | ||||||
| (int) | ||||||
| (long) | ||||||
| (double) | ||||||
| (int, int) | · | · | · | · | · | |
| (long, long) | · | · | · | · | · | |
| (double, double) | · | · | · | · | · | |
| (T, int) | · | · | · | · | · | |
| (T, long) | · | · | · | · | · | |
| (T, double) | · | · | · | · | · |
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.
IntPredicatetests anint. - To...
- the output is that primitive.
ToIntFunction<T>turns aTinto anint. - IntToLong...
- both:
intin,longout. - 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.
gotcha: same shape, different type
- functional interface = one abstract method. defaults, statics and
Object's methods don't count. - supply, consume, transform, test.
Bifor two inputs,Operatorwhen in and out match. andThenruns left to right,composeruns 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.