Skip Top Navigation Bar
Previous in the Series
Current Tutorial
Using Lambdas Expressions in Your Application

Using Lambdas Expressions in Your Application

The introduction of lambda expressions in Java SE 8 came with a major rewrite of the JDK API. More classes have been updated in the JDK 8 following the introduction of lamdbas than in the JDK 5 following the introduction of generics.

Thanks to the very simple definition of functional interfaces, many existing interfaces became functional without having to modify them. The same goes for your existing code: if you have interfaces in your application written prior to Java SE 8, they may become functional without having to touch them, making it possible to implement them with lambdas.

 

Discovering the java.util.function package

The JDK 8 also introduces a new package: java.util.function with functional interfaces for you to use in your application. These functional interfaces are also heavily used in the JDK API, especially in the Collections Frameworks and the Stream API. This package is in the java.base module.

With a little more than 40 interfaces, this package may look a little scary at first. It turns out that it is organized around four main interfaces. Understanding them gives you the key to understand all the others.

 

Creating or Providing Objects with Supplier<T>

Implementing the Supplier<T> Interface

The first interface is the Supplier<T> interface. In a nutshell, a supplier does not take any arguments and returns an object.

We should really say: a lambda that implements the supplier interface does not take any argument and returns an object. Making shortcuts makes things easier to remember, as long as they are not confusing.

This interface is really simple: it has no default or static method, just a plain get() method. Here is this interface:

@FunctionalInterface
public interface Supplier<T> {

    T get();
}

The following lambda is an implementation of this interface:

Supplier<String> supplier = () -> "Hello Duke!";`

This lambda expression simply returns the Hello Duke! string of characters. You can also write a supplier that returns a new object every time it is invoked:

Random random = new Random(314L);
Supplier<Integer> newRandom = () -> random.nextInt(10);

for (int index = 0; index < 5; index++) {
    System.out.println(newRandom.get() + " ");
}

Calling the get() method of this supplier will invoke random.nextInt(), and will produce random integers. Since the seed of this random generator is fixed to the value 314L, you should see the following random integers generated:

1
5
3
0
2

Note that this lambda is capturing a variable from the enclosing scope: random, making this variable effectively final.

Using a Supplier<T>

Note how you generated random numbers using the newRandom supplier in the previous example:

for (int index = 0; index < 5; index++) {
    System.out.println(newRandom.get() + " ");
}

Calling the get() method of the Supplier interface invokes your lambda.

Using Specialized Suppliers

Lambda expressions are used to process data in applications. How fast a lambda expression can be executed is thus critical in the JDK. Any CPU cycle that can be saved has to be saved, since it may represent a significant optimization in a real application.

Following this principle, the JDK API also offers specialized, optimized versions of the Supplier<T> interface.

You may have noticed that our second example supplies the Integer type, where the Random.nextInt() method returns an int. So in the code you wrote, there are two things that are happening under the hood:

  • the int returned by the Random.nextInt() is first boxed into an Integer, by the auto-boxing mechanism;
  • this Integer is then unboxed when assigned to the nextRandom variable, by the auto-unboxing mechanism.

The auto-boxing is the mechanism by which an int value can be directly assigned to an Integer object:

int i = 12;
Integer integer = i;

Under the hood, an object is created for you, wrapping that value.

The auto-unboxing does the opposite. You may assign an Integer to an int value, by unwrapping the value within the Integer:

Integer integer = Integer.valueOf(12);
int i = integer;

This boxing / unboxing does not come for free. Most of the time, this cost will be small compared to other things your application is doing, like getting data from a database or from a remote service. But in some cases, this cost may be not acceptable, and you need to avoid paying it.

The good news is: the JDK gives you a solution with the IntSupplier interface. Here is this interface:

@FunctionalInterface
public interface IntSupplier {

    int getAsInt();
}

Notice that you can use the exact same code to implement this interface:

Random random = new Random(314L);
IntSupplier newRandom = () -> random.nextInt();

The only modification to your application code is that you need to call getAsInt() instead of get():

for (int i = 0; i < 5; i++) {
    int nextRandom = newRandom.getAsInt();
    System.out.println("next random = " + nextRandom);
}

The result of running this code is the same, but this time no boxing / unboxing occurred: this code is more performant than the previous one.

The JDK gives you four of these specialized suppliers, to avoid unnecessary boxing / unboxing in your application: IntSupplier, BooleanSupplier, LongSupplier and DoubleSupplier.

You will see more of these specialized version of functional interfaces to handle primitive types. There is a simple naming convention for their abstract method: take the name of the main abstract method (get() in the case of the supplier), and add the returned type to it. So for the supplier interfaces we have: getAsBoolean(), getAsInt(), getAsLong(), and getAsDouble().

 

Consuming Objects with Consumer<T>

Implementing and Using Consumers

The second interface is the Consumer<T> interface. A consumer does the opposite of the supplier: it takes an argument and does not return anything.

The interface is a little more complex: there are default methods in it, which will be covered later in this tutorial. Let us concentrate on its abstract method:

@FunctionalInterface
public interface Consumer<T> {

    void accept(T t);

    // default methods removed
}

You already implemented consumers:

Consumer<String> printer = s -> System.out.println(s);

You can update the previous example with this consumer:

for (int i = 0; i < 5; i++) {
    int nextRandom = newRandom.getAsInt();
    printer.accept("next random = " + nextRandom);
}

Using Specialized Consumers

Suppose you need to print integers. Then you can write the following consumer:

Consumer<Integer> printer = i -> System.out.println(i);`

Then you may face the same auto-boxing issue as with the supplier example. Is this boxing / unboxing acceptable in your application, performance-wise?

Do not worry if this is not the case, the JDK has you covered with the three specialized consumers available: IntConsumer, LongConsumer, and DoubleConsumer. The abstract methods of these three consumers follow the same convention as for the supplier, since the returned type is always void, they are all named accept.

Consuming Two Elements with a BiConsumer

Then the JDK adds another variant of the Consumer<T> interface, which takes two arguments instead of one, called quite naturally the BiConsumer<T, U> interface. Here is this interface:

@FunctionalInterface
public interface BiConsumer<T, U> {

    void accept(T t, U u);

    // default methods removed
}

Here is an example of a biconsumer:

BiConsumer<Random, Integer> randomNumberPrinter =
        (random, number) -> {
            for (int i = 0; i < number; i++) {
                System.out.println("next random = " + random.nextInt());
            }
        };

You can use this biconsumer to write the previous example differently:

randomNumberPrinter.accept(new Random(314L), 5));

There are three specialized versions of the BiConsumer<T, U> interface to handle primitive types: ObjIntConsumer<T>, ObjLongConsumer<T> and ObjDoubleConsumer<T>.

Passing a Consumer to an Iterable

Several important methods have been added to the interfaces of the Collections Framework, that are covered in another part of this tutorial. One of them takes a Consumer<T> as an argument and is extremely useful: the Iterable.forEach() method. Here is a simple example, that you will see everywhere:

List<String> strings = ...; // really any list of any kind of objects
Consumer<String> printer = s -> System.out.println(s);
strings.forEach(printer);

This last line of code will just apply the consumer to all the objects of the list. Here it will simply print them one by one on the console. You will see another way to write this consumer in a later part.

This forEach() method exposes a way to access an internal iteration over all the elements of any Iterable, passing the action you need to take on each of these elements. It is a very powerful way of doing so, and it also makes your code more readable.

 

Testing Objects with Predicate<T>

Implementing and Using Predicates

The third interface is the Predicate<T> interface. A predicate is used to test an object. It is used for filtering streams in the Stream API, a topic that you will see later on.

Its abstract method takes an object and returns a boolean value. This interface is again a little more complex than Consumer<T>: there are default methods and static methods defined on it, which you will see later on. Let us concentrate on its abstract method:

@FunctionalInterface
public interface Predicate<T> {

    boolean test(T t);

    // default and static methods removed
}

You already saw an example of a Predicate<String> in a previous part:

Predicate<String> length3 = s -> s.length() == 3;

To test a given string, all you need to do is call the test() method of the Predicate interface:

String word = ...; // any word
boolean isOfLength3 = length3.test(word);
System.out.prinln("Is of length 3? " + isOfLength3);

Using Specialized Predicates

Suppose you need to test integer values. You can write the following predicate:

Predicate<Integer> isGreaterThan10 = i -> i > 10;

The same goes for the consumers, the supplier, and this predicate. What this predicate takes as an argument is a reference to an instance of the Integer class, so before comparing this value to 10, this object is auto-unboxed. It is very convenient but comes with an overhead.

The solution provided by the JDK is the same as for suppliers and consumers: specialized predicates. Along with Predicate<String> are three specialized interfaces: IntPredicate, LongPredicate, and DoublePredicate. Their abstract methods all follow the naming convention. Since they all return a boolean, they are just named test() and take an argument corresponding to the interface.

So you can write the previous example as follow:

IntPredicate isGreaterThan10 = i -> i > 10;

You can see that the syntax of the lambda itself is the same, the only difference is that i is now an int type instead of Integer.

Testing Two Elements with a BiPredicate

Following the convention you saw with the Consumer<T>, the JDK also adds a BiPredicate<T, U> interface, which tests two elements instead of one. Here is the interface:

@FunctionalInterface
public interface BiPredicate<T, U> {

    boolean test(T t, U u);

    // default methods removed
}

Here is an example of such a bipredicate:

Predicate<String, Integer> isOfLength = (word, length) -> word.length() == length;

You can use this bipredicate with the following pattern:

String word = ...; // really any word will do!
int length = 3;
boolean isWordOfLength3 = isOfLength.test(word, length);

There is no specialized version of BiPredicate<T, U> to handle primitive types.

Passing a Predicate to a Collection

One of the methods added to the Collections Framework takes a predicate: the removeIf() method. This method uses this predicate to test each element of the collection. If the result of the test is true, then this element is removed from the collection.

You can see this pattern in action in the following example:

List<String> immutableStrings =
        List.of("one", "two", "three", "four", "five");
List<String> strings = new ArrayList<>(immutableStrings);
Predicate<String> isEvenLength = s -> s.length() % 2 == 0;
strings.removeIf(isEvenLength);
System.out.println("strings = " + strings);

Running this code will produce the following result:

strings = [one, two, three]

There are several things worth pointing out on this example:

  • As you can see, calling removeIf() mutates this collection.
  • So you should not call removeIf() on an immutable collection, like the ones produced by the List.of() factory methods. You will get an exception if you do that because you cannot remove elements from an immutable collection.
  • Arrays.asList() produces a collection that behaves like an array. You can mutate its existing elements, but you are not allowed to add or remove elements from the list returned by this factory method. So calling removeIf() on this list will not work either.

 

Mapping Objects to Other Objects with Function<T, R>

Implementing and Using Functions

The fourth interface is the Function<T, R> interface. The abstract method of a function takes an object of type T and returns a transformation of that object to any other type U. This interface also has default and static methods.

@FunctionalInterface
public interface Function<T, R> {

    R apply(T t);

    // default and static methods removed
}

Functions are used in the Stream API to map objects to other objects, a topic that will be covered later on. A predicate can be seen as a specialized type of function, that returns a boolean.

Using Specialized Functions

This is an example of a function that takes a string and returns the length of that string.

Function<String, Integer> toLength = s -> s.length();
String word = ...; // any kind of word will do
int length = toLength.apply(word);

Here again, you can spot the boxing and unboxing operations in action. First, the length() method returns an int. Since the function returns an Integer, this int is boxed. But then the result is assigned to a variable length of type int, so the Integer is then unboxed to be stored in this variable.

If performance is not an issue in your application, then this boxing and unboxing is really not a big deal. If it is, you will probably want to avoid it.

The JDK has solutions for you, with specialized versions of the Function<T, R> interface. This set of interfaces is more complex than the one we saw for the Supplier, the Consumer<T>, or the Predicate categories because specialized functions are defined both for the type of the input argument, and the returned type.

Both the input argument and the output can have four different types:

  • a parameterized type T;
  • an int;
  • a long;
  • a double.

Things do not stop here, because there is a subtlety in the design of the API. There is a special interface: UnaryOperator<T> which extends Function<T, T>. This unary operator concept is used to name the functions that take an argument of a given type, and return a result of the same type. A unary operator is just what you would expect. All the classical math operators can be modeled by a UnaryOperator<T>: the square root, all the trigonometric operators, the logarithm, and the exponential.

Here are the 16 specialized types of functions you can find in the java.util.function package.

Parameter types T int long double
T UnaryOperator<T> IntFunction<T> LongFunction<T> DoubleFunction<T>
int ToIntFunction<T> IntUnaryOperator LongToIntFunction DoubleToIntFunction
long ToLongFunction<T> IntToLongFunction LongUnaryOperator DoubleToLongFunction
double ToDoubleFunction<T> IntToDoubleFunction LongToDoubleFunction DoubleUnaryOperator

All the abstract methods of these interfaces follow the same convention: they are named after the returned type of that function. Here are their names:

Passing a Unary Operator to a List

You can transform the elements of a list with a UnaryOperator<T>. One could wonder why a UnaryOperator<T> and not a basic Function. The answer is in fact quite simple: once declared, you cannot change the type of a list. So the function you apply can change the elements of the list, but not their type.

The method that takes this unary operator passes it to the replaceAll() method. Here is an example:

List<String> strings = Arrays.asList("one", "two", "three");
UnaryOperator<String> toUpperCase = word -> word.toUpperCase();
strings.replaceAll(toUpperCase);
System.out.println(strings);

Running this code displays the following:

[ONE, TWO, THREE]

Note that this time we used a list created with the Arrays.asList() pattern. Indeed you do not need to add or remove any element to that list: this code just modifies each element one by one, which is possible with this particular list.

Mapping Two Elements with a BiFunction

As for the consumer and predicate, functions have also a version that takes two arguments: the bifunction. The interface is BiFunction<T, U, R>, where T and U are the arguments and R the returned type. Here is the interface:

@FunctionalInterface
public interface BiFunction<T, U, R> {

    R apply(T t, U u);

    // default methods removed
}

You can create a bifunction with a lambda expression:

BiFunction<String, String, Integer> findWordInSentence =
    (word, sentence) -> sentence.indexOf(word);

The UnaryOperator<T> interface has also a sibling interface with two arguments: the BinaryOperator<T>, that extends BiFunction<T, U, R>. As you would expect, the four basic arithmetic operations can be modeled with a BinaryOperator.

A subset of all the possible specialized versions of bifunction has been added to the JDK:

 

Wrapping up the Four Categories of Functional Interfaces

The java.util.function package is now central in Java, because all the lambda expressions you are going to use in the Collections Framework or the Stream API implement one of the interfaces from that package.

As you saw, this package contains many interfaces and finding your way there may be tricky.

Firstly, what you need to remember is that there are 4 categories of interfaces:

  • the suppliers: do not take any argument, return something
  • the consumers: take an argument, do not return anything
  • the predicates: take an argument, return a boolean
  • the functions: take an argument, return something

Secondly: some interfaces have versions that take two arguments instead of one:

  • the biconsumers
  • the bipredicates
  • the bifunctions

Thirdly: some interfaces have specialized versions, added to avoid boxing and unboxing. There are too many to list them all. They are named after the type they take. For example: IntPredicate, or the type they return, as in ToLongFunction<T>. They may be named after both: IntToDoubleFunction.

Lastly: there are extensions of Function<T, R> and BiFunction<T, U, R> for the case where all the types are the same: UnaryOperator<T> and BinaryOperator<T>, with specialized versions for the primitive types.

More Learning


Last update: October 26, 2021


Previous in the Series
Current Tutorial
Using Lambdas Expressions in Your Application