Introduction

Java is a programming language used commonly throughout the world of software development. As of 2013, over 3 billion devices used Java, with the language being used primarily in web applications and Android applications.

Nonetheless, people complain about the language being more verbose and syntactically demanding than its peers (such as Ruby and Python.) Some even say it is an outdated language.

Luckily, Java 8 brought many refreshing changes designed to mold Java into something more simple and modern. Better yet, at the time of writing, version 9 is coming with more changes soon.

One of the key changes is the Stream interface which relies on a new Java component, lambda expressions.

This guide introduces lambda expressions and the Stream interface and highlights the most common Stream operations on collections.

In part two, you'll learn about more advanced methods (like reducing and collecting) and parallel streams.

What is a Lambda Expression?

Let's begin by answering the question, what are lambda expressions in the context of java?

Lambda expressions make code more functional and less object-oriented, thus shortening its length. How about an example?

The signature of the abstract method of a functional interface provides the signature of a lambda expression (this signature is called a functional descriptor).

This means that to use a lambda expression, you first need a functional interface, which is just a fancy name for an interface with one method. For example:

1
2
3
interfaceSearchable{booleantest(Car car);}

java

In fact, lambda expressions don't contain the information about which functional interface they are implementing. The type of the expression is deduced from the context in which the lambda is used. This type is called the target type.

So lambda expressions are an alternative to anonymous classes, but they are not the same.

They have some similarities:

Local variables (variables or parameters defined in a method) can only be used if they are declared final or are effectively final.

You can access instance or static variables of the enclosing class.

They must not throw more exceptions than specified in the throws clause of the functional interface method. Only the same type or a supertype.

Some significant differences between lambdas and anonymous classes:

For an anonymous class, the this keyword resolves to the anonymous class itself. For a lambda expression, this resolves to the enclosing class where the lambda is written.

Default methods of a functional interface cannot be accessed from within lambda expressions. Anonymous classes can.

Anonymous classes are compiled into inner classes, while lambda expressions are converted into private, static (in some cases) methods within their enclosing class. Using the invokedynamic instruction (added in Java 7), they are bound dynamically. Simply put, since there's no need to load another class, lambda expressions are more efficient than anonymous classes.

With this in mind, let's introduce the Stream interface.

What is a Stream?

First of all, streams are not collections.

A simple definition is that streams are wrappers for collections and arrays. They wrap an existing collection (or another data source) to support operations expressed with lambdas, so you specify what you want to do, not how to do it.

Characteristics of streams

Streams work perfectly with lambdas.

Streams don't store their elements.

Streams are immutable.

Streams are not reusable.

Streams don't support indexed access to their elements.

Streams are easily parallelizable.

Stream operations are lazy when possible.

One thing that allows this laziness is the way their operations are designed. Most of them return a new stream, allowing operations to be chained and form a pipeline that enables this kind of optimizations.

To set up this pipeline you:

Create the stream.

Apply zero or more intermediate operations to transform the initial stream into new streams.

Apply a terminal operation to generate a result or a side-effect.

Creating Streams

A stream is represented by the java.util.stream.Stream<T> interface. This works with objects only.

There are also specializations to work with primitive types, such as IntStream, LongStream, and DoubleStream. Also, there are many ways to create a stream. Let's see the three most popular.

The first one is creating a stream from a java.util.Collection implementation using the stream() method:

Also, you can't use return, break or continue to terminate an iteration either. break and continue will generate a compilation error since they cannot be used outside of a loop and return doesn't make sense when we see that the foreach method is implemented basically as:

An important thing to consider is that all of these operations use something similar to the short-circuiting of && and || operators.

Short-circuiting means that the evaluation stops once a result is found. Thus find* operations stop at the first found element.

With *Match operations, however, why would you evaluate all the elements of a stream when, by evaluating the third element (for example), you can know if all or none (again for example) of the elements will match?

Sorting a Stream

Sorting a stream is simple.

1
Stream<T>sorted()

java

The method above returns a stream with the elements sorted according to their natural order. For example:

min() returns the minimum value in the stream wrapped in an Optional or an empty one if the stream is empty.

max() returns the maximum value in the stream wrapped in an Optional or an empty one if the stream is empty.

When we talk about primitives, it is easy to know which the minimum or maximum value is. But when we are talking about objects (of any kind), Java needs to know how to compare them to know which one is the maximum and the minimum. That's why the Stream interface needs a Comparator for max() and min():