
A functional interface is an interface with exactly one abstract method. That one method is what a lambda expression implements: book -> book.year() < 1850 has no type on its own – as a Predicate<Book>, it becomes an object with a method test() that returns true or false for a book.
Functional interfaces have existed since Java 8, together with lambda expressions. The JDK ships 43 of them in the package java.util.function – from Function and Predicate to ObjDoubleConsumer. In this article, I show you which one to use when, how to read their names, and when to write one of your own.
In this article, you will find out
- what a functional interface is, and which methods do not count toward the one abstract method,
- what the
@FunctionalInterfaceannotation checks, - which interfaces the package
java.util.functioncontains, and how to tell from their names what they do, - how to combine functional interfaces with
andThen(),compose(),and(),or(), andnegate(), - what
? super Tand? extends Rmean in the signatures of the Stream API, - which functional interfaces exist outside of
java.util.function, - when to write a functional interface of your own,
- which mistakes to avoid.
The Examples in This Article
The examples use the data model of the article about Java Streams – an enum Genre, a record Book, and a small library of eleven classics:
public enum Genre {
NOVEL,
GOTHIC,
ADVENTURE,
FANTASY,
SCIENCE_FICTION
}
public record Book(String title, String author, int year, Genre genre) {}
public class Library {
public static final List<Book> BOOKS = List.of(
new Book("Pride and Prejudice", "Jane Austen", 1813, NOVEL),
new Book("Frankenstein", "Mary Shelley", 1818, GOTHIC),
new Book("Moby-Dick", "Herman Melville", 1851, ADVENTURE),
new Book("From the Earth to the Moon", "Jules Verne", 1865, SCIENCE_FICTION),
new Book("Alice's Adventures in Wonderland", "Lewis Carroll", 1865, FANTASY),
new Book("Around the World in Eighty Days", "Jules Verne", 1873, ADVENTURE),
new Book("Treasure Island", "Robert Louis Stevenson", 1883, ADVENTURE),
new Book("Kidnapped", "Robert Louis Stevenson", 1886, ADVENTURE),
new Book("The Time Machine", "H. G. Wells", 1895, SCIENCE_FICTION),
new Book("Dracula", "Bram Stoker", 1897, GOTHIC),
new Book("The War of the Worlds", "H. G. Wells", 1898, SCIENCE_FICTION));
}
You can find the complete code of all examples in the GitHub repository java-streams-examples, in the package eu.happycoders.functionalinterfaces.
What Is a Functional Interface?
A functional interface is an ordinary Java interface that declares exactly one abstract method. The simplest example in the JDK is Runnable:
@FunctionalInterface
public interface Runnable {
void run();
}
One method, run(), without parameters and without a return value – nothing else. I explain the annotation in the next chapter; it plays no role in the definition. A lambda is the shortest implementation of that one method. In the following example, it prints the title of the first book:
Runnable printFirstTitle = () -> System.out.println(BOOKS.getFirst().title());
printFirstTitle.run();
Pride and Prejudice
You call the method run() on the lambda just as on any other object that implements Runnable.
A functional interface may have more than one method, though – as long as only one of them is abstract. Comparator is such a case, shortened here to the methods that show the rules:
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
boolean equals(Object obj);
default Comparator<T> reversed() { … }
default Comparator<T> thenComparing(Comparator<? super T> other) { … }
static <T, U> Comparator<T> comparing(
Function<? super T, ? extends U> keyExtractor) { … }
// … and 15 more default and static methods
}
Only compare() is abstract. The remaining methods do not count, for three reasons:
defaultmethods likereversed()andthenComparing()have a body and are therefore not abstract.staticmethods likecomparing()belong to the interface, not to its instances.equals()is declared abstract but still does not count: It is a method ofObject, and every class inherits an implementation from there – so a lambda does not have to provide it. The Java Language Specification explicitly excludes such methods from the count.Comparatordeclaresequals()only to describe in its Javadoc when two comparators are equal.
An abstract method that the interface inherits from a super-interface counts as well: An empty interface Sub extends Runnable is functional; its one method is run(). If an interface inherits the same method from two super-interfaces, it counts once.
Whether an interface is functional is decided solely by the number of its abstract methods – not by the annotation, not by the package, not by its age: Comparator has existed since Java 1.2, and since Java 8 you can implement it with a lambda – its abstract method has stayed the same, only the default and static methods were added with Java 8.
A lambda is the shortest implementation of the one abstract method, for Comparator just as for Runnable. In the following example, a named class implements Comparator<Book> the classic way, and a lambda does the same in one line:
public class YearComparator implements Comparator<Book> {
@Override
public int compare(Book a, Book b) {
return Integer.compare(a.year(), b.year());
}
}
// An instance of the named class – needs the class from the block above as well
Comparator<Book> byYearClass = new YearComparator();
// The lambda is the complete implementation – it needs no class
Comparator<Book> byYearLambda = (a, b) -> Integer.compare(a.year(), b.year());
List<Book> books = new ArrayList<>(BOOKS);
books.sort(byYearClass);
books.sort(byYearLambda);
Both sort calls produce the same order, because sort() is passed an object with a compare() method in both cases – and both compare() methods compare the same thing, with Integer.compare(a.year(), b.year()). You can also call the compare() method yourself, like any other method:
Book frankenstein = BOOKS.get(1);
Book dracula = BOOKS.get(9);
System.out.println(byYearLambda.compare(frankenstein, dracula));
System.out.println(byYearLambda.compare(dracula, frankenstein));
-1
1
The default methods are available on the lambda as well: byYearLambda.reversed(), for example, sorts in descending order, Comparator.comparing(Book::author).thenComparing(byYearLambda) first by author and then by year.
The @FunctionalInterface Annotation
With the @FunctionalInterface annotation, you tell the compiler that an interface is meant to be functional. The compiler then checks that it is an interface (not a class, not an enum, not an annotation) and that it has exactly one abstract method.
The following interface TitleFormatter from the example code is such an annotated interface. It formats a book as a string:
@FunctionalInterface
public interface TitleFormatter {
String format(Book book);
}
TitleFormatter withYear = book -> book.title() + " (" + book.year() + ")";
System.out.println(withYear.format(BOOKS.get(9)));
Dracula (1897)
If you add a second abstract method to the interface, it no longer compiles:
@FunctionalInterface
public interface TitleFormatter {
String format(Book book);
String formatShort(Book book); // does not compile
}
error: Unexpected @FunctionalInterface annotation
TitleFormatter is not a functional interface
multiple non-overriding abstract methods found in interface TitleFormatter
Without the annotation, the second method would compile – and instead, every lambda of type TitleFormatter in the whole project would stop compiling, with an error that does not point at the cause. The annotation moves the error to where it originates: into the interface.
For the compiler, the annotation is not a requirement: It treats every interface with exactly one abstract method as functional, annotated or not. I recommend annotating every functional interface of your own anyway. The annotation documents that the interface is meant for lambdas, and the compiler makes sure it stays that way – the same division of labor as with @Override: A method overrides the inherited method without the annotation, too, but with it, the compiler reports when it no longer does.
In the JDK, all 43 interfaces in java.util.function are annotated, as are Runnable, Callable, and Comparator.
The Package java.util.function
Before Java 8, every API brought its own interface for passing behavior: Runnable for threads, Comparator for sorting, FileFilter for directory listings. With lambdas came a package of general-purpose functional interfaces that every API can use: java.util.function.
In Java 27, the package contains exactly 43 interfaces. That sounds like a lot, but it follows a simple scheme: four base forms, plus variants with two parameters (for all but Supplier), with the same type as input and output (for Function and BiFunction), and with primitive types instead of objects (for all four). I show you the four base forms first, because they are what you write most of your code with:
| Interface | Method | The lambda … |
|---|---|---|
Function<T, R> | R apply(T t) | takes a T and returns an R |
Predicate<T> | boolean test(T t) | takes a T and returns true or false |
Consumer<T> | void accept(T t) | takes a T and returns nothing |
Supplier<T> | T get() | takes nothing and returns a T |
The method names in the second column – apply(), test(), accept(), get() – are the methods that are called wherever you pass the lambda to: filter() calls test() on its Predicate, map() calls apply() on its Function. You rarely call them yourself – in the examples of this chapter, I do it anyway, so that you can see what the method returns.
The following four sections show each of the four interfaces with its call, the default methods for combining, and the places in the JDK that expect it.
Function<T, R> – One Value In, Another One Out
A Function maps one value to another. map() expects a Function, as do flatMap(), Collectors.groupingBy(), and Optional.map(), among others.
In the following example, the function is a method reference to Book.title(), and apply() calls it for the book “Dracula”:
Book dracula = BOOKS.get(9);
Function<Book, String> title = Book::title;
System.out.println(title.apply(dracula));
Dracula
You can chain two functions into one: andThen() runs the second after the first, compose() before it. In the following example, title.andThen(length) returns the length of the title – and length.compose(title) is the same function, just written the other way around:
Function<Book, String> title = Book::title;
Function<String, Integer> length = String::length;
Function<Book, Integer> titleLength = title.andThen(length);
System.out.println(titleLength.apply(dracula));
Function<Book, Integer> titleLengthComposed = length.compose(title);
System.out.println(titleLengthComposed.apply(dracula));
7
7
I recommend andThen(), because it reads in the order in which the functions are executed: first title, then length.
The static method Function.identity() returns a function that returns its argument unchanged. You need it where an API demands a function but you want the element as it is – e.g., in a map whose key is the title and whose value is the book itself:
Map<String, Book> byTitle = BOOKS.stream()
.collect(Collectors.toMap(Book::title, Function.identity()));
book -> book would do the same; Function.identity() says it by name.
Predicate<T> – A Yes-or-No Question
A Predicate answers a question about a value with true or false. filter() expects a Predicate, as do anyMatch(), allMatch(), noneMatch(), takeWhile(), dropWhile(), and Collection.removeIf(), among others.
In the following example, the predicate asks whether a book is a gothic novel:
Book dracula = BOOKS.get(9);
Predicate<Book> isGothic = book -> book.genre() == GOTHIC;
System.out.println(isGothic.test(dracula));
true
You can combine predicates with and(), or(), and negate() like conditions with &&, ||, and !. The following helper method titles() filters the library with a predicate and returns the titles; the three calls below it combine isGothic with a second predicate before1850:
static List<String> titles(Predicate<Book> condition) {
return BOOKS.stream().filter(condition).map(Book::title).toList();
}
Predicate<Book> isGothic = book -> book.genre() == GOTHIC;
Predicate<Book> before1850 = book -> book.year() < 1850;
System.out.println(titles(isGothic.and(before1850)));
System.out.println(titles(isGothic.or(before1850)));
System.out.println(titles(isGothic.negate()));
[Frankenstein]
[Pride and Prejudice, Frankenstein, Dracula]
[Pride and Prejudice, Moby-Dick, From the Earth to the Moon, Alice's Adventures in Wonderland, Around the World in Eighty Days, Treasure Island, Kidnapped, The Time Machine, The War of the Worlds]
Since Java 11, there is also the static method Predicate.not(). It does the same as negate(), but it also works with a method reference, which has no type on its own and therefore cannot have a method called on it: filter(Predicate.not(String::isBlank)) compiles, filter(String::isBlank.negate()) does not.
The second static method, Predicate.isEqual(value), returns a predicate that checks its parameter against the given value with equals() – Predicate.isEqual(NOVEL), for example, is true for the genre NOVEL and false for all other genres.
Consumer<T> – One Value In, Nothing Out
A Consumer takes a value and does something with it – usually it prints it, stores it, or sends it on. forEach() expects a Consumer, as do peek(), Optional.ifPresent(), and Iterable.forEach(), among others.
In the following example, the consumer prints the title of a book:
Book dracula = BOOKS.get(9);
Consumer<Book> printTitle = book -> System.out.println(book.title());
printTitle.accept(dracula);
Dracula
With andThen(), you chain two consumers into one that runs both in turn. In the following example, the chained consumer prints the title of every gothic novel first and then, indented, its year:
Consumer<Book> printTitle = book -> System.out.println(book.title());
Consumer<Book> printYear = book -> System.out.println(" " + book.year());
BOOKS.stream().filter(isGothic).forEach(printTitle.andThen(printYear));
Frankenstein
1818
Dracula
1897
A consumer is the one place where a lambda is supposed to have a side effect – unlike in filter() and map(), whose lambdas should only compute a value from their parameters. What happens if you do not stick to that is shown in the section on side effects in lambdas in the streams article.
Supplier<T> – Nothing In, One Value Out
A Supplier returns a value without taking one. What do you need that for? To postpone the creation of a value to the moment it is needed – or to avoid it altogether.
In the following example, the supplier returns the first book of the library, but only when get() is called:
Supplier<Book> firstBook = () -> BOOKS.getFirst();
System.out.println(firstBook.get().title());
Pride and Prejudice
One place where you can see the difference is Optional.orElseGet(): It calls the supplier only if the Optional is empty. In the following example, findByTitle() from the article about Java Optional looks for a book the library does not contain:
Optional<Book> emma = Library.findByTitle("Emma");
Book fallback = emma.orElseGet(() -> new Book("Emma", "Jane Austen", 1815, NOVEL));
System.out.println(fallback);
Book[title=Emma, author=Jane Austen, year=1815, genre=NOVEL]
With orElse(new Book("Emma", …)) instead of orElseGet(), the fallback book would be created on every call – even when findByTitle() has found the book and the fallback is discarded.
The second place where a supplier makes sense is Collectors.toCollection(): The collector calls the supplier (in the following example, the constructor reference TreeSet::new) to create the collection it collects the elements into. The following example collects the titles in a TreeSet, i.e., sorted:
TreeSet<String> sortedTitles = BOOKS.stream()
.map(Book::title)
.collect(Collectors.toCollection(TreeSet::new));
Further places that expect a supplier are Optional.orElseThrow() for the exception, Stream.generate() for the elements of an infinite stream, and Objects.requireNonNullElseGet() for the fallback value. Supplier has no default methods – there is nothing a supplier could be chained with.
UnaryOperator<T> and BinaryOperator<T> – The Same Type In and Out
A UnaryOperator<T> is a Function<T, T>, a BinaryOperator<T> a BiFunction<T, T, T>: Input and output have the same type. Both interfaces extend their general form. What they add is the shorter name, the guarantee that a value of type T comes out where one goes in – and three static methods: UnaryOperator.identity() as well as BinaryOperator.minBy() and maxBy(), which I show in a moment.
List.replaceAll() expects a UnaryOperator: It replaces every element with the result of the operator. The following example converts all titles to upper case:
UnaryOperator<String> upper = String::toUpperCase;
List<String> titles = new ArrayList<>(BOOKS.stream().map(Book::title).toList());
titles.replaceAll(upper);
System.out.println(titles.subList(0, 3));
[PRIDE AND PREJUDICE, FRANKENSTEIN, MOBY-DICK]
A BinaryOperator is expected by reduce(), which turns two elements into one – until one is left. The following example adds up the publication years of all books:
BinaryOperator<Integer> sum = Integer::sum;
int yearSum = BOOKS.stream().map(Book::year).reduce(0, sum);
System.out.println(yearSum);
20544
The static methods BinaryOperator.minBy() and maxBy() return an operator that returns the smaller or the larger of two values according to a Comparator. With reduce(), the following example finds the most recently published book that way:
BinaryOperator<Book> later =
BinaryOperator.maxBy(Comparator.comparingInt(Book::year));
Optional<Book> latest = BOOKS.stream().reduce(later);
System.out.println(latest.map(Book::title).orElse("-"));
The War of the Worlds
Map.merge() also expects a BinaryOperator, for the case that the key already has a value. The following example counts the books per author – for the first hit, merge() stores the 1; for every further one, Integer::sum adds it to the existing value:
Map<String, Integer> booksPerAuthor = new TreeMap<>();
for (Book book : BOOKS) {
booksPerAuthor.merge(book.author(), 1, Integer::sum);
}
System.out.println(booksPerAuthor);
{Bram Stoker=1, H. G. Wells=2, Herman Melville=1, Jane Austen=1, Jules Verne=2, Lewis Carroll=1, Mary Shelley=1, Robert Louis Stevenson=2}
By the way, Integer::sum fits an IntBinaryOperator just as well, which reduce() on an IntStream expects – the same method reference, a different interface. How that works is shown in the section on the primitive specializations.
Two Parameters: BiFunction, BiPredicate, and BiConsumer
For lambdas with two parameters, there are three variants of the base forms with the prefix Bi: BiFunction<T, U, R> with R apply(T t, U u), BiPredicate<T, U> with boolean test(T t, U u), and BiConsumer<T, U> with void accept(T t, U u). There is no BiSupplier – a supplier has no parameters it could double.
You meet BiConsumer and BiFunction most often in Map, whose methods pass key and value together. The following example groups the books by author and prints the count per author with Map.forEach(), which expects a BiConsumer:
Map<String, List<Book>> byAuthor = BOOKS.stream()
.collect(Collectors.groupingBy(Book::author, TreeMap::new, Collectors.toList()));
BiConsumer<String, List<Book>> printCount =
(author, books) -> System.out.println(author + ": " + books.size());
byAuthor.forEach(printCount);
Bram Stoker: 1
H. G. Wells: 2
Herman Melville: 1
Jane Austen: 1
Jules Verne: 2
Lewis Carroll: 1
Mary Shelley: 1
Robert Louis Stevenson: 2
A BiFunction is expected by Map.computeIfPresent() and Map.replaceAll(): Both pass the key and the old value and store the return value as the new value. The following example increments the counter for Jules Verne from the booksPerAuthor map of the previous section by one and then multiplies all counters by ten:
booksPerAuthor.computeIfPresent("Jules Verne", (author, count) -> count + 1);
System.out.println(booksPerAuthor.get("Jules Verne"));
booksPerAuthor.replaceAll((author, count) -> count * 10);
System.out.println(booksPerAuthor.get("Jules Verne"));
3
30
A BiPredicate is expected in the JDK by Files.find(), for example, which passes the path and its attributes for every entry of a directory tree. The following example counts the Java files under src/main/java:
BiPredicate<Path, BasicFileAttributes> isJavaFile =
(path, attributes) ->
attributes.isRegularFile() && path.toString().endsWith(".java");
try (Stream<Path> files = Files.find(Path.of("src/main/java"), 10, isJavaFile)) {
System.out.println(files.count() + " Java files");
}
49 Java files
The Bi forms have default methods for combining, too:
BiFunction.andThen()appends aFunctionthat processes the return value of theBiFunction– from aBiFunction<String, Integer, String>that joins title and year into"Dracula (1897)",.andThen(String::toUpperCase)makes one that returns"DRACULA (1897)".BiPredicatehasand(),or(), andnegate(), likePredicate.BiConsumer.andThen()appends a secondBiConsumerthat receives the same two arguments.
There is no compose() on BiFunction – BiFunction expects two parameters, and compose() cannot deliver two return values.
Primitive Specializations: IntPredicate, ToIntFunction, and the Others
A Predicate<Integer> works for int values, too – but every call of test() wraps the int in an Integer object, because a type parameter cannot be a primitive type. This boxing costs an object of 16 bytes per value – the JVM takes only the values from −128 to 127 from a cache and creates all others on demand. For a stream with millions of values, that is millions of objects the garbage collector has to collect again.
That is why there are dedicated interfaces for int, long, and double whose methods work with the primitive types. The primitive streams IntStream, LongStream, and DoubleStream expect them in their operations – and that is where you meet most of the 43 interfaces.
Their names follow a scheme you only have to learn once. The following diagram shows it on six names from java.util.function – the parts of the name that describe the parameters in blue, the parts for the return type in light blue, and next to them, parameters, return type, and base word spelled out:
The scheme in three rules:
- A prefix
Int,Long, orDoublenames the type of a parameter:IntPredicatetests anint,IntFunction<R>maps anintto anR,IntConsumertakes anint. Toplus a type names the return type:ToIntFunction<T>maps aTto anint,IntToLongFunctionanintto along.Bistands for two object parameters,Objfor one object parameter next to a primitive one:ToIntBiFunction<T, U>maps aTand aUto anint,ObjIntConsumer<T>takes aTand anint.
The base word at the end – Function, Predicate, Consumer, Supplier, UnaryOperator, BinaryOperator – has the same meaning as in the object forms: IntUnaryOperator maps an int to an int, IntBinaryOperator two ints to one, IntSupplier returns an int without parameters.
The following examples show six of these interfaces on an IntStream of the publication years of the books in our library. mapToInt() converts the Stream<Book> into this IntStream and expects a ToIntFunction<Book> for that; the operations filter(), map(), mapToObj(), reduce(), and collect() of the IntStream expect the five further interfaces – the variable declaration above each call says which one:
ToIntFunction<Book> year = Book::year;
int newest = BOOKS.stream().mapToInt(year).max().orElseThrow();
System.out.println(newest);
IntPredicate nineteenthCentury = y -> y >= 1801 && y <= 1900;
System.out.println(
BOOKS.stream().mapToInt(Book::year).filter(nineteenthCentury).count());
IntUnaryOperator decade = y -> y / 10 * 10;
System.out.println(
BOOKS.stream().mapToInt(Book::year).map(decade).distinct().boxed().toList());
IntFunction<String> century = y -> ((y - 1) / 100 + 1) + "th century";
System.out.println(
BOOKS.stream().mapToInt(Book::year).mapToObj(century).distinct().toList());
IntBinaryOperator max = Math::max;
System.out.println(BOOKS.stream().mapToInt(Book::year).reduce(0, max));
ObjIntConsumer<StringBuilder> appendYear = (sb, y) -> sb.append(y).append(' ');
StringBuilder years = BOOKS.stream()
.mapToInt(Book::year)
.limit(3)
.collect(StringBuilder::new, appendYear, StringBuilder::append);
System.out.println(years.toString().trim());
1898
11
[1810, 1850, 1860, 1870, 1880, 1890]
[19th century]
1898
1813 1818 1851
In this listing, the method reference Book::year stands for a ToIntFunction<Book>; in the section about Function, it stood for a Function<Book, Integer> – the same reference, two types. Which one applies is decided by the target type, i.e., the method you pass the reference to: mapToInt() expects the ToIntFunction, map() the Function.
All 43 Interfaces at a Glance
The following two tables show all 43 interfaces of the package, arranged by input (rows) and output (columns). The type parameters are left out – Function stands for Function<T, R>, BiPredicate for BiPredicate<T, U>. UnaryOperator and BinaryOperator are shown in parentheses after the Function they are a special case of, and ✗ means that there is no interface for this combination.
The first table contains the interfaces that return an object, a boolean, or nothing:
| Input ↓ Output → | Object | boolean | none |
|---|---|---|---|
| Object | Function (UnaryOperator) | Predicate | Consumer |
int | IntFunction | IntPredicate | IntConsumer |
long | LongFunction | LongPredicate | LongConsumer |
double | DoubleFunction | DoublePredicate | DoubleConsumer |
| none | Supplier | BooleanSupplier | ✗ |
| two objects | BiFunction (BinaryOperator) | BiPredicate | BiConsumer |
Object, int | ✗ | ✗ | ObjIntConsumer |
Object, long | ✗ | ✗ | ObjLongConsumer |
Object, double | ✗ | ✗ | ObjDoubleConsumer |
The second table contains the interfaces that return a primitive numeric type:
| Input ↓ Output → | int | long | double |
|---|---|---|---|
| Object | ToIntFunction | ToLongFunction | ToDoubleFunction |
int | IntUnaryOperator | IntToLongFunction | IntToDoubleFunction |
long | LongToIntFunction | LongUnaryOperator | LongToDoubleFunction |
double | DoubleToIntFunction | DoubleToLongFunction | DoubleUnaryOperator |
| none | IntSupplier | LongSupplier | DoubleSupplier |
| two objects | ToIntBiFunction | ToLongBiFunction | ToDoubleBiFunction |
int, int | IntBinaryOperator | ✗ | ✗ |
long, long | ✗ | LongBinaryOperator | ✗ |
double, double | ✗ | ✗ | DoubleBinaryOperator |
The gaps in the tables are intentional: According to the package’s Javadoc, the JDK developers did not include a complete set of all shapes, but enough to cover the common requirements. For the ✗ in the first table, row “none” and column “none” – no parameter, no return value – there is Runnable in java.lang; for everything else – e.g., an IntBiFunction, a BooleanPredicate, or a function with three parameters – you write an interface of your own, as the section Writing Your Own Functional Interfaces shows.
? super T and ? extends R: Reading the Signatures in the Javadoc
The Javadoc of the Stream API does not say filter(Predicate<T> predicate), but:
Stream<T> filter(Predicate<? super T> predicate);
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
The wildcards make the methods more generous than the simple form would be. Predicate<? super T> means: a predicate for T or for a supertype of T. A Predicate<Object> can test any object, and therefore any book – and that is why it may filter a Stream<Book>:
Predicate<Object> nonNull = Objects::nonNull;
List<Book> withGap = new ArrayList<>(BOOKS);
withGap.add(null);
System.out.println(withGap.stream().filter(nonNull).count());
11
If filter() were declared without the wildcard, i.e., as filter(Predicate<T> predicate), the same call would not compile – a Predicate<Object> is not a Predicate<Book>, even though it can test every book. That is because generic types are invariant in Java: Predicate<Object> is neither a subtype nor a supertype of Predicate<Book>, although Object is the supertype of Book.
error: incompatible types: Predicate<Object> cannot be converted to Predicate<Book>
For map(), the same applies to the input parameter of the function: ? super T admits a function that takes an Object. And ? extends R allows it to return a subtype of R as its return value.
In the following example, a function uses both freedoms: It takes an Object, and therefore any book, and returns a StringBuilder – and map() still returns the Stream<CharSequence> the variable demands, because a StringBuilder is a CharSequence:
Function<Object, StringBuilder> describe = o -> new StringBuilder(o.toString());
Stream<CharSequence> descriptions = BOOKS.stream().map(describe);
System.out.println(descriptions.findFirst().orElseThrow());
Book[title=Pride and Prejudice, author=Jane Austen, year=1813, genre=NOVEL]
Without the wildcards, the function would have to be exactly a Function<Book, CharSequence> – neither Object as the parameter type nor StringBuilder as the return type would be allowed.
For the lambdas you write directly in filter() and map(), the wildcards change nothing: The compiler derives their type from the target type, and that fits. They make a difference as soon as you have a predicate or a function in a variable declared for a supertype – or as soon as you write a method of your own that expects a functional interface.
Then I recommend the same form as in the JDK: Predicate<? super Book> instead of Predicate<Book>. Thanks to the wildcard, the following method select() accepts the Predicate<Object> from the nonNull example:
static List<Book> select(List<Book> books, Predicate<? super Book> condition) {
return books.stream().filter(condition).toList();
}
List<Book> nonNullBooks = select(withGap, nonNull);
With Predicate<Book> as the parameter type, the same call would not compile:
error: incompatible types: Predicate<Object> cannot be converted to Predicate<Book>
The mnemonic behind this is called PECS – “producer extends, consumer super”: A type parameter the method reads values from gets ? extends; one it puts values into gets ? super. The predicate has books put into it, hence super; the function produces values, hence extends for its return type.
Functional Interfaces Outside of java.util.function
java.util.function contains the general-purpose interfaces. Besides them, the JDK has interfaces with exactly one abstract method that serve a specific purpose and live in the package that uses them – most of them are older than lambdas and have fit them since Java 8 without any change. You meet four of them regularly:
Runnable(since Java 1.0,java.lang) withvoid run()is a task without a result – forThread,ExecutorService.execute(), andCompletableFuture.runAsync().Callable<V>(since Java 5,java.util.concurrent) withV call() throws Exceptionis a task with a result, and the only one of those named here that may throw a checked exception – forExecutorService.submit().Comparator<T>(since Java 1.2,java.util) withint compare(T a, T b)– forsort(),sorted(),min(), andmax(). How to build comparators withcomparing(),thenComparing(), andreversed()is shown in the article about Comparator and Comparable.FileFilter(since Java 1.2,java.io) withboolean accept(File file)– forFile.listFiles().
The following example shows the four, each as a lambda or a method reference:
Runnable task =
() -> System.out.println("running in " + Thread.currentThread().getName());
Thread thread = new Thread(task);
thread.start();
thread.join();
Callable<Integer> countBooks = () -> BOOKS.size();
try (ExecutorService executor = Executors.newSingleThreadExecutor()) {
Future<Integer> count = executor.submit(countBooks);
System.out.println(count.get());
}
List<Book> books = new ArrayList<>(BOOKS);
books.sort(Comparator.comparing(Book::author).thenComparing(Book::year));
System.out.println(books.stream().map(Book::title).toList().subList(0, 3));
FileFilter directories = File::isDirectory;
File[] packages = new File("src/main/java/eu/happycoders").listFiles(directories);
System.out.println(Arrays.stream(packages).map(File::getName).sorted().toList());
running in Thread-0
11
[Dracula, The Time Machine, The War of the Worlds]
[functionalinterfaces, lambdas, optional, streams]
Further examples from the JDK are DirectoryStream.Filter<T> with boolean accept(T entry) for Files.newDirectoryStream(), TemporalAdjuster with Temporal adjustInto(Temporal temporal) for LocalDate.with(), and Thread.UncaughtExceptionHandler with void uncaughtException(Thread t, Throwable e).
The rule applies just as much to libraries and to your own code: Wherever a method parameter is an interface with exactly one abstract method, a lambda fits – whether the interface is annotated or not.
Writing Your Own Functional Interfaces
Writing a functional interface is easy: an interface, one abstract method, the annotation. The question is not how you do it, but when – because most of the time, the JDK already has one that fits.
I recommend using the interfaces from java.util.function wherever one has the shape of your method. They are well known, every API accepts them, and they bring the combinators andThen(), and(), or(), and negate() with them.
An interface of your own pays off in three cases:
- The JDK has no matching shape – three parameters, a
booleanas input, twoints with abooleanas output. The tables in the section All 43 Interfaces at a Glance tell you whether that is so. - The method may throw a checked exception. None of the 43 methods declares one; how to deal with that is shown in the next section.
- The name carries a domain meaning – and the interface gets a contract or
defaultmethods of its own that aFunction<Book, String>would not have. That is also Joshua Bloch’s recommendation in Effective Java, Item 44: favor the standard interfaces, and write your own only with a descriptive name, a strong contract, or customdefaultmethods.
The third case – an interface of your own with the same signature as a general-purpose one from java.util.function, just for the sake of its name – has a price you should know before you decide on it:
The interface TitleFormatter from the section on the annotation has the same shape as a Function<Book, String> – and is still a different type for the compiler. That is because Java compares types by their name, not by their structure. So a TitleFormatter does not fit into map(), which expects a Function:
TitleFormatter withYear = book -> book.title() + " (" + book.year() + ")";
List<String> labels = BOOKS.stream().map(withYear).toList(); // does not compile
error: method map in interface Stream<T> cannot be applied to given types;
required: Function<? super Book,? extends R>
found: TitleFormatter
What fits is a method reference to its method: map(withYear::format). The reference is a new lambda of type Function that calls format(). The other way around works the same: A Function<Book, String> in the variable title becomes a TitleFormatter with TitleFormatter plain = title::apply:
List<String> labels = BOOKS.stream().map(withYear::format).toList();
Function<Book, String> title = Book::title;
TitleFormatter plain = title::apply;
So on its own, an interface of your own is nothing the Stream API or any other JDK API accepts; at every handover, the method reference has to stand in between. That is fine if the name is worth this small hurdle – and a reason to stay with Function when in doubt.
An Interface of Your Own for Checked Exceptions
A lambda may only throw the checked exceptions its abstract method declares – and none of the 43 methods in java.util.function declares one. Files.readAllLines() throws an IOException, so this pipeline does not compile:
List<Path> files = List.of(Path.of("README.md"), Path.of("pom.xml"));
List<List<String>> contents = files.stream()
.map(Files::readAllLines) // does not compile
.toList();
error: unreported exception IOException; must be caught or declared to be thrown
For the individual case, a helper method suffices that encapsulates exactly this call and wraps the exception in an unchecked exception; the article about Java Streams shows it in the section Checked Exceptions in Lambdas for Files.readAllLines(). But that helper method only applies to the one call – for Files.size() or Files.lines(), you would need another one each.
If you need this more often, you write the wrapper once, in general form: for a functional interface of your own whose method may declare the exception, and with a static method that wraps any such function in an ordinary Function:
@FunctionalInterface
public interface ThrowingFunction<T, R, E extends Exception> {
R apply(T t) throws E;
static <T, R> Function<T, R> unchecked(ThrowingFunction<T, R, ?> function) {
return t -> {
try {
return function.apply(t);
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new IllegalStateException(e);
}
};
}
}
ThrowingFunction is the functional interface: Its method apply() declares the exception E, which is why the lambda Files::readAllLines may throw it. The static method unchecked() takes such a function and returns an ordinary Function that wraps the checked exception in an IllegalStateException – and that one fits into map():
List<Integer> lineCounts = files.stream()
.map(unchecked(Files::readAllLines))
.map(List::size)
.toList();
The type parameter E ensures that ThrowingFunction<Path, List<String>, IOException> declares exactly the IOException and not a blanket Exception. So whoever calls the interface directly, instead of wrapping it with unchecked(), has to handle exactly that exception.
Common Mistakes and Recommendations
Function<Book, Boolean> Instead of Predicate<Book>
A lambda that returns true or false is a Predicate – even if it can be declared as a Function<Book, Boolean>. The Function has no negate(), no and(), and no or(), and filter() does not accept it.
The same goes for Function<Book, Void> instead of Consumer<Book>: The return type Void forces the lambda into a return null that means nothing.
And for Function<Integer, Integer> instead of UnaryOperator<Integer>: The operator already says in its type that the same type comes out that goes in.
I recommend choosing the type by the shape of the lambda: If it returns a boolean, it is a Predicate; if it returns nothing, a Consumer; if it takes nothing, a Supplier; if it returns the same type it takes, a UnaryOperator. Function is for the rest.
UnaryOperator<Integer> Instead of IntUnaryOperator
On an IntStream, you can apply a UnaryOperator<Integer> only after a boxed() – and every element is wrapped in an Integer object for that. The following listing shows both ways; both return the same years:
UnaryOperator<Integer> nextYearBoxed = year -> year + 1;
System.out.println(
BOOKS.stream().mapToInt(Book::year).boxed().map(nextYearBoxed).toList());
IntUnaryOperator nextYear = year -> year + 1;
System.out.println(
BOOKS.stream().mapToInt(Book::year).map(nextYear).boxed().toList());
[1814, 1819, 1852, 1866, 1866, 1874, 1884, 1887, 1896, 1898, 1899]
[1814, 1819, 1852, 1866, 1866, 1874, 1884, 1887, 1896, 1898, 1899]
For eleven books, that is eleven Integer objects. For a stream with millions of values, I recommend the primitive form: It saves one allocation per element and keeps the stream an IntStream, whose sum(), average(), and summaryStatistics() you can still call afterwards.
Overloaded Methods with Interfaces of the Same Shape
Two overloads of a method that differ only in their functional interface – one with Consumer<String>, one with Function<String, String> – cannot be told apart by the compiler for an implicitly typed lambda. The article about lambda expressions shows the error and the three notations that resolve it in the section Overloaded Methods. For your own APIs, I recommend avoiding such overloads and giving the methods different names instead.
Summary
A functional interface is an interface with exactly one abstract method; default methods, static methods, and the methods of Object do not count. That one method is what a lambda or a method reference implements – and whether an interface is functional is decided solely by that count, not by the @FunctionalInterface annotation, which merely guards it.
The package java.util.function contains 43 such interfaces following one scheme: the four base forms Function, Predicate, Consumer, and Supplier, plus the operators with the same input and output type, the Bi forms for two parameters, and the specializations for int, long, and double, whose names state input and output.
Three recommendations for everyday work:
- Use the interfaces from
java.util.functionwherever one fits – one of your own only for a shape that does not exist there, for a checked exception, or for a name with a contract of its own. - On primitive streams, take the primitive form (
IntPredicateinstead ofPredicate<Integer>); otherwise, you pay one allocation per element. - Declare the parameters of your own methods with wildcards, as the JDK does:
Predicate<? super T>,Function<? super T, ? extends R>.
How the compiler matches a lambda against the one method, and everything else there is to know about lambdas, is in the article about Java Lambda Expressions. Where the interfaces are found most often in everyday work – in filter(), map(), collect(), and the other operations – is shown in the article about Java Streams.
Did this article save you time? Then I’d be happy if you invested a minute of it in a review on my ProvenExpert profile. Your feedback shows me that the work on these articles pays off.
Would you like to be notified when I take a close look at the next Java release? Then click here to sign up for the HappyCoders newsletter.




