Use Java object and primitive Streams, including lambda expressions implementing functional interfaces, to supply, filter, map, consume, and sort data.
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
.
@FunctionalInterface
AnnotationJava 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:
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.
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
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 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
.
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.
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:
this
refers to the instance of the anonymous class itself. In a lambda expression, this
refers to the enclosing class instance.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.
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:
Predicate<T>
Consumer<T>
Function<T, R>
Supplier<T>
UnaryOperator<T>
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:
IntPredicate
LongConsumer
BooleanSupplier
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:
BiPredicate<L, R>
BiConsumer<T, U>
BiFunction<T, U, R>
BinaryOperator<T>
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 Consumer
s 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 Consumer
s 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:
IntFunction
:
@FunctionalInterface
public interface IntFunction<R> {
R apply(int value);
}
ToIntFunction
:
@FunctionalInterface
public interface ToIntFunction<T> {
int applyAsInt(T value);
}
IntToDoubleFunction
:
@FunctionalInterface
public interface IntToDoubleFunction {
double applyAsDouble(int value);
}
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 BiConsumer
s 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 int
s, 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
.
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:
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
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
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
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.
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.
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);
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:
TriFunction
is the object type that contains the method to execute.TriFunction
is the type of the first parameter.TriFunction
is the type of the second parameter.TriFunction
is the return type of the method to execute. Notice how this is omitted (inferred) in the lambda expression and the method reference.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
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");
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.
A functional interface is an interface that contains only one abstract method. It may contain default or static methods.
The @FunctionalInterface
annotation is used to indicate an interface is intended to be a functional interface. It enables compiler checks but is not required if the interface meets the functional interface criteria.
If an interface declares an abstract method overriding a public method in java.lang.Object
, it doesn’t count toward the interface’s abstract method count.
Lambda expressions enable you to treat functionality as a method argument or code as data.
The syntax of a lambda is: (parameters) -> expression
or (parameters) -> { statements; }
.
You can use var
in the parameter list of a lambda expression to allow type inference.
The target type of a lambda expression can be inferred in contexts like variable declarations, assignments, return statements, array initializers, method/constructor arguments, ternary expressions, and cast expressions.
Lambda expressions are similar to anonymous classes in some ways, like the use of local variables, but differ in their treatment of this
, default methods, parameter lists, and instance variables.
Local variables used in a lambda expression or anonymous class must be final or effectively final (not modified after initialization) for thread safety and consistency.
Java provides built-in functional interfaces in the java.util.function
package for common use cases. The main ones are Predicate<T>
, Consumer<T>
, Function<T, R>
, Supplier<T>
, and UnaryOperator<T>
.
These interfaces also have primitive specializations (like IntPredicate
, LongConsumer
, etc.) to avoid autoboxing overhead when working with primitives.
There are also binary versions of some of these interfaces that accept two parameters, like BiPredicate<L, R>
, BiConsumer<T, U>
, BiFunction<T, U, R>
, and BinaryOperator<T>
.
Predicate<T>
represents a boolean-valued function that takes an object of type T
as input. It has and
, or
, and negate
default methods for combining predicates.
Consumer<T>
represents an operation that accepts a single input argument and returns no result. It has an andThen
default method for chaining consumers.
Function<T, R>
represents a function that accepts one argument and returns a result. It has compose
and andThen
default methods for combining functions.
Supplier<T>
represents a supplier of results, it takes no arguments and returns a result.
UnaryOperator<T>
represents an operation on a single operand that produces a result of the same type as its operand. It’s a specialization of Function
where the argument and result types are the same.
Method references provide a way to refer to a method without invoking it, using the ::
operator. They can be used where a lambda expression is expected.
There are four kinds of method references: to a static method, to an instance method of an object of a particular type, to an instance method of an existing object, and to a constructor.
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?