The power of Streams in JAVA 8

HACENE KASDI
6 min readMar 22, 2020

--

In this guide, we will study the power of Streams and introduce some situations that a developer can face daily. The Streams API provides a functional approach to process collections of objects.

We have to distinguish the processing of objects and processing streams of bytes using both InputStream and OutputStream API.

We consider that you have knowledge in functional programming and familiar with lambdas expressions of JAVA 8.

#Steam.map() #Steam.reduce() #Steam.filter() #Steam.flatMap() #Steam.min() #Steam.max() #LambdaExpression

Introduction

Streams can operate in two ways, intermediate or terminal. Intermediate operations return a type Stream so we can chain multiple intermediate operations either to return new Stream without using semicolons, in this optic we can find Stream.filter, Stream.flatmap, Stream.map…, or applying terminal operations that are either void, Stream.foreach as an example, or return a non-stream result. For more example take a look at Stream API javadoc.

Most stream operations accept some kind of lambda expression parameter, a functional interface specifying the exact behaviour of the operation.

The underlying data of my collection will not be modified when applying a lambda expression, these types of functions are called non-interfering functions.

Stream operations use stateless functions for processing data, no lambda expression depends on variables or mutable states of the external scope that might change during the execution of the pipeline stream.

Let’s take some examples.

Stream.map & Stream.reduce

Map

Stream.map(), This method converts the object provided by the collection into another object, of the same type as the source type or into another type. eg, a List of type Person and for each person, we will convert the name attribute of the class into UPPERCASE.

Class Person snippet :

import java.util.Arrays;
import java.util.List;
public class Person { private String name;
private int age;
private Profession profession;
public Person(String name, int age, Profession profession) {
this.name = name;
this.age = age;
this.profession = profession;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public Profession getProfession() {
return profession;
}
public static List<Person> init() {
return Arrays.asList(
new Person("Arnold", 22, Profession.SOFTWARE_ENGINEER),
new Person("Anna", 42, Profession.ARCHITECT),
new Person("Luise", 32, Profession.NETWORK_ADMINISTRATOR),
new Person("Léa", 27, Profession.MANAGER),
new Person("Yun", 56, Profession.SOFTWARE_ENGINEER)
);
}
@Override
public String toString() {
return '{' +
"name= " + name + " | profession= " + profession +
'}';
}
}

In the snippet below we will create a function using a lambda expression that takes an object of type Person then extracts the name and will convert each string name to uppercase.

List<Person> persons = Person.init();//["Arnold", "Anna", "Luise"] ==> ["ARNOLD", "ANNA", "LUISE"]List<String> names= persons
.stream()
.limit(3) // Take only three objects
.map(person -> person.getName().toUpperCase())
.collect(Collectors.toList());

Reduce

Let’s introduce the function reduce(). Stream reduce() performs a reduction on the elements of the stream. It uses identity and accumulator function for reduction.

In the example below, we will pass BinaryOperator as an accumulator. In the case of numeric BinaryOperator, the start value will be 0. In the case of the string, the start value will be a blank string.

reduce(BinaryOperator accumulator)

// [{A}{B}{C}{D}{E}] --> {A B C D E}List<String> tokens = 
Arrays.asList("I", "am", "a", "software", "craftsman.");
tokens
.stream()
.reduce((string1, string2) -> string1 + " " + string2)
.ifPresent(System.out::println);
// Result ==> I am a software craftsman.

Stream.filter

Stream filter(Predicate) used to filter out elements from a Java stream and returns new stream contains elements that match its predicate. If the element is to be included in the resulting Stream, the Predicate should return true. If the element should not be included, the Predicate should return false.

In the snippet below, I added one method in class Person to check if the profession given in the argument of the method equals to the profession of the current object (Person).

public boolean isSameProfession(Profession profession){
return profession.equals(this.profession);
}

Filter function snippet.

List<Person> softwareEngineers = persons
.stream()
.filter(person ->person.isSameProfession(SOFTWARE_ENGINEER))
.collect(Collectors.toList());
// Result : 2 objects of type Person, Arnold & Yun.

FlatMap

The flatMap() method first flattens the input Stream of Streams to a Stream of Objects, in the example below we will take streams of strings with a different length then we apply a function flatMap with lambda Collection::Stream, the aim is to convert a list of lists to flatten input, (for more about flattening, see the article).

// List<List<T>> --> List<T>

List<String> stringWithLength3 = Arrays.asList("BBB", "DDD", "AAA");
List<String> stringWithLength2 = Arrays.asList("EE", "FF", "CC");
List<List<String>> stringsByLength = Arrays.asList(stringWithLength3, stringWithLength2);

List<String> allStrings = stringsByLength
.stream()
.flatMap(Collection::stream)
.collect(Collectors.toList());
// Result : [BBB, DDD, AAA, EE, FF, CC]

Collectors.groupingBy(T.e)

The class Collectors implements various useful reduction operations, Java 8 Collectors class provides a static groupingBy method to group objects by some property and store the results in a Map instance. I will illustrate one example for grouping persons by profession.

// List<T> --> Map<T.e, List<T>>Map<Profession, List<Person>> personsByProfession = persons
.stream()
.collect(Collectors.groupingBy(Person::getProfession));
// Result : {
ARCHITECT=[{name= Phillipe | profession= ARCHITECT}], SOFTWARE_ENGINEER=[{name= Arnold ...}, {name= Yun ..}],
MANAGER=[{name= Léa | profession= MANAGER}],
NETWORK_ADMINISTRATOR=[{name= Luise | profession= NETWORK_ADMINISTRATOR}]
}

Grouping persons by profession and map it to counts.

// List<T> --> Map<T.e, Long>Map<Profession, Long> countByProfession = persons
.stream()
.collect(
Collectors.groupingBy(Person::getProfession,Collectors.counting()));
// Result : {
ARCHITECT=1,
SOFTWARE_ENGINEER=2,
MANAGER=1,
NETWORK_ADMINISTRATOR=1
}

Stream.min() & Stream.max()

When we seek the maximum or minimum from a stream of comparable objects we can use comparator.comparing().

min() method on the stream used to get the minimum value by passing a lambda function as a comparator, this is used to decide the sorting logic for deciding the minimum value.

String max = Stream.of("A", "B", "C", "D","B", "G", "E", "K", "N")
.min(Comparator.comparing(String::toString))
.orElse(null);
// Result : A

max() method on the stream used to get the maximum value by passing a lambda function as a comparator, this is used to decide the sorting logic for deciding the maximum value.

String max = Stream.of("A", "B", "C", "D","B", "G", "E", "K", "N")
.max(Comparator.comparing(String::toString))
.orElse(null);
// Result : N

Conclusion

My tutorial ends here. We have seen how to iterate collection objects using Stream API, map and reduce functions, filtering data using Stream.filter(Predicate), how to flatten the input Stream and how can we group data stream by some property, then we have seen how can we get the max and the min by applying comparator function as a lambda expression.

If you are interested to learn more about streams Java 8 I will recommend you Stream Javadoc package documentation. If you want to learn more about Stream pipelining concept, you probably want to read Oracle article.

Happy Learning !!

--

--

HACENE KASDI

FullStack craftsman engineer, passionate about new technologies, Agile methodologies, clean code and the Cloud Native Applications.