Summary of the Oracle JDK8 MOOC

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:

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 to avoid excessive boxing/unboxing.

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:

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.

Working with streams.
Working with streams.

Sources of streams can be collections and arrays through the following methods and their overloads:

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:

The intermediate operations are usually implemented as methods of the Stream<T> interface and its primitive counterparts. The most important are:

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:

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.

comments powered by Disqus