Java 8 Streams - Tutorial & Examples

In this tutorial I describe the functionality of the Java 8 streams and show the difference to classical Java implementation by means of examples.
Java 8 Streams Tutorial

Java 8 Streams Tutorial

Preamble

Collections, lists or sets are part of the everyday work of a Java programmer. There is no software that is implemented without these data structures. The classic way to deal with these data structures are loops.

Loops unfortunately have the disadvantage that they make the code a little confusing. The programmer has to analyze line by line through the source code to understand what has been implemented.

With Java 8 the Streams API was introduced. Streams allow the programmer to write source code that is more readable and therefore more maintainable.

In this tutorial I will show you how you can better structure your source code. For this I will show you in comparison how you can solve a problem in the classical Java way and how it is solved with streams.

Requirement

  • Java knowledge

Stream vs ParallelStream

There are two types of streams in Java. On the one hand we have the classic streams and on the other hand the parallel streams. Both streams can be created from any collection, list or set. What exactly the differences are we will see in an example.

Stream

In the context of Java 8, a stream is a sequence of objects on which certain methods can be executed. Which method this is will be explained below.

The „classical“ streams in Java 8, which are created with the method .stream(). These can be created from a collection, list or set as mentioned above. The elements of the „classical“ streams are processed one after the other. Basically you can compare this with an iteration. Only with the difference that you don’t implement in the iteration step what should happen to the current element.

ParallelStream

In addition to the „classic“ streams, Java 8 also offers the ParallelStreams. As the name suggests, ParallelStreams are streams that are processed in parallel. This has the big advantage that you don’t have to take care of the multithreading yourself. The Stream API does that for us.

The following source code shows the usage of Streams & ParallelStreams.

List list = Arrays.asList("Customer 1", "Customer 2", "Customer 3", "Customer 4", "Customer 5");

list.stream().forEach(s -> System.out.println("Stream: " + s + " - " + Thread.currentThread()));

list.parallelStream().forEach(s -> System.out.println("ParallelStream: " + s + " - " + Thread.currentThread()));

Create your own streams

Of course it is also possible to create your own streams. For this the Streams API offers several possibilities.

The following source code shows how streams can be created.

Stream myStream = Stream.of("Customer 1", "Customer 2", "Customer 3", "Customer 4", "Customer 5");

myStream.forEach(s -> System.out.println("My Stream: " + s));

Java Streams Filter

Now that we know what types of streams exist in Java 8, let’s take a closer look at the Filter method. As the name suggests, the Filter method can be used to filter the objects in the stream. The return of the Lampda expression of the filter method must return a true if the object is to remain in the stream. With a false the current object is removed from the stream.

Simple filter function

To illustrate filtering in streams, we will look at two implementations. The first will be familiar to most Java programmers. This is a classic for loop with an if query.

The following source code shows how to use a for loop and an if query to select objects from a list and then return them as a list.

Customer customer1 = new Customer("1", "Albert", "Einstein");
Customer customer2 = new Customer("2", "Otto", "Hahn");
Customer customer3 = new Customer("3", "Isaac", "Newton");
Customer customer4 = new Customer("4", "Stephen", "Hawking");
Customer customer5 = new Customer("5", "Werner", "Heisenberg");

List customerList = Arrays.asList(customer1, customer2, customer3, customer4, customer5);
List resultList = new ArrayList<>();
for (Customer customer : customerList) {
    if (Integer.valueOf(customer.getId()) > 3 ) {
        resultList.add(customer);
    }
}

System.out.println("Size of ResultList: " + resultList.size());

The following source code shows the implementation with Java streams.

List resultListStream = customerList.stream()
    .filter(customer -> Integer.valueOf(customer.getId()) > 3)
    .collect(Collectors.toList());


System.out.println("Size of Stream ResultList: " + resultListStream.size());

As we can see the implementation with Java streams is more readable and clearer. As a developer it is easier to understand the source code. It is also no longer necessary to create an additional list and remember the objects you have selected. What the collect() method does we will learn in the Collectors section.

Complex Filter Functions & Predicate

In your daily work it will unfortunately not be enough to have to filter according to only one criterion. Under certain circumstances you have to filter a list according to two or more criteria. Also here the filter function of Java Streams offers us an elegant and simple possibility.

The concatenation of the filter function

Let’s take our example from above and extend it so that we now want to filter by first name and by last name.

In the classical way we would implement the code as follows.

Customer customer1 = new Customer("1", "Albert", "Einstein");
Customer customer2 = new Customer("2", "Otto", "Hahn");
Customer customer3 = new Customer("3", "Isaac", "Newton");
Customer customer4 = new Customer("4", "Stephen", "Hawking");
Customer customer5 = new Customer("5", "Werner", "Heisenberg");

List customerList = Arrays.asList(customer1, customer2, customer3, customer4, customer5);
List resultList = new ArrayList<>();
for (Customer customer : customerList) {
    if (customer.getFirstname().equals("Isaac") &&  customer.getFirstname().equals("Newton")) {
        resultList.add(customer);
    }
}

System.out.println("Size of ResultList: " + resultList.size());

Basically, the implementation is almost identical to the implementation of simple filtering. Again, we used an ArrayList to remember the objects we remembered from the if query.

We can also implement this implementation with Java Streams and the filter function simplified. For this we can call the filter function twice

The following source code shows the implementation with the concatenation of the filter function with Java streams.

List resultListStream = customerList.stream()
        .filter(customer -> customer.getFirstname().equals("Isaac"))
        .filter(customer -> customer.getLastname().equals("Newton"))
        .collect(Collectors.toList());

System.out.println("Size of Stream ResultList: " + resultListStream.size())

Here, too, we see that our source code looks much clearer and tidier.

Filtering with Predicate

It is not only possible to write more complex filter functions with the concatenation of the filter function. We have the possibility to use Predicate to create more complex filter functions. Predicate objects can be concatenated using the and(…) method. So the developer can write several predicates and chain them later. Predicate offer another advantage! Predicate are passed to the filter function as a lambda expression and can therefore be replaced at runtime. This means in plain text that depending on the requirements it is possible to use different filter criteria with one filter function.

The following source code shows the implementation with Predicate

Predicate firstnamePredicate = (Customer customer) -> customer.getFirstname().equals("Isaac");
Predicate lastnamePredicate = (Customer customer) -> customer.getLastname().equals("Newton");

List resultListStream2 = customerList.stream()
        .filter(firstnamePredicate.and(lastnamePredicate))
        .collect(Collectors.toList());

System.out.println("Size of Stream ResultList 2: " + resultListStream2.size());

At first sight the implementation with Predicate seems quite extensive and complex. Especially since it takes some time to get used to the Predicate and Functional Interfaces.

Collectors

With Java Streams you can not only filter objects from a collection, list or set, but also return the objects you filtered directly as a list, set or map.

Let’s stick to our example from above. There we saw that after filtering we always call a collect(…) function. The collect function is used to collect the objects remaining in the stream and then return them.

Collectors.toList()

First we look at the Collector for List. It collects the objects in the stream and returns them in a list.

The following source code shows the use of the Collectors.toList().

Customer customer1 = new Customer("1", "Albert", "Einstein");
Customer customer2 = new Customer("2", "Otto", "Hahn");
Customer customer3 = new Customer("3", "Isaac", "Newton");
Customer customer4 = new Customer("4", "Stephen", "Hawking");
Customer customer5 = new Customer("5", "Werner", "Heisenberg");

List customerList = Arrays.asList(customer1, customer2, customer3, customer4, customer5);

List resultList = customerList.stream()
        .filter(customer -> Integer.valueOf(customer.getId()) > 3)
        .collect(Collectors.toList());

Collectors.toSet()

Next we look at the Collector for Set. It does the same as the Collector for List. Here the return value is a set and not a list.

The following source code shows the use of the Collectors.toSet().

Set resultSet = customerList.stream()
        .filter(customer -> Integer.valueOf(customer.getId()) > 3)
        .collect(Collectors.toSet());

Collectors.toMap(

As a last example, let’s take a look at the Collector for Map. It’s a little more extensive than the previous collectors, but that doesn’t mean it’s more complicated.

We know that we always need a key and a value for a map. This is exactly what we need to specify when using the collector. In our example we will use the id of the customer object as the key and the customer object itself as the value.

The following source code shows the use of the Collectors.toMap().

Map<String, Customer> resultMap = customerList.stream()
        .filter(customer -> Integer.valueOf(customer.getId()) > 3)
        .collect(Collectors.toMap(o -> o.getId(), Function.identity()));

At the end we see the following call „Function.identity()“. This call returns the customer object. Instead of Function.identity() we could have written o -> o.
Only I find that the first spelling is much more legible and neat.

Java Streams map() method

The next stream method we will look at is the map(…) method. This serves to convert the objects in the stream into another data structure. After mapping, only the objects that have been mapped are available.

The following source code shows how to use the map method.

Customer customer1 = new Customer("1", "Albert", "Einstein");
Customer customer2 = new Customer("2", "Otto", "Hahn");
Customer customer3 = new Customer("3", "Isaac", "Newton");
Customer customer4 = new Customer("4", "Stephen", "Hawking");
Customer customer5 = new Customer("5", "Werner", "Heisenberg");

List customerList = Arrays.asList(customer1, customer2, customer3, customer4, customer5);

List firstnameList = customerList.stream()
        .map(Customer::getFirstname)
        .collect(Collectors.toList());

firstnameList.forEach(s -> System.out.println("Firstname: " + s));

As you may have noticed, there is the expression „.map(Customer::getFirstname)“. The spelling with the two colons is the simplified version of „.map(customer -> customer.getFirstname())“.

At the end the result is summarized in a list of strings and returned as already described in the section Collectors above.

Optional

Next we will have a look at the optional object. With the optional object the developer is able to use the spelling he knows from the streams.

Imagine the find method of a class returns a customer object if it is found. If the search query is not successful, a zero is returned. In almost 100% of classic implementations, the query whether a result was found would look like this.

The following source code shows the classic implementation.

Customer foundCustomer = customerRepository.findById("someId");
if (foundCustomer == null) {
    throw new RuntimeException("Customer not found");
}

//Do something with the customer object

Empty

The result of an optional object can be empty. In this case, there is no customer object with which you can continue working. Here you have to decide as a developer how to implement further.

Optional.ifPresent

If an object is returned in the optional object, you can access it in the ifPresent merhode. This is equivalent to a classic if (foundCustomer != null).

The following source code shows how to use the ifPresent method.

customerRepository.findByIdReturnEmptyOptional("someId")
        .ifPresent(customer -> System.out.println("Customer is found"));

Optional.orElse

After returning an optional object you can use the orElse method to return the customer object which is in the optional object or the object which was passed to the orElse method.

The following source code shows how to use the orElse method implementation.

Customer defaultCustomer = new Customer("", "", "");
Customer customerOfElse = customerRepository.findByIdOptionalImplementation("someId")
        .orElse(defaultCustomer);

Optional.orElseGet

The optional object also offers the orElseGet method. Many will now ask themselves what is the difference between orElse and orElseGet. Both return a customer object in our case. Both methods first return the object in the optional and if not the object passed in the orElse or orElseGet.

The difference is that orElseGet only executes the code block that was passed when the optional object is empty. You can compare this with the lazy loading principle.

The following source code shows how to use the orElseGet method implementation.

Customer customerOfElseGet = customerRepository.findByIdOptionalImplementation("someId")
        .orElseGet(() -> new Customer("", "", ""));

Optional.orElseThrow

If you want to throw an exception in case the optional object does not return a value, it is best to use the orElseThrow method.

The following source code shows how to use the orElseThrow method implementation.

Customer cutomerOrElseThrow = customerRepository.findByIdReturnEmptyOptional("someId")
        .orElseThrow(() -> new RuntimeException("Customer not found"));

The conclusion

In this tutorial we have seen some examples how the Java 8 Streams API can be used. We also saw that you can write your source code more readable with Java Streams. In the beginning the spelling is strange to a classical Java developer. With time you will also learn to love Java streams.

Verwandte Beiträge

Comments (1)

This is so help full

Leave a comment