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:
- Define a new comparator class that implements
Comparator<T> and overrides thecompare() method (good for reuse) - Define an anonymous
Comparator<T> class and overrides thecompare() method (good for single use)
In both cases, I would type a lot of words before defining the actual
public class Example {
public static void main(String[] args) {
ArrayList<Integer> nums = new ArrayList<>(Arrays.asList(1,2,3,4,10,9,8,7,5,6));
// Method 1: Instantiate Comparator class
Collections.sort(nums, new MyComparator());
System.out.println(nums); //[1,3,5,7,9,2,4,6,8,10]
// Method 2: Anonymous Comparator class
Collections.sort(nums, new Comparator<Integer>() {
@Override
public int compare(Integer i1, Integer i2) {
return i1 % 2 == 0 ? (i2 % 2 == 0 ? i1i2: 1): (i2 % 2 == 0 ? 1: i1i2);
}
});
System.out.println(nums); //[1,3,5,7,9,2,4,6,8,10]
}
}
class MyComparator implements Comparator<Integer> {
@Override
public int compare(Integer i1, Integer i2) {
return i1 % 2 == 0 ? (i2 % 2 == 0 ? i1i2: 1): (i2 % 2 == 0 ? 1: i1i2);
}
}
Now with lambda expressions, those unnecessary typings are all gone. I can define the anonymous class as such:
// Method 3: Lambda Expression
Collections.sort(nums, (a,b) -> a % 2 == 0 ? (b % 2 == 0 ? ab: 1):
(b % 2 == 0 ? 1: ab));
System.out.println(nums); //[1,3,5,7,9,2,4,6,8,10]
It's indeed a lifesaver! The parenthesis reminds me that I'm overriding the abstract function
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
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.
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.
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
A Consumer provides
A Function provides
A Supplier provides
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.
ArrayList<Integer> ages =
new ArrayList<>(Arrays.asList(21,25,40,60,54,12,23,30,29,48));
double averageAge = ages.stream()
.mapToInt(Integer::intValue)
.average()
.getAsDouble();
System.out.println(averageAge); //34.2
First, we get the stream by calling
Map<Character, String> initialMap = Arrays.stream(new String[] {
"Harry", "Jelly", "Alice", "Bob", "Leonardo DiCaprio"
}).filter((s) -> !s.contains(" "))
.collect(Collectors.toMap((s) -> s.charAt(0), (s) -> s));
System.out.println(initialMap); //{A=Alice, B=Bob, H=Harry, J=Jelly}
This example introduces two more common functions used in stream:
Finally, let's look at a special use of
String alphabet = "qwertyuiopasdfghjklzxcvbnm";
StringBuilder stringBuilder = Arrays.stream((alphabet.split(""))).sorted()
.collect(() -> new StringBuilder(),
(sb,s) -> sb.append(s),
(sb1, sb2) -> sb1.append(sb2.toString()));
System.out.println(stringBuilder.toString()); //abcdefghijklmnopqrstuvwxyz
In this case,
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.