Java Streams and Lambdas

AJ
5 min readMay 6, 2017

--

Java 8 introduced Streams and Lambdas in 2014. So why am I talking about it now? Well, they’ll be introduced to Android Studio with the next release. They are already available on the Canary version if anyone is interested in trying it out. Unfortunately streams are only supported for API levels greater than 24. Lambdas will be supported for API levels greater than 9. Let’s look at what they are.

What are streams?

A sequence of elements supporting sequential and parallel aggregate operations.

Let’s break that down.

A sequence of elements : Collection of data (or a stream of data if you will).

Supporting sequential and parallel operations : We can deal with the collection sequentially (one by one) or in parallel (multiple elements will be dealt with in parallel).

Aggregate operations : We can collect elements of the stream and aggregate them into a single object.

Let’s create some streams.

There are few ways to create streams, we’ll look at a few ways here.

  1. Using a Collection : Collection interface in Java now implements a stream() method which returns the stream of that collection type. The simplest way to get a stream from a collection is to call the stream() method on it.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
Stream<Integer> numberStream = numbers.stream();

2. Stream.of(…) : The of method accepts a bunch of objects as parameters and returns a stream of those.

Stream<Integer> numberStream = Stream.of(1, 2, 3, 4, 5, 6);
Stream<Object> objectStream = Stream.of(1, 'a', 'b', "CD"); //Don't do this.

3. Primitive streams : These are special streams which are provided by Java to deal with primitives. IntStream, LongStream and DoubleStream can be created this way.

IntStream.range(1, 7) //Gives a stream of ints from 1 to 6
IntStream.rangeClosed(1, 7) //Gives a stream of ints from 1 to 7

Stream operators: Let’s look at a few stream operations.

Map : A map operation takes each element from a stream and maps it to something else. A map takes an input of type Function<T, R> where T is the input type and R is the return type. Let’s look at an example.

I want to double each element of the below list and store that in another list. Let’s solve this problem using a for loop first.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
List<Integer> doubled = new ArrayList<>();
for(int number : numbers) { //Step through each number in the list
//Double that number, add it to a new list
doubled.add(number * 2);
}
//Output : [2, 4, 6, 8, 10, 12]

There are a couple of problems with this code. We are explicitly stepping over each element. We are changing the doubled list on every iteration. We are telling the compiler not just what to do, but how to do it as well. Let’s look a solution using streams.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
List<Integer> doubled = number.stream()
.map(new Function<Integer, Integer>() {
@Override
public Integer apply(Integer t) {
//Double the input and return
return t * 2;
})
.collect(Collectors.toList());
//Output : [2, 4, 6, 8, 10, 12]

Here we just tell the compiler, what to do. We say, take each element, double it and return a list. The value of doubled is never changed in this case. This gives us the benefit of declaring it final, which means it’s immutable.

But you’re thinking, “Hey, do I have to create an inner anonymous class just to double my elements? I’m better off sticking with the tried and trusted for loop.” Let’s fix that using lambdas.

What are lambdas?

Lambdas are anonymous functions.

That definition doesn’t clarify anything. We know each function in Java has an access modifier, a return type, a name, input parameters and a body.

public int add(int a, int b) { return a + b; } //Typical Java function

Let’ look at a lambda expression.

(a, b) -> a + b

The elements to the left of the arrow are the input parameters. The stuff to the right of the arrow is the body. Where are the other parts of the function? We don’t really care about those. Since lambdas are anonymous, they don’t have a name. The return type is inferred and the scope is local to the caller.

In Java 8, any interface which has a single abstract method is called a Functional interface. And all functional interfaces can be replaced by lambdas. Look at the map function above. It takes a Function which is a functional interface. So we can replace that with a lambda.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
List<Integer> doubled = number.stream()
.map(t -> t * 2)
.collect(Collectors.toList());
//Output : [2, 4, 6, 8, 10, 12]

map is called a higher-order function (we’ll look at higher order functions in detail in the next post) as it has a function as it’s parameter. We can use lambdas anywhere we have an interface with a single method.

Let’s look at another example.

Filter only even numbers from the given list.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
numbers.stream()
.filter(number -> number % 2 == 0)
.forEach(number -> System.out.print(number));
//Output : 246

A filter takes a Predicate object which takes input of type T and returns a boolean. In our case, the input type is Integer, and we check to see if it’s even.

Look at the forEach method, the input and the parameter for the function are both same. And those instances can be replaced by method references. It is denoted by ::

forEach(number -> System.out.print(number));
forEach(System.out::print) //Both yield the same output.

Chaining operators :

The real power of streams comes from being able to chain operators. Let’s combine our above examples. Filter only even elements and double them.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
numbers.stream()
.filter(number -> number % 2 == 0)
.map(number -> number * 2)
.forEach(System.out::println);
//Output :
4
8
12

You can chain any number of operators, but a stream will only be evaluated when it reaches a terminal function. Let’s look at what are terminal functions and a couple of other things.

Lazy evaluation, intermediate functions and terminal functions.

Streams follow lazy evaluation. What it means is, the evaluation of the stream will not start until we provide a terminal function. A stream operator can be of 2 types. Intermediate operations (map, filter, flatmap etc) and terminal functions are (collect, forEach, reduce etc). What are the benefits of doing things this way? We don’t waste resources evaluating something which won’t be used. Let’s look at an example.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
number.stream()
.map(new Function<Integer, Integer>() {
@Override
public Integer apply(Integer t) {
System.out.println("Applying transformation");
return t * 2;
}
});
//Nothing gets printed here. map is an intermediate function and won't be called until we encounter a terminal function.

In our next article, let’s look at higher order function, parallel streams and more operator chaining examples.

Java Streams and Lambdas — Part 2

You can find code samples on my Github :

--

--

AJ

I’m an Android developer by profession. I like trying out different technologies in my free time.