Tutorial Lambda expression and Stream API in Java 8 Java 13.11.2016

java_lambda_expression.png

Lambda expression

Lambda expressions are introduced in Java 8 and are touted to be the biggest feature of Java 8 - a compact way of passing around behavior. Lambda expression facilitates functional programming, and simplifies the development a lot.

Lambdas allows to specify a block of code which should be executed later. If a method expects a functional interface as parameter it is possible to pass in the lambda expression instead.

A lambda expression is characterized by the following syntax

parameter -> expression body

(int a, int b) -> {return a + b}

Following are the important characteristics of a lambda expression

  • It is an anonymous function.
  • Optional type declaration. No need to declare the type of a parameter. The compiler can inference the same from the value of the parameter.
  • Optional parenthesis around parameter. No need to declare a single parameter in parenthesis. For multiple parameters, parentheses are required.
  • Optional curly braces. No need to use curly braces in expression body if the body contains a single statement.
  • Optional return keyword. The compiler automatically returns the value if the body has a single expression to return the value. Curly braces are required to indicate that expression returns a value.
  • Lambda expressions works nicely together only with functional interfaces. You cannot use lambda expressions with an interface with more than one abstract method.
An interface with exactly one abstract method is called Functional Interface. @FunctionalInterface annotation is added so that we can mark an interface as functional interface.

The Java compiler automatically identifies functional interfaces. The only requirement is that they have only one abstract method. However, is possible to capture the design intent with a @FunctionalInterface annotation.

Several default Java interfaces are functional interfaces:

  • java.lang.Runnable
  • java.util.concurrent.Callable
  • java.io.FileFilter
  • java.util.Comparator
  • java.beans.PropertyChangeListener
public class Example {
    interface MathOperation {
        int operation(int a, int b);
    }

    public static void main(String args[]){
        // different forms of notation
        MathOperation addition = (a, b) -> a + b;
        MathOperation multiplication = (int a, int b) -> { return a * b; };

        System.out.println("5 + 5 = " + addition.operation(5, 5));
        System.out.println("5 * 5 = " + multiplication.operation(5, 5));
    }
}

Following are the important points to be considered in the above example.

  • Lambda expressions are used primarily to define inline implementation of a functional interface, i.e., an interface with a single method only.
  • Lambda expression eliminates the need of anonymous class and gives a very simple yet powerful functional programming capability to Java.

We can use new feature of Java 8 method reference for referencing to static methods, instance methods, constructors using new operator. Method references help to point to methods by their names. A method reference is described using :: (double colon) symbol.

Let's look into an example of method referencing to get a more clear picture.

import java.util.List;
import java.util.ArrayList;

public class Example {
    public static void main(String args[]){
        List<String> planets = new ArrayList<String>>();
        planets.add("Mercury");
        planets.add("Venus");
        planets.add("Earth");
        planets.add("Mars");

        planets.forEach(System.out::println);
    }
}

Difference between a lambda expression and a closure

The Java programming language supports lambdas but not closures. A lambda is an anonymous function, e.g., it can be defined as parameter. Closures are code fragments or code blocks which can be used without being a method or a class. This means that a closure can access variables not defined in its parameter list and that it can also be assigned to a variable.

Let’s look at some use case examples of java lambda expression.

  • A boolean expression: (List list) -> list.isEmpty()
  • Creating objects: () -> new Movie()
  • Consuming from an object: (Movie m) -> { System.out.println(m.getYear()); }
  • Select/extract from an object: (String s) -> s.length()
  • Produce a single value by performing computation on two values: (int a, int b) -> a * b
  • Compare two objects: (Movie m1, Movie m2) -> m1.getYear().compareTo(m2.getMovie())

Stream API

Stream API is a new abstract layer introduced in Java 8. Using stream, you can process data in a declarative way similar to SQL statements. Stream API represents a sequence of elements from a source, which supports different kind of operations to perform computations upon those elements. Following are the characteristics of a Stream.

  • Sequence of elements. A stream provides a set of elements of specific type in a sequential manner. A stream gets/computes elements on demand. It never stores the elements.
  • Source. Stream takes Collections, Arrays, or I/O resources as input source.
  • Aggregate operations. Stream supports aggregate operations like filter, map, limit, reduce, find, match, and so on.
  • Pipelining. Most of the stream operations return stream itself so that their result can be pipelined. These operations are called intermediate operations and their function is to take input, process them, and return output to the target.
  • Automatic iterations. Stream operations do the iterations internally over the source elements provided, in contrast to Collections where explicit iteration is required.
java_stream.jpg
Stream<Integer> stream = Stream.of(1,2,3,4,5,6,7,8,9);
stream.forEach(p -> System.out::println);

IntStream stream = "abcde".chars();
stream.forEach(p -> System.out::println);

Collections vs Streams. Collections are in-memory data structures which hold elements within it. Each element in the collection is computed before it actually becomes a part of that collection. On the other hand Streams are fixed data structures which computes the elements on-demand basis. The Java 8 Streams can be seen as lazily constructed Collections, where the values are computed when user demands for it.

Stream operations are either intermediate or terminal. Intermediate operations return a stream so we can chain multiple intermediate operations without using semicolons. Terminal operations are either void or return a non-stream result. Methods filter, map and sorted are intermediate operations whereas forEach is a terminal operation.

Most stream operations accept some kind of lambda expression parameter, a functional interface specifying the exact behavior of the operation.

With Java 8, Collection interface has two methods to generate a Stream.

  • Method stream() returns a sequential stream considering collection as its source.
  • Method parallelStream() returns a parallel Stream considering collection as its source.
// example 1
List<String> strings = Arrays.asList("a", "", "b", "c", "f","", "g");
List<String> filtered = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.toList());

// example 2
String[] in = {"aa", "", "bbb", "ccc", "f","", "gg"};
String[] out = Arrays.stream(in).filter(s -> s.length() > 2).toArray(String[]::new);
System.out.println(out[0]);

The limit() method is used to reduce the size of the stream. The following code segment shows how to print 10 random numbers using limit().

Random random = new Random();
random.ints().limit(10).forEach(System.out::println);

The map() method is used to map each element to its corresponding result. In other words, for each item in the collection you create a new object based on that item. The following code segment prints unique squares of numbers using map().

List<Integer> numbers = Arrays.asList(7, 6, 2, 1, 4, 9, 8);
List<Integer> squares = numbers.stream().map(i -> i * i).distinct().collect(Collectors.toList());

The filter() method is used to eliminate elements based on a criteria. The following code segment prints a count of empty strings using filter().

List<String> strings = Arrays.asList("a", "", "b", "c", "f","", "g");
int count = strings.stream().filter(string -> string.isEmpty()).count();

strings.stream().filter(string -> string.startsWith("a"));

The method forEach() is used to iterate each element of the stream. The following code segment shows how to print 10 random numbers using forEach().

Random random = new Random();
random.ints().limit(10).forEach(System.out::println);

The sorted() method is used to sort the stream. The following code segment shows how to print 10 random numbers in a sorted order.

Random random = new Random();
random.ints().limit(10).sorted().forEach(System.out::println);

Collectors are used to combine the result of processing on the elements of a stream. Collectors can be used to return a list or a string.

List<String> strings = Arrays.asList("a", "", "b", "c", "f","", "g");
List<String> filtered = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.toList());
System.out.println("Filtered list: " + filtered);

String merged = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.joining(", "));
System.out.println("Merged string: " + merged);

Statistics collectors are introduced to calculate all statistics when stream processing is being done.

List<Integer> numbers = Arrays.asList(7, 6, 2, 1, 4, 9, 8);
IntSummaryStatistics stats = numbers.stream().mapToInt((x) -> x).summaryStatistics();

System.out.println("Highest number in List : " + stats.getMax());
System.out.println("Lowest number in List : " + stats.getMin());
System.out.println("Sum of all numbers : " + stats.getSum());
System.out.println("Average of all numbers : " + stats.getAverage());

Various matching operations (allMatch, anyMatch, noneMatch) can be used to check whether a certain predicate matches the stream. All of those operations are terminal and return a boolean result.

List<String> strings = Arrays.asList("a", "", "b", "c", "f","", "g");
boolean matched = strings.stream().anyMatch((s) -> s.startsWith("c"));

The reduce() method performs a reduction on the elements of the stream with the given function. The result is an Optional holding the reduced value.

The Optional class acts as a wrapper around a value that may or may not be null, and is used to reduce the frequency of NullPointerException in applications that take advantage of it.
List<String> strings = Arrays.asList("a", "", "b", "c", "f","", "g");
Optional<String> reduced = strings.stream().reduce((s1, s2) -> s1 + "-" + s2);
reduced.ifPresent(System.out::println);

The findFirst() method will return first element from stream and then will not process any more element.

List<String> strings = Arrays.asList("a", "", "b", "c", "f","", "g");
String first = strings.stream().filter((s) -> s.startsWith("b")).findFirst().get();
System.out.println(first);

Besides regular object streams Java 8 ships with special kinds of streams for working with the primitive data types int, long and double. As you might have guessed it's IntStream, LongStream and DoubleStream.

IntStreams can replace the regular for-loop utilizing IntStream.range()

IntStream.range(1, 7).forEach(System.out::println);
LongStream longStream = LongStream.rangeClosed(1, 3);

The range(int startInclusive, int endExclusive) method creates an ordered stream from first parameter to the second parameter. It increments the value of subsequent elements with the step equal to 1. The result doesn’t include the last parameter, it is just an upper bound of the sequence.

The rangeClosed(int startInclusive, int endInclusive) method does the same with only one difference – the second element is included. These two methods can be used to generate any of the three types of streams of primitives.

Useful links