Java Stream and Lambda Expressions

Feb. 26, 2020, 00:30:14

Welcome back! It's almost a month since my last post. I've been quite busy the past few weeks working on my philosophy paper and several midterms. Finally, I can take a break from my coursework and find some interesting things to learn.

Java Stream & Lambda Expressions - two new features introduced in Java 8 which I'm not totally sure whether I really know them or not - look like a good starting point for me.

Disclaimer: This is only a study note, not a techincal article. If you find errors in this note, please post them as Issues.

Lambda Expressions

I've used lambda expressions before in my JavaFX projects, mainly to save some time typing anonymous event listeners. Now after I learned more on the subject, I find my initial feeling is quite accurate in defining the use of lambda expressions - to save time.

Let's take comparators for example. Previously if I want to customize a comparator in Java, I would do so in one of the following two ways:

  1. Define a new comparator class that implements Comparator<T> and overrides the compare() method (good for reuse)
  2. Define an anonymous Comparator<T> class and overrides the compare() method (good for single use)

In both cases, I would type a lot of words before defining the actual compare() method.

Now with lambda expressions, those unnecessary typings are all gone. I can define the anonymous class as such:

It's indeed a lifesaver! The parenthesis reminds me that I'm overriding the abstract function compare(T a, T b) in the Comparator<T> interface. The arrow indicates that I'm defining the function body as follows. In short, lambda expressions let me override methods without the class framework.

As expected, there are some limitations of lambda expressions. To use them, there is a major constraint on the type of interface that is to be implemented, namely the interface must contain one and only one abstract method. Such interface is called a functional interface.

The reason behind, as I understand is quite simple. Since we don't have the traditional framework for defining an anonymous class, there's no way for one lambda expression to implement several abstract methods at the same time. To avoid making mistakes like using lambda expressions on a non-functional interface, it's a good practice to annotate with the @FunctionalInterface tag when using lambda expressions to get some help from the compiler.

Java Stream

Now let's switch to talk about Java Stream, which works closely with lambda expressions in improving code readibilities.

The name of Stream is pretty explainatory. It's a stream of actions performed on an object and it's especially useful when analyzing or manipulating data.

Or it can be understood as performing operations on a stream of data.

Before moving on to examples, let's first understand two important concepts in Java Stream:

1. Intermediate & Terminal Operations

An intermediate operation is used to manipulate data in certain ways and return the result. Exemplary uses would be to filter out value greater than 100 from a list of values, get the id of each Employee object in a list or sort a list of numbers in increasing order. filter() and map() are examples of intermediate operations.
A terminal operation is often used to summarize data or pack processed data to a container. Exemplary uses would be to find the sum or average of a list of numbers or to create a map between employee id and employee salary from a list of Employee objects. collect() and count() are examples of terminal operations.

2. Predicates, Consumers, Functions and Suppliers

These functional interfaces are also introduced in Java 8 and they take advantage of the use of lambda expressions in streams.
A Predicate provides boolean test(T t) method for testing certain properties of an object.
A Consumer provides void accept(T t) method for performing actions on an object.
A Function provides R apply(T t) method for manipulating an object and returning a potentially different result.
A Supplier provides T get() method for feeding data into the stream.

Now with these two concepts ready, let's dive into some examples. The first example shows how to calculate the average of a list of integers with stream.

First, we get the stream by calling stream() on the list of ages. Then we explicitly convert a stream of integers into an IntStream through the mapToInt function. With an IntStream, we can easily calculate the average of ages and return the result as a double.




This example introduces two more common functions used in stream: filter() and collect().

filter() is used to select only those elements that satisify a certain condition. Here we use a Predicate as the condition checker, and we implement it with a lambda expression (remeber what we talked about before?).

collect() is used to pack data to a container. In this case, we'd like to pack our data into a map. We specified two Function to generate the key and value pair in the map - the key is generated by taking the first character of the given name and value is the name itself. The two Function are implemented using lambda expressions.




Finally, let's look at a special use of collect(). (This example is adopted from stream package summary. See reference below.)

In this case, collect() requires 3 arguments and, instead of packing data to a collection, it packs processed data into a StringBuilder. The first argument is a supplier, which returns a new StringBuilder as a container for strings. Then for each string, it is appended to that StringBuilder by an accumulator as defined by the second argument. Finally, to combine two StringBuilders, we need to define a combiner that concatenate them together, as defined by the last argument. This way of packing data is called mutable reduction, because all data is added to a single mutable container (in this case a StringBuilder).

Conclusion

Using Streams and Lambda Expressions can greatly save programmers time typing. They are also concise and easier to understand. Overall, they are handy tools to have in one's toolkit.

Useful Links/References:

Stream Intermediate Operations
Stream Package Summary