Introduction
In a previous post, I summarised the new functional features of Java 8. Oracle has recently released a 3-week JDK 8 Massive Open and Online Course called “Lambdas and Streams”, which discusses the new JDK8 functional features in much more details. It’s a great course and if you’re interested in Java and new JDK8 it can help you get started. This article summarises the course and my previous post and can be used as a quick ref-card.
Lecture 1: Lambda Expressions
Lambda Syntax
To understand lambdas, first we need to understand functional interfaces. In Java 8, an interface is called functional, if it has only one abstract method. In previous Java versions, all interface methods were abstract. In Java 8 interfaces can have default and static methods as well. In order for an interface to be function it should have exactly one abstract method regardless if any default or static methods are defined. It is recommended to annotate functional interfaces with the @FunctionalInterface annotation so that the compiler can check if they meet the requirement.
In Java, a lambda function can be thought of as a syntactic short-cut for defining anonymous instances of functional interfaces. Lambda expressions are defined with the following pattern:
1
(param1, param2, ... , paramN) -> expression | block of code
If the parameter list has only one parameter, the brackets can be omitted. Note that there are no types in the lambda definitions, as types are automatically inferred from the functional interface definition. Types can be defined explicitly, but this is not required. The body of the lambda expressions can be either a single expression (whose evaluation is the result of the function call) or a block of code, similar to a method definition. The following examples demonstrate functionally equivalent definitions:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Comparator <Integer> cmp1 = (x, y) -> x.compareTo(y); // Expression body. Types NOT specified
Comparator <Integer> cmp2 = (Integer x, Integer y) -> { // Block of code body. Types specified
return x.compareTo(y);
};
Comparator <Integer> cmp3 = new Comparator<Integer>() { // Java 7 style
public int compare(Integer x, Integer y) {
return x.compareTo(y);
}
};
Runnable r1 = () -> System.out.println("Test"); // Expression body
Runnable r2 = () -> { System.out.println("Test"); }; // Block of code body
Runnable r3 = new Runnable() { // Java 7 style
public void run() {
System.out.println("Test");
}
};
Alike anonymous classes, lambdas can use the variables of their environment. When using local variables in lambdas, they must be effectively final, which means they are not assigned to from anywhere, regardless if they are actually marked as final.
Method References
Lambdas are a neat way to implement functional interfaces, but often all they do is call an existing method. To make things simpler Java 8 introduces method references. Method references are a shorthand for implementing functional interfaces by calling already defined methods.
There are four types of method references:
Reference to | Syntax | Lambda Equivalent |
---|---|---|
Static method | Class::staticMethod | (param1, ... paramN) -> Class.staticMethod(param1, ... paramN) |
Specific instance's method | var::instanceMethod | (param1, ... paramN) -> var.instanceMethod(param1, ... paramN) |
Instance method | Class::instanceMethod | (var, param1, ... paramN) -> var.instanceMethod(param1, ... paramN) |
Constructor | Class::new | (param1, ... paramN) -> new Class(param1, ... paramN) |
The following examples demonstrate equivalent definitions with method references and lambdas:
1
2
3
4
5
6
7
8
9
10
11
12
Predicate <String> p1 = Boolean::getBoolean; // Static method reference
Predicate <String> p2 = s -> Boolean.getBoolean(s); // Equivalent lambda
String var = "TestEquality";
Predicate <String> p1 = var::equals; // Specific instance's method reference
Predicate <String> p2 = s -> var.equals(s); // Equivalent lambda
Predicate <String> p1 = String::isEmpty; // Instance method reference
Predicate <String> p2 = s -> s.isEmpty(); // Equivalent lambda
Predicate <String> p1 = Boolean::new; // Constructor reference
Predicate <String> p2 = s -> new Boolean(s); // Equivalent lambda
Functional Interfaces in the standard library
The standard library has always had a bunch of functional interfaces – Runnable, Callable, Comparator etc.
Java 8 introduces a whole new package of functional interfaces called
java.util.function.
In Java there are two kinds of types – referential and primitives.
Java generics can only be used with
referential types – e.g. List<int>
is invalid.
Thus, the java.util.function
package contains
multiple versions of each interface – a generic version for referential types, and specialised versions
for the primitives. For example we’ve got
Consumer<T> and
IntConsumer.
In the rest of the article we’ll look at the generic interface only.
The main functional interfaces in the package are:
- Consumer<T> - takes an argument of type T returns void;
- BiConsumer<T,U> - a consumer with 2 arguments;
- Supplier<T> - takes no argument and returns T;
- Function<T,R> - takes an argument of type T and returns R;
- BiFunction<T,U,R> - a function of two arguments;
- UnaryOperator<T> - shorthand for Function<T,T>;
- BinaryOperator<T> - shorthand for BiFunction<T,T>;
- Predicate<T> - given an argument of type T returns a boolean;
- BiPredicate<T,U> - given arguments of types T and U, returns a boolean.
The consumer interfaces in the above list have a default method andThen, which takes as an argument another consumer. The result is a new consumer instance, which runs the two consumers one after the other, as in the example:
1
2
3
4
5
6
7
8
9
//Define two consumers
Consumer <String> helloConsumer = name -> System.out.println("Hello, " + name);
Consumer <String> byeConsumer = name -> System.out.println("Bye, " + name);
//Create and call a composite consumer which “chains” the previous two
Consumer <String> helloByeConsumer = helloConsumer.andThen(byeConsumer);
// Prints "Hello, Guest\nBye, Guest"
helloByeConsumer.accept("Guest");
The andThen
default method of the Function
interfaces works similarly – it chains the function invocation as
in f(g(x))
. The compose
method does the same, but swaps the functions –
i.e. g(f(x))
instead of f(g(x))
. The following example demonstrates this:
1
2
3
4
5
6
7
8
9
10
UnaryOperator <Integer> plus1 = x -> x + 1;
UnaryOperator <Integer> mult2 = x -> x * 2;
//Create a new function with andThen
Function <Integer, Integer> plus1Mult2 = plus1.andThen(mult2);
System.out.println(plus1Mult2.apply(1));// Prints 4
//Create a new function with compose
Function <Integer, Integer> mult2Plus1 = plus1.compose(mult2);
System.out.println(mult2Plus1.apply(1)); // Prints 3
Note: in the above example you may wish to use
IntUnaryOperator instead of UnaryOperator
Similarly, the predicate interfaces have default methods and
, or
, and negate
, which
can be used to create new predicates with combined logic.
New Methods in JDK 8
The Java 8 standard collections library introduces several new methods which use functional interfaces:
-
Iterable.forEach(Consumer c) – iterates and applies the consumer to each element;
-
Collection.removeIf(Predicate p) – removes all elements for which the predicate returns true;
-
List.replaceAll(UnaryOperator o) – replaces all elements with the result of the argument operator;
-
List.sort(Comparator c) - replaces Collections.sort.
Also, many methods of the standard Logger
have been overloaded to take as an argument a Supplier<String> instance, instead of string. For example,
when the Logger.fine(Supplier<String>)
method is invoked it will only call the provided supplier if the logging level is below or equal to Fine
. Otherwise, the supplier will not be called leading to fewer operations like string concatenation and formatting.
Lecture 2: The Streams API
Streams
JDK 8 defines a stream as a sequence of elements. Streams of referential elements inherit from the Stream<T> interface, and there are specialised interfaces for streams of primitives. Once again we’ll only consider the generic types, as the primitive streams offer analogous functionalities.
Streams are usually used in a pipeline of operations, where each operation produces or transforms a stream until a result is derived. Hence, each stream operation can be can be classified as:
-
Stream source – generates a stream;
-
Inermediate operation – processes a stream;
-
Terminal operation – given a stream, yields the final result.
Sources of streams can be collections and arrays through the following methods and their overloads:
-
Collection.stream() - converts the collection to a stream;
-
Collection.parallelStream() - converts to a stream, which can be subjected to parallel processing;
-
Arrays.stream(T[]) - utility method converting the provided array to a stream;
Other typical sources of streams are random number generators (see Random), lists of file paths (see Files) and list of lines in a text (see BufferedReader).
The generic Stream<T> interface and its primitive counterparts provide several utility source functions for creating or transforming streams:
-
Stream.concat(Stream, Stream) - returns the concatenation of the two streams.
-
Stream.empty() - creates an empty stream;
-
Stream.of(T… values) - a new stream with the specified values;
-
IntStream.range(int, int), IntStream.rangeClosed(int, int) – a new integer stream representing a range.
The intermediate operations are usually implemented as methods of the Stream<T> interface and its primitive counterparts. The most important are:
-
Stream.distinct() - returns a stream with the unique elements.
-
Stream.filter(Predicate p) - returns a stream with the elements for which the predicate returns true.
-
Stream.map(Function f) - returns a stream with the result of the function application on each element.
-
Stream.flatMap(Function f) – if the return type of the function is another stream, then
map(f)
will return a stream of streams, which may not be convenient. TheflatMap
method resolves this problem by flattening/combining the results into a single result stream. -
Stream.sorted(Comparator c) – returns a sorted stream.
-
Stream.skip(long n) – returns a new stream, which does not contain the first n elements.
-
Stream.limit(long n) – returns a new stream, which contains the first n elements.
The methods mapToInt, mapToDouble, and mapToLong are handy when converting/mapping a stream of objects to a stream of primitives.
The terminal operations are special, as they cause the pipeline to be evaluated. Prior a terminal operation, the stream operations are only “accumulated” or delayed. This allows the terminal operation to execute effectively (e.g. in parallel) on the aggregated stream.
Most often you would probably use one of the following terminal operations:
-
Stream.collect(Collector c) - collects all elements - e.g. in a list or concatenates them in a string.
-
Stream.toArray() - converts the stream to array.
-
Stream.reduce(BinaryOperator accumulator) – consequently applies the accummulator over all elements until it yields a single value. Example: multiply all integers.
Other frequently used terminal methods are count
, max
, and min,
which return
Optional instances.
Optionals
In Java 8, Optional<T>
is a container which holds a reference to an object or null
. It is used to avoid excessive null checks throughout the code. It can also be thought of as a stream which has either 0 or 1 elements.
In that sense, it is a generalisation of the rule that you should prefer to return empty
collections and arrays from methods, rather than null.
The count, max and min methods from the previous section all return optionals, whose embedded value is null if the stream is empty.
Instead of doing a null
check, you can use the ifPresent(Consumer c)
method to run code if the optional contains a value:
1
2
3
4
Optional <String> opt = …
// Will print only if opt refers to a non-null value
opt.ifPresent(System.out::println);
You can also use the filter
and map
methods, as you would do with streams.
Lecture 3: More Advanced Lambdas and Stream Concepts
The Stream.reduce(BinaryOperator)
method is a terminal operation which aggregates the stream to a single value.
The provided operator is applied to null
and the first element, then its result and the
second element are fed in the operator and so on … until a single value is achieved. There is no guarantee
as to the actual sequence of execution, but the result is always as if the aforementioned procedure is executed.
This can be used to implement things like computing the min, max or sum of all elements. In fact, streams
already have predefined short-cut methods for these functionalities. The reduction mechanism is a
generalisation of this concept, and the reduce method has overloads providing additional parameters.
Given an infinite stream, the
Stream.findFirst and
Stream.findAny methods are
typically used to terminate it and return a value matching the specified predicate wrapped in an Optional.
The findFirst
method returns the first match, while findAny
can return any match thus allowing
for parallelism behind the scenes.
1
2
3
4
5
6
// Find a random positive and even integer
int r = Random.ints().findFirst(i -> i > 0 && i % 2 == 0);
int[] a = new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// Can return either 2, 4, 6, 8, or 10
int i = Arrays.stream(a).findAny(i -> i % 2 == 0);
Another terminal operation is Stream.collect(Collector) which converts the stream to a container – usually this is a collection or a string. The collector parameter defines how the stream elements will be converted to the resulting container. The Collectors class defines utility methods for instantiating and combining common collectors.
1
2
3
4
5
6
7
8
9
10
11
Stream <Integer> s = Arrays.stream (new Integer[] {1, 2, 3, 4});
// To list and set
List <Integer> lst = s.collect(Collectors.toList());
Set <Integer> set = s.collect(Collectors.toSet());
// Map of same keys and values
Map <Integer, Integer> map = s.collect(Collectors.map(Function.identity(), Function.identity()));
// A string of all values with a separator
String toString = s.collect(Collectors.joining(","));
Streams can be serial or parallel. All operations on a sequential stream are run in sequence on a single thread. Operations on a parallel streams are run in a thread pool using the Fork Join framework. The size of the thread pool is determined by the number of available processors, but could be overridden by setting a system property.
Parallel streams can be created directly from collections through the parallelStream
method.
Every stream can be made parallel or sequential with the respective stream methods.