Lambda Expression
Replace Anonymous Class with Lambda
A lambda expression in Java is a concise and straightforward way of declaring and instantiating a class which implements a functional interface.
We’ll explore multiple means to pass functionalities to method invocations in the order from redundant to concise. Suppose that we iterate through a list and print elements if they satisfy a certain condition.
public class Test {
public static void doSomething(
List<Person> roster,
Filter filter
) {
for (Person p : roster) {
if (filter.test(p)) {
System.out.println(p);
}
}
}
public interface Filter {
boolean test(Person p);
}
@Data
@AllArgsConstructor
public static class Person {
private String name;
private int age;
}
}
When invoking the method doSomething(List<Person> roster, Filter filter)
, we can declare and instantiate a concrete class whose type is Filter
using anonymous class. (Note that anonymous classes can be used when extending another class as well as implementing an interface.)
public class Test {
public static void doSomething(
List<Person> roster,
Filter filter
){...};
public interface Filter {...}
public static class Person {...}
public static void main(String args[]) {
List<Person> roster = Arrays.asList(
new Person("Alice", 20),
new Person("Bob", 25),
new Person("Chris", 30)
);
// anonymous class
Filter filter = new Filter() {
@Override
public boolean test(Person p) {
return p.getAge() <= 28;
}
};
doSomething(roster, filter);
}
}
Now, if the declared type is a functional interface, anonymous classes can be replaced with lambda expressions. An interface is said to be functional if it has only a single abstract method. With lambda expressions, the previous code can be written more concisely.
public class Test {
public static void doSomething(
List<Person> roster,
Filter filter
){...};
public interface Filter {...}
public static class Person {...}
public static void main(String args[]) {
List<Person> roster = ...;
// lambda expression
Filter filter = p -> p.getAge() <= 28;
doSomething(roster, filter);
}
}
Additionally, standard functional interfaces for general purposes have already been declared in java.util.function
package. In this case, we don’t need to declare our own Filter
interface because it can be replaced with one of built-in functional interfaces, Predicate<T>
.
public class Test {
public static void doSomething(
List<Person> roster,
Predicate<Person> filter // built-in functional interface
){...};
public static class Person {...}
public static void main(String args[]) {
List<Person> roster = ...;
// lambda expression
Predicate<Person> filter = p -> p.getAge() <= 28;
doSomething(roster, filter);
}
}
What we discussed so far is summarized in the following table.
Furthermore, we can replace the previous code with lambda expressions to a greater extent using another functional interface Consumer<T>
.
public class Test {
public static void doSomething(
List<Person> roster,
Predicate<Person> filter,
Consumer<Person> consumer // pass functionality
){
for (Person p : roster) {
if (filter.test(p)) {
consumer.accept(p);
}
}
};
public static class Person {...}
public static void main(String args[]) {
List<Person> roster = ...;
// lambda expression
Predicate<Person> filter = p -> p.getAge() <= 28;
Consumer<Person> consumer = p -> System.out.println(p);
doSomething(roster, filter, consumer);
}
}
The well-known built-in functional interfaces are shown in the following table.
Note that our implementation of Consumer<T>.accept(T t)
is just an invocation of an existing method. In such cases, a lambda expression can be written more concisely using method reference.
// lambda expression
Consumer<Person> consumer = p -> System.out.println(p);
// method reference
Consumer<Person> consumer = System.out::println;
When Do We Need to Define Our Own Functional Interface?
Although there are numerous built-in functional interfaces available, there are occasions where it becomes necessary to define our own functional interfaces. This is discussed in Effective Java:
… you need to write your own if none of the standard ones does what you need, for example if you require a predicate that takes three parameters, or one that throws a checked exception.
Item44: Favor the use of standard functional interfaces
Stream
What Is Stream?
Package java.util.stream
provides us with the abstraction of stream literally. A stream is not a data structure that stores elements; instead, it conveys elements from a source such as data structures through a pipeline of operations.
A stream pipeline consists of a source (e.g. Collection
), zero or more intermediate operations, and a terminal operation. Intermediate operations do not apply the operation; instead it creates a new stream that contains the elements to which the operation will be applied when traversed. Afterward, a terminal operation traverses the stream to produce a result.
roster // a source
.stream() // 0 or more intermediate
.filter(p -> p.getAge() <= 28) // 0 or more intermediate
.map(Person::getName) // 0 or more intermediate
.forEach(System.out::println); // a terminal operation
Reduction
A reduction operation is a terminal operation which combines a sequence of elements into a single value by repeatedly applying operations. On the other hand, a mutable reduction operation accumulates a sequence of elements into a mutable result container, rather than producing a single result. Stream<T>.reduce
and Stream<T>.collect
serve as general-purpose reduction methods respectively:
Here are some simple examples:
// reduction
Integer max = roster.stream()
.reduce(
0, // identity
(prev, p) -> Math.max(prev, p.getAge()), // accumulator
Math::max // combiner
);
// mutable reduction
List<String> strings = roster.stream()
.collect(
ArrayList::new, // supplier
(prev, p) -> prev.add(p.getName()), // accumulator
ArrayList::addAll // combiner
);
There’s another version of Stream<T>.collect
which takes java.util.stream.Collector
as its parameter. A Collector
represents a mutable reduction operation. In other words, it encapsulates the three functions required in the previous version.
java.util.stream.Collectors
class provides you with useful static methods which generate Collector
instance. For instance, the following two code snippets work identically.
// collect method takes three functions
List<String> strings = roster.stream()
.map(Person::getName)
.collect(
ArrayList::new,
ArrayList::add,
ArrayList::addAll
);
// collect method takes Collector
List<String> strings2 = roster.stream()
.map(Person::getName)
.collect(Collectors.toList());
Collectors.reducing
and Collectors.mapping
serve as general-purpose methods to generate Collector
instances.
Collectors.reducing
returns like a map & reduce reduction.Collectors.mapping
returns like a map & collect reduction.
They are the most useful when used in a multi-level reduction, such as downstream of Collectors.groupingBy
method:
Map<GENDER, Integer> totalAgeByGender = roster.stream().collect(
Collectors.groupingBy(
Person::getGender,
Collectors.reducing( // map & reduce
0,
Person::getAge,
Integer::sum
)));
Map<GENDER, List<String>> namesByGender = roster.stream().collect(
Collectors.groupingBy(
Person::getGender,
Collectors.mapping( // map & collect
Person::getName,
Collectors.toList()
)));
What Stream Can Not Do
Comparison Between Stream and Iterator
Streams don’t make iterations obsolete. This is discussed in Effective Java:
There are some things you can do from code blocks that you can’t do from function objects:
Item45: Use streams judiciously
- From a codeblock you can read or modify any local variable in scope;…
- From a code block, you can
return
from the enclosing method,break
orcontinue
an enclosing loop, or throw any checked exception that this method is declared to throw; from a lambda you can do none of these things.
Handling Checked Exceptions in Stream APIs
Stream APIs accept standard functional interfaces as parameters. However, all abstract methods declared in these standard functional interfaces cannot throw any checked exception due to their signatures. Consequently, we cannot pass operations which could throw checked exceptions to stream APIs directly.
The well-known approach to avoid it is:
- Define our own functional interface whose abstract method might throw checked exceptions.
- Create a wrapper method which takes that functional interface as its parameter and invokes its abstract method. It returns the standard functional interface required in stream APIs if unchecked exceptions are not thrown, or catches and handles it or just converts to an unchecked exception otherwise.
- Invoke a stream API with parameters of that wrapper method, instead of the operation directly.
public class Test {
@FunctionalInterface
interface MyFunction<T, R, E extends Exception> {
R apply(T t) throws E;
}
public static <T, R, E extends Exception> Function<T, R> wrapper(MyFunction<T, R, E> func) {
return t -> {
try {
return func.apply(t);
} catch (Exception e) {
throw new RuntimeException(e);
}
};
}
public static void main(String args[]) {
Stream.of(1, 2, 3)
.map(wrapper(t -> {
throw new Exception();
}));
// cannot compile !
Stream.of(1, 2, 3)
.map(t -> {
throw new Exception();
});
}
}
This is discussed in, for instance, O’Reilly’s page.
Reference
- Official documents
- Lambda expression
- Stream
- https://docs.oracle.com/javase/tutorial/collections/streams/index.html
- https://docs.oracle.com/javase/tutorial/collections/streams/reduction.html
- https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html#package.description
- https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html#Reduction
- https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html#MutableReduction
- Other materials
- Joshua Bloch. (2017). Effective Java (3rd ed.). Addison-Wesley.
- Item44: Favor the use of standard functional interfaces
- Item45: Use streams judiciously
- (Exception handling in streams) https://www.oreilly.com/content/handling-checked-exceptions-in-java-streams/
- Joshua Bloch. (2017). Effective Java (3rd ed.). Addison-Wesley.