Java 8 added a handful of key features to the Java language, accompanied by additions to the standard library. Among the language features added were lambdas, often called anonymous functions or closures. In order to implement this feature in a manner consistent with the historical aims and use of the language, it was decided that lambdas would be restricted to implementation of functional interfaces—interfaces with exactly one unimplemented method. Such interfaces had already existed in the standard library, but the distinction was formalized in Java 8, and several new functional interfaces were added to the standard library. This document is an overview of some of the functional interfaces, with lambda-based implementation examples.
Note: Due to the lambda-centric focus of this document, Comparable<T>
, Iterable<T>
, Closeable
, and AutoCloseable
are not addressed, even though they are formally functional interfaces (since each has exactly one unimplemented method). The purposes of those interfaces essentially dictate that they perform computations based on the states of instances of the implementing classes; thus, they are not well-suited to implementation via lambdas, which don’t define accessible state.
Runnable
(java.lang
)Predicate<T>
(java.util.function
)Consumer<T>
(java.util.function
)Supplier<T>
(java.util.function
)Function<T,R>
(java.util.function
)BinaryOperator<T>
(java.util.function
)Comparator<T>
(java.util
)
Runnable
(java.lang
)
-
Primarily intended for use as a unit of processing operations that can be executed in a separate thread from the originator. For example, an instance of a
Runnable
implementation may be passed to a newThread
in theThread(Runnable)
constructor; when theThread.start()
method is invoked, the new execution thread is started, and theRunnable.run()
method is invoked in that thread. -
The method to be implemented is
void run()
. Thus, an implementing lambda must take no parameters, return no value, and throw no checked exceptions. -
While
Runnable
is intended for use as a basic “atom” of concurrent processing, it is not required to be used in a concurrent mode. In particular, unlikeThread.start()
,Runnable.run()
may be invoked multiple times, from any thread.
Example: Create Runnable
and invoke run
method on threads.
Runnable identify = () ->
System.out.printf("%d: %s%n", System.currentTimeMillis(),
Thread.currentThread().getName());
Thread t = new Thread(identify);
identify.run();
t.start();
In this example, a reference to the lambda implementation of Runnable
is declared and initialized as the local variable identify
. A Thread
is created, passing this lambda as a constructor parameter. Then identify.run()
is invoked on the main thread, after which the new thread is started, causing identify.run()
to be invoked on that thread.
The output from the above will be slightly different every time, but it will generally be something like
1587678772815: main
1587678772840: Thread-6
Since both threads (main
and—in the example output above—Thread-6
) are writing to a shared resource (the standard output device accessed via System.out
), it’s actually possible to get interleaved output between the 2 threads.
Predicate<T>
(java.util.function
)
-
Typically used for filter operations on a
Collection
orStream
. -
Parameterized by the type contained in the
Collection
orStream
. -
Method to be implemented is
boolean test(T t)
. Thus, a lambda must take a single parameter and return/evaluate to aboolean
result.
Examples: Filter Stream
or Collection
…
… using Collection
.removeIf(
Predicate
)
.
Collection<Integer> data =
new LinkedList<>(List.of(1, 1, 2, 3, 5, 8, 13));
data.removeIf((v) -> v % 2 == 0); // Remove even values.
// data contains [1, 1, 3, 5, 13]
… using Stream
.filter(
Predicate
)
.
List<Integer> data = Stream.of(1, 1, 2, 3, 5, 8, 13)
.filter((v) -> v % 2 == 1) // Preserve odd values.
.collect(Collectors.toList());
// data contains [1, 1, 3, 5, 13]
Note that the sense of the predicate is reversed in the two uses shown above: Collection.removeIf(Predicate)
removes an item from the collection if (and only if) the predicate evaluates to true
for that item; Stream.filter(Predicate)
preserves an item in the stream if (and only if) the predicate evaluates to true
for that item.
Consumer<T>
(java.util.function
)
-
Typically used to perform a common operation on all items in a
Collection
orStream
, or on the value which may be contained in anOptional
. (It can be useful to viewCollection.forEach(Consumer)
as an “inside-out” variation on the enhanced-for.) -
Parameterized by the type contained in the
Collection
,Stream
,Optional
, etc. -
Method to be implemented is
void accept(T t)
. Thus, a lambda must take a single parameter and return no result. (If an expression lambda is used, the evaluated result will be ignored.)
Examples: Print all items in Collection
or Stream
…
… using Collection
.forEach(
Consumer
)
.
List<Integer> data =
new LinkedList<>(List.of(1, 1, 2, 3, 5, 8, 13));
data.forEach(System.out::println); // Print each value.
… using Stream
.forEach(
Consumer
)
.
Stream.of(1, 1, 2, 3, 5, 8, 13)
.forEach(System.out::println); // Print each value.
Both of these examples produce this output:
1
1
2
3
5
8
13
Supplier<T>
(java.util.function
)
-
Used to provide a source of values for a
Stream
, alternative values for anOptional
, etc. -
Method to be implemented is
T get()
. Thus, a lambda must take no parameters and return/evaluate to the parameterized typeT
. -
Implemented by a lambda in some cases; however, since state may often need to be maintained across invocations of
get()
,Supplier
is sometimes implemented via a class with the relevant state fields. -
When using
Stream.generate(Supplier)
, the size of the resulting stream must generally be controlled throughStream.limit(long)
or another operation that truncates the stream in some fashion.
Example: Generate Stream
of random numbers using Stream
.generate(
Supplier
)
.
Stream.generate(Math::random) // Get each value in turn.
.limit(100) // Take only the first 100 values.
.forEach(System.out::println); // Print each value.
This example uses a Supplier<Double>
, implemented as a lambda (expressed in method reference syntax), to generate a Stream<Double>
of random values. The stream is truncated after 100 items, and then each item is printed via a Consumer<Double>
lambda, expressed with method reference syntax.
Function<T,R>
(java.util.function
)
-
Used to map a value of one type to a value of another, or to mutate an object in-place (returning the mutated object rather than a new object).
-
Often employed to transform all of the items in a
Stream
, usingStream.map(Function)
. -
Parameterized by the type of the original value (
T
) and the type of the mapped value (R
); these may, in fact, be the same type. (UnaryOperator<T>
extendsFunction<T,R>
explicitly for this purpose.) If used inStream.map(Function)
, then this also has the effect of changing aStream<T>
to aStream<R>
. -
Method to be implemented is
R apply(T t)
. Thus, a lambda implementation must take a single parameter of typeT
, and return a value of typeR
. …
Example: Map Stream
of random values to dice rolls using Stream
.map(
Function
)
.
Stream.generate(() -> Math.random())
.limit(100) // Take only the first 100 values.
.map((v) -> 1 + (int) (6 * v)) // Map from [0, 1) to {1 … 6}.
.forEach(System.out::println); // Print each value.
The example uses a lambda implementing Supplier<Double>
to generate a Stream<Double>
of pseudo-random numbers. Then, a lambda implementation of Stream.map(Function)
is used to map the Double
values to Integer
values in the range from 1 to 6 (inclusive)—i.e. random dice roll values. (Note that this example includes 2 auto-boxing operations, and 1 auto-unboxing operation. Can you spot them?)
BinaryOperator<T>
(java.util.function
)
-
Among other uses, employed in multiple overloads of
Stream.reduce
to combine pairs of values in the stream, ultimately resulting in producing a single value. -
The method to be implemented is
T apply(T t1, T t2)
; a lambda implementation must therefore take 2 parameters of typeT
and return a value of typeT
. (This is a subinterface ofBiFunction<T,U,R>
; theapply
method of that interface takes 2 parameters, of typesT
andU
, respectively, and returns a result of typeR
.)
Example: Sluggify String
using Stream
.reduce(
BinaryOperator
)
.
Pattern delimiter = Pattern.compile("\\W+");
String source = "The quick brown fox jumps over the lazy dog.";
delimiter.splitAsStream(source) // Split on non-word characters.
.filter((s) -> !s.isEmpty()) // Filter out empty strings.
.map(String::toLowerCase) // Map each string to lower-case.
.reduce((s1, s2) -> s1 + "-" + s2) // Concatenate into "slug".
.ifPresent(System.out::println); // Print the value.
This example splits a String
into a Stream<String>
using a regular expression Pattern
. Then, empty items are filtered out (using a Predicate<String>
lambda) and the remaining values are converted to lower-case (using a Function<String, String>
lambda). Finally, the Stream<String>
contents are reduced to an Optional<String>
using a BinaryOperator<String>
lambda, and the contents of the Optional<String>
are printed using a Consumer<String>
lambda (expressed with method reference syntax). This produces the output
the-quick-brown-fox-jumps-over-the-lazy-dog
(Note: A joining collector could have produced the same result as the reduce operation in the above example; see the Stream.sorted(Comparator)
example, below, for an illustration.)
Comparator<T>
(java.util
)
-
Used for ordering (sorting or maintaining the sorted order of) items in an array,
Collection
, orStream
. -
A
Comparator<T>
is typically used when a natural ordering is not defined (i.e. whenT
does not implementComparable<T>
), or as an alternative to natural order. -
An implementation can (and generally should) take advantage of comparison capabilities defined for the types that make up the state of
T
, as well as the static factory methods provided byComparator
. -
The method to be implemented is
int compare(T t1, T t2)
. A lambda implementation must therefore take 2 parameters of typeT
, and return anint
, where a negative value implies thatt1 < t2
(for the purpose of ordering), a positive value impliest1 > t2
, and zero means thatt1
andt2
are equivalent for ordering purposes.
Examples: Sort array, Collection
, or Stream
by reciprocals …
… using Arrays.sort(T[],
Comparator
)
.
Integer[] data = {-1, 3, 2, 10, -5, 6};
Arrays.sort(data, (d1, d2) -> Double.compare(1.0 / d1, 1.0 / d2));
System.out.println(Arrays.toString(data));
… using List
.sort(
Comparator
)
.
List<Integer> data =
new LinkedList<>(List.of(-1, 3, 2, 10, -5, 6));
data.sort((d1, d2) -> Double.compare(1.0 / d1, 1.0 / d2));
System.out.println(data);
… using Stream
.sorted(
Comparator
)
.
System.out.println(Stream.of(-1, 3, 2, 10, -5, 6)
.sorted((d1, d2) -> Double.compare(1.0 / d1, 1.0 / d2))
.map(String::valueOf)
.collect(Collectors.joining(", ", "[", "]")));
All of the above produce the same output:
[-1, -5, 10, 6, 3, 2]