Java 8 Part 1 – Lambdas, Streams, and Functional Interfaces

Author

Java 8 is tentatively scheduled to be released in March 2014. Several exciting new features are being developed as well as a few fixes. This post is part of a series of Java posts that will introduce various features and changes in Java SE 8. This post will cover default methods, functional interfaces, lamda expressions, and streams.

Default Methods

Normally, interface methods are declared with the abstract keyword which requires an implementing or extending class to define a behavior for the method. Unfortunately, this requirement severely restricts existing interfaces such as Collection without breaking existing implementations. In order to extend existing libraries without compromising backwards compatibility, Java 8 is implementing default methods, also referred to as virtual extension methods and defender methods, which will utilize the default keyword. For example:

public interface Collections {
    public default void forEach(Consumer<? super T> consumber) {
        for (T t : this) {
             consumer.accept(t);
        }
    }
}

In the case of a class implementing multiple interfaces with the same default method signature, the compiler will throw an error to prevent the diamond problem.

The use of default methods on a regular basis should be discouraged. While it can be useful to extend existing APIs, in practice interfaces should constitute a contract, not behavior.

Functional Interfaces

A functional interface is an interface that contains a single abstract method. As with regular interfaces, functional interfaces are allowed to have any number of default methods. Several existing interfaces already follow this pattern: Callable, ActionListener, and Runnable.

The annotation @FunctionalInterface is being introduced. This annotation acts similarly to @Override by signaling to the compiler that the interface is intended to be a functional interface. The compiler will then throw an error if the interface has multiple abstract methods.

@FunctionalInterface
public interface myFuncInterface{
    abstract int work(int x, int y);
}

Java 8 will include a set of useful functional interfaces in the java.util.function package. Several important functional interfaces to note will be the Predicate, Supplier, and Consumer. Although functional interfaces alone do not add functionality, they will primarily be used by lambda expressions.

Lambda Expressions

Lambda expressions are one of the more exciting features in Java 8. Lambda expressions are anonymous methods which are intended to replace the bulkiness of anonymous inner classes with a much more compact mechanism.

Lambda expressions can come in several forms:

(int x, int y) -> {return x + y;}
(radians) -> 3.14159 * radians

The first expression simply returns the addition of two integers. In this case, the return type is inferred to be an integer. The second expression omits the optional “return” keyword, parameter types, and brackets for one-liners. Brackets have be used if the lambda expression spans multiple lines. Method references may even be passed in the body of the expression:

//These statements will behave the same.
(s) -> System::println;
(s) -> System.out.println(s);

At compile time, lambda expressions and method references are converted to functional interfaces. The type of the functional interface is inferred from the context and is known as the target type.

Here is an example of how lambda expressions can reduce the bulkiness of anonymous inner classes:

new Thread(new Runnable() {
        public void run() {
            System.out.println("Hello World!");
        }
    }).start();
 
new Thread(() -> System.out.println("Hello World!")).start();

Lambda expressions can be used anywhere where a functional interface is expected (e.g. Runnable, Predicate).

Streams

Traditionally, processing an array or collection required an iterator or manual loop. In Java 8 the Stream class will be able to more succinctly process an array or collection. Streams will also be able to utilize both lambda expressions and functional interfaces.

There are two “modes” for a stream: sequential and parallel. In sequential mode each item in the stream is read and processed, and then the next item is read. In parallel mode the data is split into multiple segments using a Spliterator. Each of the segments is then processed individually on possibly different threads.

//Sequential
int sum = intList.stream().reduce(0, Integer::sum);
//Parrelel
int sum = intList.parallelStream().reduce(0, Integer::sum);

The Stream class contains two types of methods: intermediary and terminal. Intermediary methods in the Stream API return a stream and are always lazy methods. These intermediary methods, such as filter or map, are then combined to create a stream pipeline. A terminal method, such as collect or reduce, ends the pipeline by causing the stream to be “consumed.” Once the stream has been traversed or consumed, it cannot be used again.

Streams offer two major benefits. The first benefit is succinct code. Using an iterator to filter a list:

ArrayList<Person> filteredPeople = new ArrayList<Person>();
Iterator<Person> iter = people.iterator();
while (iter.hasNext()){
    Person p = iter.next();
   if(p.getAge() > 21 && p.getWeight() > 100 && "Chris".equals(p.getName())){
        filteredPeople.add(p);
    }
}

Using a parallel stream to filter the same list:

List<Person> filterPeople = people.parallelStream()
    .filter((Person p) -> p.getAge() > 21 && p.getWeight() > 100 && "Chris".equals(p.getName()))
    .collect(Collectors.toList());

The stream is able to reduce the amount of lines through chaining function calls together. Although this chaining mechanism does save space, long chains should be avoided if possible because they can make code harder to read and maintain.

The second major benefit is improved performance. The following chart displays the runtimes of an example case to filter an Arraylist using an iterator, a parallel stream, and a sequential stream on a quad-core laptop:

 java

In this test case, the parallel stream started to show a performance improvement above 2,000 elements and continued to perform significantly faster with even greater sizes. The sequential stream and iterator showed similar results at all data points. Parallel streams should not be used for small lists because the overhead of parallelizing the computation dwarfs the performance gains.

What’s the Impact?

Java 8 is adding some exciting features, some of which need to be used carefully. Default methods, for example, should be used sparingly. While they have their uses, specifically in extending APIs after initial publication, default methods alter the idea that interfaces define a contract, not behavior. Functional interfaces should also be used only where appropriate.

Streams can offer a measurable benefit compared to other methods of looping through data. As shown, parallel streams used on large data sets can show significantly improved results. Streams also benefit from the use of lambda expressions and functional interfaces. While streams will have their pitfalls, their performance improvements can be quite impressive.

The code for my simple test cases is available here. I encourage you to try it, and see if you get similar results.

This post is part one of a series covering Java 8:

- Java 8 Part 1 – Lamdas, Streams, and Functional Interfaces
- Java 8 Part 2 – Nashorn
- Java 8 Part 3 – HashMap and java.time
- Java 8 Part 4 – Concurrency, TLS SNI, and PermGen

  • Matt Levy

    thanks for the great post Chris!