waterfall in forest

Lambda and Stream

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.

Java
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.)

Java
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.

Java
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>.

Java
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>.

Java
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.

Java
// 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.

Java
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:

Java
// 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.

Java
// 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:

Java
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:

  • 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 or continue 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.
Item45: Use streams judiciously

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:

  1. Define our own functional interface whose abstract method might throw checked exceptions.
  2. 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.
  3. Invoke a stream API with parameters of that wrapper method, instead of the operation directly.
Java
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


Posted

in

Tags: