Chapter EIGHT
Functional Interfaces and Lambda Expressions


Exam Objectives

Use Java object and primitive Streams, including lambda expressions implementing functional interfaces, to create, filter, transform, process, and sort data.

Chapter Content


Functional Interfaces

Java 8 brought us lambda expressions, a new feature that aims to simplify development by taking a more functional programming approach. But for this to work, Java also introduced the concept of functional interfaces.

A functional interface is an interface that contains only one abstract method. They may contain one or more default methods or static methods, but there can be only one abstract method.

At first glance, you might think that using functional interfaces is not much different than using regular classes and objects. After all, we’ve been able to define interfaces with a single method for a long time. But the key difference is how they enable the use of lambda expressions.

Lambda expressions let you treat functionality as method arguments, or code as data. Instead of defining a class that implements a single-method interface, you can directly pass a lambda expression as an instance of a functional interface, allowing for cleaner and more concise code.

public interface MyInterface {
    public void myMethod();
}

MyInterface ref = () -> System.out.println("Hello World!"); 

In this example, the lambda expression () -> System.out.println("Hello World!") is treated as an instance of the MyInterface functional interface. We’re assigning a block of code to the variable ref.

The @FunctionalInterface Annotation

Java 8 also introduced the @FunctionalInterface annotation, which is used to indicate that an interface is intended to be a functional interface. It’s a kind of hint to the compiler that you intend for this interface to adhere to the rules of a functional interface:

@FunctionalInterface
public interface MyInterface {
    void myMethod();
}

However, the @FunctionalInterface annotation is not required. If an interface meets the criteria of a functional interface (it has only one abstract method), it’s a functional interface whether or not it has the @FunctionalInterface annotation.

So why use it?

There are a couple of reasons:

  1. It makes your intent clear. By using @FunctionalInterface, you’re signaling to other developers (and to your future self) that this interface is meant to be used with lambda expressions.

  2. It enables compiler checks. If you annotate an interface with @FunctionalInterface and then try to add a second abstract method to it, the compiler will throw an error. This can help prevent accidental violations of the functional interface contract.

@FunctionalInterface
public interface MyInterface {
    void myMethod();
    void myOtherMethod();  // This will cause a compiler error
}

However, the annotation does not become part of the generated bytecode. It’s purely for compile-time checks and for developer clarity.

Also, note that if an interface is annotated with @FunctionalInterface, but does not actually meet the criteria (for example, if it has no abstract methods at all), the compiler will raise an error:

@FunctionalInterface
public interface NonFunctionalInterface {
    // No abstract methods
}  // This will cause a compiler error

Rules for Defining a Functional Interface

Functional interfaces do not limit what you can do. You can still define as many default and static methods on the interface as you’d like.

Default methods enable you to add new functionality to the interfaces of your libraries and ensure binary compatibility with code written for older versions of those interfaces. Static methods in interfaces are used for providing utility methods, like null-checking for example.

interface MyInterface {
    void abstractMethod(int x);  
    default void defaultMethod() { }        
    static void staticMethod() { }  
}

Only the abstractMethod counts toward the single abstract method test for a functional interface.

It’s also important to note that if an interface declares an abstract method overriding one of the public methods of java.lang.Object, that also does not count toward the interface’s abstract method count since any implementation of the interface will have an implementation from java.lang.Object or elsewhere. For example:

interface MyInterface {
    boolean equals(Object obj); 
    // Other methods
}

In this case, MyInterface is still a functional interface since equals is a public method in Object.

Using lambda expressions with functional interfaces is simply a new option in our coding toolbox. You can still use anonymous inner classes or implement the interface the old-fashioned way:

MyInterface ref = new MyInterface() {
    @Override
    public void myMethod() {
        System.out.println("Hello World!");
    }
};

// Implementing the interface in a separate class
class MyClass implements MyInterface {
    @Override
    public void myMethod() {
        System.out.println("Hello World!");
    }
}
MyInterface ref = new MyClass();

Also, a class or lambda expression can implement multiple functional interfaces if they’re compatible. For example, if two interfaces have identical abstract methods, they’re effectively the same functional interface:

@FunctionalInterface
interface Interface1 {
    void method();
}

@FunctionalInterface
interface Interface2 {
    void method();
}

// Implementing multiple compatible interfaces in a class
class MyClass implements Interface1, Interface2 {
    @Override
    public void method() {
        System.out.println("Hello World!");
    }
}

// Using a lambda expression 
Interface1 ref1 = () -> System.out.println("Hello World!");
Interface2 ref2 = () -> System.out.println("Hello World!");

And if the built-in functional interfaces like Runnable or Comparator don’t meet your needs, you can easily define your own. Just remember the single abstract method rule.

Lambda Expressions

Lambda expressions allow you to treat functionality as a method argument or code as data, enabling a more functional programming style. For example, they enable you to write code like this:

List<Car> compactCars = findCars(cars,
     (Car c) ->
        c.getType().equals(CarTypes.COMPACT)
);

Instead of:

List<Car> compactCars = findCars(cars,
     new Searchable() {
        public boolean test(Car car) {
           return car.getType().equals(
                     CarTypes.COMPACT);
        }
});

In essence, a lambda expression is a concise way to represent a function. The term lambda expression comes from lambda calculus, written as λ-calculus, where λ is the Greek letter lambda. This form of calculus deals with defining and applying functions.

Functional interfaces are the foundation upon which lambda expressions are built. For example, consider the following functional interface:

@FunctionalInterface
interface MyFunction {
    int apply(int a);
}

You can use a lambda expression wherever an instance of this interface is expected:

MyFunction doubler = (int a) -> a * 2;

The lambda expression a -> a * 2 conforms to the signature of the apply method in MyFunction.

Syntax of a Lambda Expression

A lambda expression has three parts: a list of parameters, an arrow token (->), and a function body.

Here’s the basic syntax:

(parameters) -> expression
// or 
(parameters) -> { statements; }

For example, consider this functional interface:

@FunctionalInterface
interface MyFunction {
    int apply(int a, int b);
}

And this lambda expression that takes two integers and returns their sum:

MyFunction f = (int a, int b) -> a + b

You can use the var keyword in the parameter list of a lambda expression. This allows the type of the parameter to be inferred by the compiler:

MyFunction f = (var a, var b) -> a + b

You can omit the parameter types, the compiler can also infer them from the context:

MyFunction f = (a, b) -> a + b

If the lambda expression only takes one parameter, you can even omit the parentheses:

@FunctionalInterface
interface MyFunction {
    int apply(int a);
}

//...

MyFunction f = a -> a * 2

You can also use the var keyword to declare a variable without specifying its type only when the compiler can infer the type from the context.

For example, taking into account the MyFunction interface of the previous example and just the following expression:

var f = a -> a * 2;

You’d get a compile-time error with the following message: “Cannot infer type: lambda expression requires an explicit target type.”

You cannot use var directly with a lambda expression like var f = (var a) -> a * 2; because the lambda needs a target type that var cannot provide.

However, in this case:

MyInterface f = (a) -> a * 2; // Lambda assigned to functional interface
var fVar = f; // `var` infers type MyInterface
System.out.println(fVar.apply(5)); // Outputs 10

You can use var because you are assigning a lambda to a previously defined functional interface, where the type can be inferred from the context.

The contexts where the target type (the functional interface) of a lambda expression can be inferred include:

If you understand the concept, you don’t need to memorize this list.

Lambda Expressions and Anonymous Classes

Prior to Java 8, anonymous classes were the primary way to represent a one-off piece of functionality. With the introduction of lambda expressions in Java 8, we now have a more concise way to write certain types of anonymous classes.

Consider this anonymous class:

Runnable r1 = new Runnable() {
    public void run() {
        System.out.println("Hello!");
    }
};

This can be replaced with a lambda expression:

Runnable r2 = () -> System.out.println("Hello!");

However, while lambda expressions and anonymous classes share some similarities, they also have significant differences:

Similarities:

Differences:

Here’s an example about using local variables inside the body of a lambda:

public class LambdaExample {
    public void testLambda() {
        int localVariable = 10;
        Runnable r = () -> {
            System.out.println("Lambda: " + localVariable);
        };
        r.run();
    }

    public void testAnonymous() {
        int localVariable = 10;
        Runnable r = new Runnable() {
            public void run() {
                System.out.println("Anonymous: " + localVariable);
            }
        };
        r.run();
    }

    public static void main(String[] args) {
        LambdaExample example = new LambdaExample();
        example.testLambda();
        example.testAnonymous();
    }
}

This is the output:

Lambda: 10
Anonymous: 10

In this example, both the lambda expression and the anonymous class are able to access the localVariable defined in their respective methods. However, if we try to modify the localVariable after it has been used in the lambda expression or anonymous class, we will get a compilation error:

public void testLambda() {
    int localVariable = 10;
    Runnable r = () -> {
        System.out.println("Lambda: " + localVariable); // Compilation error
    };
    localVariable = 20;  // Because of this
    r.run();
}

This is because the localVariable must be effectively final (its value doesn’t change after initialization) in order to be used inside the lambda expression or anonymous class.

Local variables have to be final because of the way they are implemented in Java. Instance variables are stored on the heap, while local variables live on the stack. Variables on the heap are shared across threads, but variables on the stack are confined to the thread they’re in.

When you create an instance of an anonymous inner class or a lambda expression, the values of local variables are copied. This prevents thread-related problems and ensures that you are working with a consistent value, as the variable cannot be modified after initialization.

By requiring final (or effectively final) variables, Java ensures thread safety and consistency, as the value cannot be changed, eliminating visibility issues and potential thread problems.

Java Built-In Lambda Interfaces

In the previous section, we used functional interfaces like the following:

@FunctionalInterface
interface MyFunction {
    int apply(int a, int b);
}

However, you don’t have to write an interface like that in each program that uses it (or link a library that contains it). An interface that does the same but accepts any object type already exists in the language.

Java provides functional interfaces for common use cases in the java.util.function package.

These are the main five:

Where T and R represent generic types (T represents a parameter type and R the return type).

They also have specializations for cases where the input parameter is a primitive type (specifically for int, long, double, and boolean for Supplier), for example:

Where the name is preceded by the appropriate primitive type.

Additionally, four of them have binary versions, which means they take two parameters instead of one:

Where T, U, and R represent generic types (T and U represent parameter types and R the return type).

The following tables show the complete list of interfaces. You don’t have to memorize them, just try to understand them.

Functional Interface Primitive Versions
Predicate<T> IntPredicate
LongPredicate
DoublePredicate
Consumer<T> IntConsumer
LongConsumer
DoubleConsumer
Function<T, R> IntFunction<R>
IntToDoubleFunction
IntToLongFunction
LongFunction<R>
LongToDoubleFunction
LongToIntFunction
DoubleFunction<R>
DoubleToIntFunction
DoubleToLongFunction
ToIntFunction<T>
ToDoubleFunction<T>
ToLongFunction<T>
Supplier<T> BooleanSupplier
IntSupplier
LongSupplier
DoubleSupplier
UnaryOperator<T> IntUnaryOperator
LongUnaryOperator
DoubleUnaryOperator
Functional Interface Primitive Versions
BiPredicate<L, R>  
BiConsumer<T, U> ObjIntConsumer<T>
ObjLongConsumer<T>
ObjDoubleConsumer<T>
BiFunction<T, U, R> ToIntBiFunction<T, U>
ToLongBiFunction<T, U>
ToDoubleBiFunction<T, U>
BinaryOperator<T> IntBinaryOperator
LongBinaryOperator
DoubleBinaryOperator

Predicate

A predicate is a statement that may be true or false depending on the values of its variables.

This functional interface can be used anywhere you need to evaluate a boolean condition.

This is how the interface is defined:

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
    // Other default and static methods
    // ...
}

The functional descriptor (method signature) is:

Predicate<T>

Here’s an example using an anonymous class:

Predicate<String> startsWithA = new Predicate<String>() {
    @Override
    public boolean test(String t) {
        return t.startsWith("A");
    }
};
boolean result = startsWithA.test("Arthur");

And with a lambda expression:

Predicate<String> startsWithA = t -> t.startsWith("A");
boolean result = startsWithA.test("Arthur");

This interface also has the following default methods:

default Predicate<T> and(Predicate<? super T> other)
default Predicate<T> or(Predicate<? super T> other)
default Predicate<T> negate()

These methods return a composed Predicate that represents a short-circuiting logical AND and OR of this predicate and another and its logical negation.

Short-circuiting means that the other predicate won’t be evaluated if the value of the first predicate can predict the result of the operation (if the first predicate returns false in the case of AND or if it returns true in the case of OR).

These methods are useful to combine predicates and make the code more readable, for example:

Predicate<String> startsWithA = t -> t.startsWith("A");
Predicate<String> endsWithA = t -> t.endsWith("A");
boolean result = startsWithA.and(endsWithA).test("Hi");

Also, there’s a static method:

static <T> Predicate<T> isEqual(Object targetRef)

That returns a Predicate that tests if two arguments are equal according to Objects.equals(Object, Object).

There are also primitive versions for int, long, and double. They don’t extend from Predicate.

For example, here’s the definition of IntPredicate:

@FunctionalInterface
public interface IntPredicate {
    boolean test(int value);
    // And the default methods: and, or, negate
}

So instead of using:

Predicate<Integer> even = t -> t % 2 == 0;
boolean result = even.test(5);

You can use:

IntPredicate even = t -> t % 2 == 0;
boolean result = even.test(5);

Why?

Just to avoid the conversion from Integer to int and work directly with primitive types.

Notice that these primitive versions don’t have a generic type. Due to the way generics are implemented, parameters of the functional interfaces can be bound only to object types.

Since the conversion from the wrapper type (Integer) to the primitive type (int) uses more memory and comes with a performance cost, Java provides these versions to avoid autoboxing operations when inputs or outputs are primitives.

Here’s the corrected text:

Consumer

Consumer represents an operation that accepts a single input argument and returns no result, it just executes some operations on the argument.

This is how the interface is defined:

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
    // And a default method
    // ...
}

The functional descriptor (method signature) is:

T -> void

Here’s an example using an anonymous class:

Consumer<String> consumeStr = new Consumer<String>() {
    @Override
    public void accept(String t) {
        System.out.println(t);
    }
};
consumeStr.accept("Hi");

And with a lambda expression:

Consumer<String> consumeStr = t -> System.out.println(t);
consumeStr.accept("Hi");

This interface also has the following default method:

default Consumer<T> andThen(Consumer<? super T> after)

This method returns a composed Consumer that performs, in sequence, the operation of the consumer followed by the operation of the parameter.

These methods are useful to combine Consumers and make the code more readable, for example:

Consumer<String> first = t ->
    System.out.println("First:" + t);
Consumer<String> second = t ->
    System.out.println("Second:" + t);
first.andThen(second).accept("Hi");

The output is:

First: Hi
Second: Hi

Look how both Consumers take the same argument and the order of execution.

There are also primitive versions for int, long, and double. They don’t extend from Consumer.

For example, here’s the definition of IntConsumer:

@FunctionalInterface
public interface IntConsumer {
    void accept(int value);
    default IntConsumer andThen(IntConsumer after) {
        // ...
    }
}

So instead of using:

int[] a = { 1,2,3,4,5,6,7,8 };
printList(a, t -> System.out.println(t));
//...
void printList(int[] a, Consumer<Integer> c) {
    for(int i : a) {
        c.accept(i);
    }
}

You can use:

int[] a = { 1,2,3,4,5,6,7,8 };
printList(a, (IntConsumer) t -> System.out.println(t));
//...
void printList(int[] a, IntConsumer c) {
    for(int i : a) {
        c.accept(i);
    }
}

Function

Function represents an operation that takes an input argument of a certain type and produces a result of another type.

A common use is to convert or transform from one object to another.

This is how the interface is defined:

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
    // Other default and static methods
    // ...
}

The functional descriptor (method signature) is:

T -> R

Assuming a method:

void round(double d, Function<Double, Long> f) {
    long result = f.apply(d);
    System.out.println(result);
}

Here’s an example using an anonymous class:

round(5.4, new Function<Double, Long>() {
    @Override
    public Long apply(Double d) {
        return Math.round(d);
    }
});

And with a lambda expression:

round(5.4, d -> Math.round(d));

This interface also has the following default methods:

default <V> Function<V,R> compose(
    Function<? super V,? extends T> before)
default <V> Function<T,V> andThen(
    Function<? super R,? extends V> after)

The difference between these methods is that compose applies the function represented by the parameter first, and its result serves as the input to the other function. andThen first applies the function that calls the method, and its result acts as the input of the function represented by the parameter.

For example:

Function<String, String> f1 = s -> s.toUpperCase();
Function<String, String> f2 = s -> s.toLowerCase();
System.out.println(f1.compose(f2).apply("Compose"));
System.out.println(f1.andThen(f2).apply("AndThen"));

The output is:

compose
andthen

In the first case, f2 is the first function to be applied. In the second case, f2 is the last function to be applied.

Also, there’s a static method:

static <T> Function<T, T> identity()

That returns a function that always returns its input argument.

In the case of primitive versions, they also apply to int, long, and double, but there are more combinations than the previous interfaces:

Remember that these interfaces are for convenience, to work directly with primitives, for example:

DoubleFunction<R> instead of Function<Double, R>
ToLongFunction<T> instead of Function<T, Long>
IntToLongFunction instead of Function<Integer, Long>

Supplier

Supplier is the opposite of Consumer. It takes no arguments and only returns some value.

This is how the interface is defined:

@FunctionalInterface
public interface Supplier<T> {
    T get();
}

The functional descriptor (method signature) is:

() -> T

Here’s an example using an anonymous class:

String t = "One";
Supplier<String> supplierStr = new Supplier<String>() {
    @Override
    public String get() {
        return t.toUpperCase();
    }
};
System.out.println(supplierStr.get());

And with a lambda expression:

String t = "One";
Supplier<String> supplierStr = () -> t.toUpperCase();
System.out.println(supplierStr.get());

This interface doesn’t define default methods.

There are also primitive versions for int, long, double, and boolean, but they don’t extend from Supplier.

For example, here’s the definition of BooleanSupplier:

@FunctionalInterface
public interface BooleanSupplier {
    boolean getAsBoolean();
}

These primitive versions are used instead of Supplier for their respective types.

UnaryOperator

UnaryOperator is just a specialization of the Function interface (in fact, this interface extends from it) for when the argument and the result are of the same type.

This is how the interface is defined:

@FunctionalInterface
public interface UnaryOperator<T> extends Function<T, T> {
    // Just the identity
    // method is defined
}

The functional descriptor (method signature) is:

T -> T

Here’s an example using an anonymous class:

UnaryOperator<String> uOp = new UnaryOperator<String>() {
    @Override
    public String apply(String t) {
        return t.substring(0,2);
    }
};
System.out.println(uOp.apply("Hello"));

And with a lambda expression:

UnaryOperator<String> uOp = t -> t.substring(0,2);
System.out.println(uOp.apply("Hello"));

This interface inherits the default methods of the Function interface:

default <V> Function<V, T> compose(
    Function<? super V, ? extends T> before)
default <V> Function<T, V> andThen(
    Function<? super T, ? extends V> after)

And just defines the static method identity() for this interface (since static methods are not inherited):

static <T> UnaryOperator<T> identity()

That returns a UnaryOperator that always returns its input argument.

There are also primitive versions for int, long, and double. They don’t extend from UnaryOperator.

For example, here’s the definition of IntUnaryOperator:

@FunctionalInterface
public interface IntUnaryOperator {
    int applyAsInt(int operand);
    // Definitions for compose, andThen, and identity
}

So instead of using:

int[] a = {1,2,3,4,5,6,7,8};
int sum = sumNumbers(a, t -> t * 2);
//...
int sumNumbers(int[] a, UnaryOperator<Integer> unary) {
    int sum = 0;
    for(int i : a) {
        sum += unary.apply(i);
    }
    return sum;
}

You can use:

int[] a = {1,2,3,4,5,6,7,8};
int sum = sumNumbers(a, t -> t * 2);
//...
int sumNumbers(int[] a, IntUnaryOperator unary) {
    int sum = 0;
    for(int i : a) {
        sum += unary.applyAsInt(i);
    }
    return sum;
}

BiPredicate

This interface represents a predicate that takes two arguments.

It is defined as follows:

@FunctionalInterface
public interface BiPredicate<T, U> {
    boolean test(T t, U u);
    // Default methods are also defined
}

The functional descriptor (method signature) is:

(T, U) -> boolean

Here’s an example using an anonymous class:

BiPredicate<Integer, Integer> divisible =
    new BiPredicate<Integer, Integer>() {
        @Override
        public boolean test(Integer t, Integer u) {
            return t % u == 0;
        }
    };
boolean result = divisible.test(10, 5);

And with a lambda expression:

BiPredicate<Integer, Integer> divisible =
    (t, u) -> t % u == 0;
boolean result = divisible.test(10, 5);

This interface defines the same default methods as the Predicate interface, but with two arguments:

default BiPredicate<T, U> and(
    BiPredicate<? super T, ? super U> other) {
    return (t, u) -> test(t, u) && other.test(t, u);
}

default BiPredicate<T, U> or(
    BiPredicate<? super T, ? super U> other) {
    return (t, u) -> test(t, u) || other.test(t, u);
}

default BiPredicate<T, U> negate() {
    return (t, u) -> !test(t, u);
}

This interface doesn’t have primitive versions.

BiConsumer

This interface represents a consumer that takes two arguments (and doesn’t return a result).

This is how it is defined:

@FunctionalInterface
public interface BiConsumer<T, U> {
    void accept(T t, U u);
    // andThen default method is defined
}

The functional descriptor (method signature) is:

(T, U) -> void

Here’s an example using an anonymous class:

BiConsumer<String, String> consumeStr =
    new BiConsumer<String, String>() {
        @Override
        public void accept(String t, String u) {
            System.out.println(t + " " + u);
        }
    };
consumeStr.accept("Hi", "there");

And with a lambda expression:

BiConsumer<String, String> consumeStr =
    (t, u) -> System.out.println(t + " " + u);
consumeStr.accept("Hi", "there");

This interface also has the following default method:

default BiConsumer<T, U> andThen(
    BiConsumer<? super T, ? super U> after)

This method returns a composed BiConsumer that performs, in sequence, the operation of the consumer followed by the operation of the parameter. It will throw NullPointerException if the after parameter is null.

As in the case of a Consumer, these methods are useful to combine BiConsumers and make the code more readable, for example:

BiConsumer<String, String> first = (t, u) -> System.out.println(t.toUpperCase() + u.toUpperCase());
BiConsumer<String, String> second = (t, u) -> System.out.println(t.toLowerCase() + u.toLowerCase());
first.andThen(second).accept("Again", " and again");

The output is:

AGAIN AND AGAIN
again and again

There are also primitive specialization versions for int, long, and double. They don’t extend from BiConsumer, and instead of taking two ints, for example, they take one object and a primitive value as a second argument. So the naming convention changes to ObjXXXConsumer, where XXX is the primitive type. For example, here’s the definition of ObjIntConsumer:

@FunctionalInterface
public interface ObjIntConsumer<T> {
    void accept(T t, int value);
}

So instead of using:

int[] a = {1,2,3,4,5,6,7,8};
printList(a, (t, i) -> System.out.println(t + i));
//...
void printList(int[] a, BiConsumer<String, Integer> c) {
    for(int i : a) {
        c.accept("Number:", i);
    }
}

You can use:

int[] a = {1,2,3,4,5,6,7,8};
printList(a, (t, i) -> System.out.println(t + i));
//...
void printList(int[] a, ObjIntConsumer<String> c) {
    for(int i : a) {
        c.accept("Number:", i);
    }
}

BiFunction

This interface represents a function that takes two arguments of different types and produces a result of another type.

This is how it is defined:

@FunctionalInterface
public interface BiFunction<T, U, R> {
    R apply(T t, U u);
    // Other default and static methods
    // ...
}

The functional descriptor (method signature) is:

(T, U) -> R

Assuming a method:

void round(double d1, double d2, BiFunction<Double, Double, Long> f) {
    long result = f.apply(d1, d2);
    System.out.println(result);
}

Here’s an example using an anonymous class:

round(5.4, 3.8, new BiFunction<Double, Double, Long>() {
    @Override
    public Long apply(Double d1, Double d2) {
        return Math.round(d1 + d2);
    }
});

And with a lambda expression:

round(5.4, 3.8, (d1, d2) -> Math.round(d1 + d2));

This interface, unlike Function, has only one default method:

default <V> BiFunction<T, U, V> andThen(Function<? super R, ? extends V> after)

That returns a composed function that first applies the function that calls andThen to its input, and then applies the function represented by the argument to the result.

This interface also has fewer primitive versions than Function. It only has the versions that take generic types as arguments and return int, long and double primitive types, with the naming convention ToXXXBiFunction, where XXX is the primitive type.

For example, here’s the definition of ToIntBiFunction:

@FunctionalInterface
public interface ToIntBiFunction<T, U> {
    int applyAsInt(T t, U u);
}

This replaces BiFunction.

BinaryOperator

This interface is a specialization of the BiFunction interface (in fact, this interface extends it) for when the arguments and the result are of the same type.

This is how the interface is defined:

@FunctionalInterface
public interface BinaryOperator<T> extends BiFunction<T, T, T> {
    // Two static methods are defined
}

The functional descriptor (method signature) is:

(T, T) -> T

Here’s an example using an anonymous class:

BinaryOperator<String> binOp = new BinaryOperator<String>() {
    @Override
    public String apply(String t, String u) {
        return t.concat(u);
    }
};
System.out.println(binOp.apply("Hello", " there"));

And with a lambda expression:

BinaryOperator<String> binOp = (t, u) -> t.concat(u);
System.out.println(binOp.apply("Hello", " there"));

This interface inherits the default method of the BiFunction interface:

default <V> BiFunction<T, T, V> andThen(Function<? super T, ? extends V> after)

And defines two new static methods:

static <T> BinaryOperator<T> minBy(Comparator<? super T> comparator)
static <T> BinaryOperator<T> maxBy(Comparator<? super T> comparator)

That return a BinaryOperator, which returns the lesser or greater of two elements according to the specified Comparator.

Here’s a simple example:

BinaryOperator<Integer> biOp = BinaryOperator.maxBy(Comparator.naturalOrder());
System.out.println(biOp.apply(28, 8));

As you can see, these methods are just a wrapper to execute a Comparator.

Comparator.naturalOrder() returns a Comparator that compares Comparable objects in natural order. To execute it, we just call the apply() method with the two arguments needed by the BinaryOperator. Unsurprisingly, the output is:

28

There are also primitive versions for int, long, and double, where the two arguments and the return type are of the same primitive type. They don’t extend BinaryOperator or BiFunction.

For example, here’s the definition of IntBinaryOperator:

@FunctionalInterface
public interface IntBinaryOperator {
    int applyAsInt(int left, int right);
}

That you can use instead of BinaryOperator.

Primitive-specific Functional Interfaces

There’s also a set of functional interfaces that are specifically designed to work with primitive types. These interfaces can provide better performance than their generic counterparts when working with primitives, as they avoid the overhead of boxing and unboxing.

There are several categories of primitive-specific functional interfaces:

  1. ToDoubleFunction<T>, ToIntFunction<T>, ToLongFunction<T>: These interfaces represent functions that accept an object of type T and return a primitive double, int, or long, respectively. For example, this is how ToIntFunction<T> is defined:
    @FunctionalInterface
    public interface ToIntFunction<T> {
     int applyAsInt(T value);
    }
    

And here’s an example of how to use it:

ToIntFunction<String> stringToInt = Integer::parseInt;
int i = stringToInt.applyAsInt("123");  // 123
  1. ToDoubleBiFunction<T, U>, ToIntBiFunction<T, U>, ToLongBiFunction<T, U>: These interfaces represent functions that accept two objects of types T and U and return a primitive double, int, or long, respectively. For example, this is how ToIntBiFunction<T, U> is defined:
    @FunctionalInterface
    public interface ToIntBiFunction<T, U> {
     int applyAsInt(T t, U u);
    }
    

And here’s an example of how to use it:

ToIntBiFunction<String, String> comparator = String::compareTo;
int result = comparator.applyAsInt("abc", "def");  // a negative value
  1. DoubleToIntFunction, DoubleToLongFunction, IntToDoubleFunction, IntToLongFunction, LongToDoubleFunction, LongToIntFunction: These interfaces represent functions that accept one primitive type and return another primitive type. For example, this is how DoubleToIntFunction is defined:
    @FunctionalInterface
    public interface DoubleToIntFunction {
     int applyAsInt(double value);
    }
    

And here’s an example of how to use it:

DoubleToIntFunction roundDown = d -> (int) d;
int i = roundDown.applyAsInt(9.9);  // 9
  1. ObjDoubleConsumer<T>, ObjIntConsumer<T>, ObjLongConsumer<T>: These interfaces represent functions that accept an object of type T and a primitive double, int, or long, and return void. For example, this is how ObjIntConsumer<T> is defined:
    @FunctionalInterface
    public interface ObjIntConsumer<T> {
    
     /**
      * Performs this operation on the given arguments.
      *
      * @param t the first input argument
      * @param value the second input argument
      */
     void accept(T t, int value);
    }
    

And here’s an example of how to use it:

ObjIntConsumer<List<Integer>> listAddInt = List::add;
List<Integer> list = new ArrayList<>();
listAddInt.accept(list, 1);  // [1]

These interfaces differ from DoubleFunction<R>, IntFunction<R>, LongFunction<R>, etc., in that the latter accept a primitive and return an object. For example:

IntFunction<String> intToString = Integer::toString;
String s = intToString.apply(123);  // "123"

The choice of which interface to use depends on your specific needs. If you’re working primarily with primitives and want to avoid the overhead of autoboxing and unboxing, the primitive-specific interfaces are a good choice. However, if you need to work with objects, or if the boxing overhead is not a concern, the generic interfaces like Function<T, R> and BiFunction<T, U, R> are often more convenient.

Method References

As you know, in Java we can use references to objects, either by creating new objects:

List list = new ArrayList();
store(new ArrayList());

Or by using existing objects:

List list2 = list;
isFull(list2);

But what about a reference to a method?

If we only use a method of an object in another method, we still have to pass the full object as an argument. Wouldn’t it be more practical to just pass the method as an argument? Like this for example:

isFull(list.size);

Thanks to lambda expressions, we can do something like that. We can use methods as if they were objects or primitive values.

And that’s because a method reference is the shorthand syntax for a lambda expression that executes just one method.

Here’s the syntax for a method reference:

Object :: methodName

You can use lambda expressions instead of using an anonymous class, but sometimes, the lambda expression is really just a call to some method. For example:

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

To make the code clearer, you can turn that lambda expression into a method reference:

Consumer<String> c = System.out::println;

In a method reference, you place the object (or class) that contains the method before the :: operator and the name of the method after it without arguments.

But you may be thinking:

First of all, a method reference can’t be used for any method. They can be used only to replace a single-method lambda expression.

So to use a method reference you first need a lambda expression with one method. And to use a lambda expression you first need a functional interface, an interface with just one abstract method.

In other words:

Instead of using

AN ANONYMOUS CLASS

you can use

A LAMBDA EXPRESSION

And if this just calls one method, you can use

A METHOD REFERENCE

There are four types of method references:

Let’s begin by explaining the most natural case, a static method.

Static Methods

In this case, we have a lambda expression like the following:

(args) -> Class.staticMethod(args)

That can be turned into the following method reference:

Class::staticMethod

Notice that between a static method and a static method reference instead of the . operator, we use the :: operator, and that we don’t pass arguments to the method reference.

In general, we don’t have to pass arguments to method references. However, arguments are treated depending on the type of method reference.

In this case, any arguments (if any) taken by the method are passed automatically behind the curtains.

Wherever we can pass a lambda expression that just calls a static method, we can use a method reference. For example, assuming this class:

class Numbers {
    public static boolean isMoreThanFifty(int n1, int n2) {
        return (n1 + n2) > 50;
    }
    public static List<Integer> findNumbers(
        List<Integer> l, BiPredicate<Integer, Integer> p) {
        List<Integer> newList = new ArrayList<>();
        for (Integer i : l) {
            if (p.test(i, i + 10)) {
                newList.add(i);
            }
        }
        return newList;
    }
}

We can call the findNumbers() method:

List<Integer> list = Arrays.asList(12, 5, 45, 18, 33, 24, 40);

// Using an anonymous class
findNumbers(list, new BiPredicate<Integer, Integer>() {
    public boolean test(Integer i1, Integer i2) {
        return Numbers.isMoreThanFifty(i1, i2);
    }
});

// Using a lambda expression
findNumbers(list, (i1, i2) -> Numbers.isMoreThanFifty(i1, i2));

// Using a method reference
findNumbers(list, Numbers::isMoreThanFifty);

Instance Method of An Object of A Particular Type

In this case, we have a lambda expression like the following:

(obj, args) -> obj.instanceMethod(args)

Where an instance of an object is passed, and one of its methods is executed with some optional parameters.

That can be turned into the following method reference:

ObjectType::instanceMethod

This time, the conversion is not that straightforward. First, in the method reference, we don’t use the instance itself. We use its type.

Second, the other argument of the lambda expression, if any, is not used in the method reference, but it’s passed behind the curtains like in the static method case.

For example, assuming this class:

class Shipment {
    public double calculateWeight() {
        double weight = 0;
        // Calculate weight
        return weight;
    }
}

And this method:

public List<Double> calculateOnShipments(
    List<Shipment> l, Function<Shipment, Double> f) {
    List<Double> results = new ArrayList<>();
    for (Shipment s : l) {
        results.add(f.apply(s));
    }
    return results;
}

We can call that method using:

List<Shipment> l = new ArrayList<Shipment>();

// Using an anonymous class
calculateOnShipments(l, new Function<Shipment, Double>() {
    public Double apply(Shipment s) { // The object
        return s.calculateWeight(); // The method
    }
});

// Using a lambda expression
calculateOnShipments(l, s -> s.calculateWeight());

// Using a method reference
calculateOnShipments(l, Shipment::calculateWeight);

In this example, we don’t pass any arguments to the method. The key point here is that an instance of the object is the parameter of the lambda expression, and we form the reference to the instance method with the type of the instance.

Here’s another example where we pass two arguments to the method reference.

Java has a Function interface that takes one parameter, a BiFunction that takes two parameters, but there’s no TriFunction that takes three parameters, so let’s make one:

interface TriFunction<T, U, V, R> {
    R apply(T t, U u, V v);
}

Now assume a class with a method that takes two parameters and returns a result, like this:

class Sum {
    Integer doSum(String s1, String s2) {
        return Integer.parseInt(s1) + Integer.parseInt(s2);
    }
}

We can wrap the doSum() method within a TriFunction implementation using an anonymous class:

TriFunction<Sum, String, String, Integer> anon =
    new TriFunction<Sum, String, String, Integer>() {
        @Override
        public Integer apply(Sum s, String arg1, String arg2) {
            return s.doSum(arg1, arg2);
        }
    };
System.out.println(anon.apply(new Sum(), "1", "4"));

Or using a lambda expression:

TriFunction<Sum, String, String, Integer> lambda =
    (Sum s, String arg1, String arg2) -> s.doSum(arg1, arg2);
System.out.println(lambda.apply(new Sum(), "1", "4"));

Or just using a method reference:

TriFunction<Sum, String, String, Integer> mRef = Sum::doSum;
System.out.println(mRef.apply(new Sum(), "1", "4"));

Here:

It may seem odd to just see the interface, the class, and how they are used with a method reference, but this becomes more evident when you see the anonymous class or even the lambda version.

From:

(Sum s, String arg1, String arg2) -> s.doSum(arg1, arg2)

To

Sum::doSum

Instance Method of An Existing Object

In this case, we have a lambda expression like the following:

(args) -> obj.instanceMethod(args)

That can be turned into the following method reference:

obj::instanceMethod

This time, an instance defined elsewhere is used, and the arguments (if any) are passed behind the scenes like in the static method case.

For example, assuming these classes:

class Car {
    private int id;
    private String color;
    // More properties
    // And getters and setters
}
class Mechanic {
    public void fix(Car c) {
        System.out.println("Fixing car " + c.getId());
    }
}

And

this method:

public static void execute(Car car, Consumer<Car> c) {
    c.accept(car);
}

We can call the above method using:

final Mechanic mechanic = new Mechanic();
Car car = new Car();

// Using an anonymous class
execute(car, new Consumer<Car>() {
    public void accept(Car c) {
        mechanic.fix(c);
    }
});

// Using a lambda expression
execute(car, c -> mechanic.fix(c));

// Using a method reference
execute(car, mechanic::fix);

The key in this case is to use any object visible by an anonymous class/lambda expression and pass some arguments to an instance method of that object.

Here’s another quick example using another Consumer:

Consumer<String> c = System.out::println;
c.accept("Hello");

Constructor

In this case, we have a lambda expression like the following:

(args) -> new ClassName(args)

That can be turned into the following method reference:

ClassName::new

The only thing this lambda expression does is to create a new object, so we just reference a constructor of the class with the keyword new. As in the other cases, arguments (if any) are not passed in the method reference.

Most of the time, we can use this syntax with two (or three) interfaces from the java.util.function package.

If the constructor takes no arguments, a Supplier will do the job:

// Using an anonymous class
Supplier<List<String>> s = new Supplier<List<String>>() {
    public List<String> get() {
        return new ArrayList<String>();
    }
};
List<String> l = s.get();

// Using a lambda expression
Supplier<List<String>> s = () -> new ArrayList<String>();
List<String> l = s.get();

// Using a method reference
Supplier<List<String>> s = ArrayList::new;
List<String> l = s.get();

If the constructor takes an argument, we can use the Function interface. For example:

// Using an anonymous class
Function<String, Integer> f =
    new Function<String, Integer>() {
        public Integer apply(String s) {
            return new Integer(s);
        }
    };
Integer i = f.apply("100");

// Using a lambda expression
Function<String, Integer> f = s -> new Integer(s);
Integer i = f.apply("100");

// Using a method reference
Function<String, Integer> f = Integer::new;
Integer i = f.apply("100");

If the constructor takes two arguments, we use the BiFunction interface:

// Using an anonymous class
BiFunction<String, String, Locale> f = new BiFunction<String, String, Locale>() {
    public Locale apply(String lang, String country) {
        return new Locale(lang, country);
    }
};
Locale loc = f.apply("en", "UK");

// Using a lambda expression
BiFunction<String, String, Locale> f = (lang, country) -> new Locale(lang, country);
Locale loc = f.apply("en", "UK");

// Using a method reference
BiFunction<String, String, Locale> f = Locale::new;
Locale loc = f.apply("en", "UK");

If you have a constructor with three or more arguments, you would have to create your own functional interface.

You can see that referencing a constructor is very similar to referencing a static method. The difference is that the constructor’s method name is new.

Many of the examples of this chapter are very simple and probably don’t justify the use of lambda expressions or method references.

As mentioned at the beginning of the chapter, use method references if they make your code clearer.

You can avoid the one method restriction by grouping all your code in a static method, for example, and create a reference to that method instead of using a class or a lambda expression with many lines.

But the real power of lambda expressions and method references comes when they are combined with another feature of Java: streams.

That will be the topic of the next chapter.

Key Points

Practice Questions

1. Which of the following statements are true about functional interfaces in Java? (Choose all that apply.)

A) A functional interface can have multiple abstract methods.
B) A functional interface can have default and static methods.
C) The @FunctionalInterface annotation is mandatory to declare a functional interface.
D) Lambda expressions can be used to instantiate functional interfaces.

2. Which of the following lambda expressions correctly implements the Comparator<String> interface?

Comparator<String> comparator = /* lambda expression */;

A) (s1, s2) -> s1.compareTo(s2)
B) (String s1, s2) -> s1.compareTo(s2)
C) s1, s2 -> s1.compareTo(s2)
D) (s1, s2) -> return s1.compareTo(s2);
E) (s1, s2) -> { s1.compareTo(s2); }

3. Which of the following Java built-in lambda interfaces represents a function that accepts two arguments and produces a result?

A) java.util.function.Function
B) java.util.function.BiFunction
C) java.util.function.Supplier
D) java.util.function.Consumer
E) java.util.function.Predicate

4. What is the output of the following code?

import java.util.function.Function;

public class Main {
    public static void main(String[] args) {
        Function<Integer, Integer> multiplyByTwo = x -> x * 2;
        Function<Integer, Integer> addThree = x -> x + 3;

        Function<Integer, Integer> combinedFunction = multiplyByTwo.andThen(addThree);

        System.out.println(combinedFunction.apply(5));
    }
}

A) 13
B) 16
C) 10
D) 11
E) 8

5. Which of the following method references correctly replaces the lambda expression in the code below?

import java.util.function.Function;

public class Main {
    public static void main(String[] args) {
        Function<String, Integer> func = str -> Integer.parseInt(str);
        System.out.println(func.apply("123"));
    }
}

A) String::valueOf
B) Integer::valueOf
C) Integer::parseInt
D) String::parseInt
E) Integer::toString

Do you like what you read? Would you consider?


Do you have a problem or something to say?

Report an issue with the book

Contact me