In this chapter, you’ll learn how to adopt lambda expressions, the flagship feature of Java 8. First, you’ll learn about a pattern called behavior parameterization, which lets you write code that can cope with requirement changes. Then, you’ll see how lambda expressions let you use this pattern in a more concise way than what was possible before Java 8. Next, you’ll learn precisely where and how to use lambda expressions. You’ll also learn about method references, another Java 8 feature that lets you write code that is more succinct and descriptive. You’ll then bring all this new knowledge together into a practical refactoring example. Finally, you’ll also learn how to test using lambda expressions and method references.
The motivation for introducing lambda expressions into Java is related to a pattern called behavior parameterization. This pattern lets you cope with requirement changes by letting you write more flexible code. Prior to Java 8, this pattern was very verbose. Lambda expressions fix that by letting you utilize the behavior parameterization pattern in a concise way. Here’s an example: say you need to find invoices greater than a certain amount. You could create a method findInvoicesGreaterThanAmount
:
List
<
Invoice
>
findInvoicesGreaterThanAmount
(
List
<
Invoice
>
invoices
,
double
amount
)
{
List
<
Invoice
>
result
=
new
ArrayList
<>();
for
(
Invoice
inv:
invoices
)
{
if
(
inv
.
getAmount
()
>
amount
)
{
result
.
add
(
inv
);
}
}
return
result
;
}
Using this method is simple enough. However, what if you need to also find invoices smaller than a certain amount? Or worse, what if you need to find invoices from a given customer and also of a certain amount? Or, what if you need to query many different properties on the invoice? You need a way to parameterize the behavior of filter
with some form of condition. Let’s represent this condition by defining InvoicePredicate
interface and refactoring the method to make use of it:
interface
InvoicePredicate
{
boolean
test
(
invoice
inv
);
}
List
<
Invoice
>
findInvoices
(
List
<
Invoice
>
invoices
,
InvoicePredicate
p
)
{
List
<
Invoice
>
result
=
new
ArrayList
<>();
for
(
Invoice
inv:
invoices
)
{
if
(
p
.
test
(
inv
))
{
result
.
add
(
inv
);
}
}
return
result
;
}
With this useful code, you can cope with any requirement changes involving any property of an Invoice
object. You just need to create different InvoicePredicate
objects and pass them to the find
Invoices
method. In other words, you have parameterized the behavior of findInvoices
. Unfortunately, using this new method introduces additional verbosity, as shown here:
List
<
Invoice
>
expensiveInvoicesFromOracle
=
findInvoices
(
invoices
,
new
InvoicePredicate
()
{
public
test
(
Invoice
inv
)
{
return
inv
.
getAmount
()
>
10_000
&&
inv
.
getCustomer
()
==
Customer
.
ORACLE
;
}
});
In other words, you have more flexibility but less readability. Ideally, you want both flexibility and conciseness, and that’s where lambda expressions come in. Using this feature, you can refactor the preceding code as follows:
List
<
Invoice
>
expensiveInvoicesFromOracle
=
findInvoices
(
invoices
,
inv
->
inv
.
getAmount
()
>
10_000
&&
inv
.
getCustomer
()
==
Customer
.
ORACLE
);
Now that you know why you need need lambda expressions, it’s time to learn more precisely what they are. In the simplest terms, a lambda expression is an anonymous function that can be passed around. Let’s take a look at this definition in greater detail:
A lambda expression is anonymous because it does not have an explicit name as a method normally would. It’s sort of like an anonymous class in that it does not have a declared name.
A lambda is like a method in that it has a list of parameters, a body, a return type, and a possible list of exceptions that can be thrown. However, unlike a method, it’s not declared as part of a particular class.
A lambda expression can be passed as an argument to a method, stored in a variable, and also returned as a result.
Before you can write your own lambda expressions, you need to know the syntax. You have seen a couple of lambda expressions in this guide already:
Runnable
r
=
()
->
System
.
out
.
println
(
"Hi"
);
FileFilter
isXml
=
(
File
f
)
->
f
.
getName
().
endsWith
(
".xml"
);
These two lambda expressions have three parts:
A list of parameters, e.g. (File f)
An arrow composed of the two characters -
and >
A body, e.g. f.getName().endsWith(".xml")
There are two forms of lambda expressions. You use the first form when the body of the lambda expression is a single expression:
(
parameters
)
->
expression
You use the second form when the body of the lambda expression contains one or multiple statements. Note that you have to use curly braces surrounding the body of the lambda expression:
(
parameters
)
->
{
statements
;}
Generally, one can omit the type declarations from the lambda parameters if they can be inferred. In addition, one can omit the parentheses if there is a single parameter.
Now that you know how to write lambda expressions, the next question to consider is how and where to use them. In a nutshell, you can use a lambda expression in the context of a functional interface. A functional interface is one with a single abstract method. Take, for example, the two lambda expressions from the preceding code:
Runnable
r
=
()
->
System
.
out
.
println
(
"Hi"
);
FileFilter
isXml
=
(
File
f
)
->
f
.
getName
().
endsWith
(
".xml"
);
Runnable
is a functional interface because it defines a single abstract method called run
. It turns out FileFilter
is also a functional interface because it defines a single abstract method, called accept
:
@FunctionalInterface
public
interface
Runnable
{
void
run
();
}
@FunctionalInterface
public
interface
FileFilter
{
boolean
accept
(
File
pathname
);
}
The important point here is that lambda expressions let you create an instance of a functional interface. The body of the lambda expression provides the implementation for the single abstract method of the functional interface. As a result, the following uses of Runnable
via anonymous classes and lambda expressions will produce the same output:
Runnable
r1
=
new
Runnable
()
{
public
void
run
()
{
System
.
out
.
println
(
"Hi!"
);
}
};
r1
.
run
();
Runnable
r2
=
()
->
System
.
out
.
println
(
"Hi!"
);
r2
.
run
();
You’ll often see the annotation @FunctionalInterface
on interfaces. It’s similar to using the @Override
annotation to indicate that a method is overridden. Here, the @FunctionalInterface
annotation is used for documentation to indicate that the interface is intended to be a functional interface. The compiler will also report an error if the interface annotated doesn’t match the definition of a functional interface.
You’ll find several new functional interfaces such as Function<T, R>
and Supplier<T>
in the package java.util.function
, which you can use for various forms of lambda expressions.
Method references let you reuse existing method definitions and pass them around just like lambda expressions. They are useful in certain cases to write code that can feel more natural and readable compared to lambda expressions. For example, you can find hidden files using a lambda expression as follows:
File
[]
hiddenFiles
=
mainDirectory
.
listFiles
(
f
->
f
.
isHidden
());
Using a method reference, you can directly refer to the method isHidden
using the double colon syntax (::
).
File
[]
hiddenFiles
=
mainDirectory
.
listFiles
(
File:
:
isHidden
);
The most simple way to think of a method reference is as a shorthand notation for lambda expressions calling for a specific method. There are four main kinds of method references:
A method reference to a static method:
Function
<
String
,
Integer
>
converter
=
Integer:
:
parseInt
;
Integer
number
=
converter
.
apply
(
"10"
);
A method reference to an instance method. Specifically, you’re referring to a method of an object that will be supplied as the first parameter of the lambda:
Function
<
Invoice
,
Integer
>
invoiceToId
=
Invoice:
:
getId
;
A method reference to an instance method of an existing object:
Consumer
<
Object
>
=
System
.
out
::
println
;
Specifically, this kind of method reference is very useful when you want to refer to a private helper method and inject it into another method:
File
[]
hidden
=
mainDirectory
.
listFiles
(
this
::
isXML
);
private
boolean
isXML
(
File
f
)
{
return
f
.
getName
.
endsWith
(
".xml"
);
}
A constructor reference:
Supplier
<
List
<
String
>>
listOfString
=
List:
:
new
;
At the start of this chapter, you saw this verbose example of Java code for sorting invoices:
Collections
.
sort
(
invoices
,
new
Comparator
<
Invoice
>()
{
public
int
compare
(
Invoice
inv1
,
Invoice
inv2
)
{
return
Double
.
compare
(
inv2
.
getAmount
(),
inv1
.
getAmount
());
}
});
Now you’ll see exactly how to use the Java 8 features you’ve learned so far to refactor this code so it’s more readable and concise.
First, notice that Comparator
is a functional interface because it only declares a single abstract method called compare
, which takes two objects of the same type and returns an integer. This is an ideal situation for a lambda expression, like this one:
Collections
.
sort
(
invoices
,
(
Invoice
inv1
,
Invoice
inv2
)
->
{
return
Double
.
compare
(
inv2
.
getAmount
(),
inv1
.
getAmount
());
});
Since the body of the lambda expression is simply returning the value of an expression, you can use the more concise form of lambda expression:
Collections
.
sort
(
invoices
,
(
Invoice
inv1
,
Invoice
inv2
)
->
Double
.
compare
(
inv2
.
getAmount
(),
inv1
.
getAmount
()));
In Java 8, the List
interface supports the sort
method, so you can use that instead of Collections.sort
:
invoices
.
sort
((
Invoice
inv1
,
Invoice
inv2
)
->
Double
.
compare
(
inv2
.
getAmount
(),
inv1
.
getAmount
()));
Next, Java 8 introduces a static helper, Comparator.
comparing
, which takes as argument a lambda to extract a comparable key. It then generates a Comparator
object for you. You can use it as follows:
Comparator
<
Invoice
>
byAmount
=
Comparator
.
comparing
((
Invoice
inv
)
->
inv
.
getAmount
());
invoices
.
sort
(
byAmount
);
You may notice that the more concise method reference Invoice::getAmount
can simply replace the lambda (Invoice inv) -> inv.getAmount()
:
Comparator
<
Invoice
>
byAmount
=
Comparator
.
comparing
(
Invoice:
:
getAmount
);
invoices
.
sort
(
byAmount
);
Since the method getAmount
returns a primitive double
, you can use Comparator.comparingDouble
, which is a primitive specialized version of Comparator.comparing
, to avoid unnecessary boxing:
Comparator
<
Invoice
>
byAmount
=
Comparator
.
comparingDouble
(
Invoice:
:
getAmount
);
invoices
.
sort
(
byAmount
);
Finally, let’s tidy up the code and use an import static and also get rid of the local variable holding the Comparator
object to produce a solution that reads like the problem statement:
import
static
java
.
util
.
Comparator
.
comparingDouble
;
invoices
.
sort
(
comparingDouble
(
Invoice:
:
getAmount
));
You may be concerned with how lambda expressions are going to affect testing. After all, lambda expressions introduce behaviors that need to be tested. When deciding how to test code that contains lambda expressions, consider the following two options:
If the lambda expression is small, test the behavior of the surrounding code that uses it.
If the lambda expression is reasonably complex, extract it into a separate method reference that you can inject and test in isolation.
Here are the key concepts from this chapter:
A lambda expression can be understood as a kind of anonymous function.
Lambda expressions and the behavior parameterization pattern let you write code that is both flexible and concise.
A functional interface is an interface that declares a single abstract method.
Lambda expressions can only be used in the context of a functional interface.
Method references can be a more natural alternative to lambda expressions when you need to reuse an existing method and pass it around.
In the context of testing, extract large lambda expressions into separate methods that you can then inject using method references.