Overview of Java 8 Stream API

This blog explains the Java 8 Stream API and most common usage. After reading this blog you should be able to work with stream API. Java stream produces pipe-lined data to perform different operations on group of elements. Streams internally iterate over the data and perform operations, then returns the result of functions performed if any.

Java.util.stream package was added in java 8. Java Stream API package contains classes that helps to perform various operations on streams or any collection of objects.

It may seem that Streams are similar to Collection. But there is a huge difference in both the programming paradigms. Streams API is not data structure. It iterates over the data that is used to perform different operations. Whereas Collection is in memory data structure which requires all data to be loaded in memory. Stream does not store any of the data in the memory of the computer.

Another noteworthy point is that,  java.util.stream is a package and java.util.stream.Stream is an interface.

Java 8 Stream API workflow

Java stream involves creation of stream, performing intermediate operations followed by terminal operations on the stream. This process or this flow is known as the Stream pipeline. A general high level stream work flow looks as below:

java 8 stream work flow diagram

As we can see, in the first step a stream is created with the given input. This input can be any type of data ranging from arrays, collections also primitives. Then this stream is passed on to the intermediate operations. These in turn produce the output streams and give them to the terminal operations. As a result of which output is generated form the terminal operations.

As we can see stream has 3 steps
1) Creation of java stream object
2) Intermediate operations on java stream  
3) Terminal operation on java stream

Step 1: Creation of Java Stream

There are many different ways to create streams like using methods of Stream class in stream APIs, methods using collections and arrays, Special streams (Stream of primitives), Stream of Strings, Files as a stream etc.

Let us focus on methods using collections and arrays as these are the most commonly used ways to create stream object.
These methods take collections and arrays as input to produce streams out of them.

Create java stream from collections

While creating streams from collection object we can use .stream() method on the collection object like lists, sets etc.

//Creation of simple stream from list
List<String> names = new Arraylist():
names.add(“Alpha”);
names.add(“Beta”);
Stream stream = names.stream();
//Creation of simple stream from Set
Set<Integer> set = new HashSet<>();
set.add(1);
set.add(2);
Stream stream = set.stream();

Create stream from Array using of() method

While creating streams from an array we need to use use .of() method of Stream.java class

//Creation of simple stream from Array
String[] arrays ={ "Alpha", "Beta", "Theta", "Gamma"};
Stream stream = Stream.of(arrays );

Step 2: Intermediate operations on stream in java [ps2id id="intermediateOperations" target=''/]

The operations which returns source object (stream) after processing are known as intermediate operations.(Also known as Non-Terminal operations). As they return stream object we can again call another intermediate or terminal operation on them. This helps to create chained operations one after the other consequently [Chain of Responsibility Design Pattern in Java] , to perform multiple operations on same element. In chained operation the output of one operation becomes the input of next.

Most important point to note stream operations does not change source objects, it returns new stream with updated elements.

The most common operations on java 8 steam object

  • Filter : used for conditional filtering the collection objects
  • Map : convert object from one form to another
  • Flatmap : Used for mapping nested objects in java stream
  • Sorted : for sorting stream elements

Java 8 stream conditional filter

Filter() is most frequent operation required on java streams
This method is used to filter elements as per the required condition . Predicate function is used (java.util.function.Predicate) to define filter criteria. Predicate function can be implemented using lambda expression or method references

//Using Lambda expression [read more]
List<User> users = ... // some list of users data
List<User> usersAbove18Age = users.stream().filter(u -> d.age() > 18).collect(Collectors.toList());

In the above example we are using java stream conditional filter to get user having age greater than 18

Java 8 stream Map

Java stream Map function is used to map object from one form to other of same or different type. It needs Function (java.util.function.Function) which takes one input and gives output of same or another type.
We can use lambda function or method reference to map object

//Using Lambda expression [read more]
List<User> users = ... // some list of users data
List<String> userNamess = users.stream().map(o -> o.getName());
//Using method reference [read more]
List<User> users = ... // some list of users data
List <String>userNamess = users.stream().map(User::getName);

Note: -

Map always transforms one object to exact one object, if we need to map conditional where we may get zero or more object we need to use flat map

Flatmap : map conditional or nested object

Similar to map function, flatmap is also used to convert object with extra feature of flattening the nested object. It means we can use flatmap() for mapping nested objects in stream.
We have seen that map is used to convert or transform object from one to exact one other object.

If we want to transform from one to many or no object, we cannot use map and we need to use flat map. Also if we need to transform nested object, we need to use flat map.

Flatmap is very useful if we need to map objects conditionally without using filter function.

Flat map transfers each element into streams

List<Student> computerDepartment = new ArrayList<>();
computerDepartment.add(new Student(1, “Example1”));
computerDepartment.add(new Student(2, “Example1”));

List <Student> mechanicalDepartment = new ArrayList<>();
mechanicalDepartment.add(new Student(101, “tutorial1”));
mechanicalDepartment.add(new Student(102, “tutorial2”));

List<List<Student> > allStudents = Arrays.asList(computerDepartment, mechanicalDepartment);
allStudents.stream().flatMap(l -> (l.stream()).forEach(System.out::println);

//Conditional mapping example without nested list
computerDepartment.stream().flatMap(s-> (s.getId()>1) ? Stream.of(s):Stream.empty()).forEach(System.out::println);

Sorted : sorting stream elements

Stream elements can be sorted by 3 ways

  1. Natural ordering :- stream.sorted() The objects must implement comparable interface for sorting
  2. Using Comparator:  stream.sorted(comparator), we can create our own comparator or we can use static method provided in Comparator interface, like comparing, comparingInt, etc
    1. list.stream().sorted( (o1, o2)->o1.getAge()-o2.getAge() ): Sorting by age using Lambda to implement comparator
    2. list.stream().sorted(Comparator.comparing(user::getAge)) : sorting by age using comparing method from Comparator
  3. Reverse ordering :- For reverse ordering we can use reverseOrder() method of Comparator.
    1. list.stream().sorted(Comparator.reverseOrder()) : natural order reverse
    2. list.stream().sorted(Comparator.comparing(user::getAge).reversed()) : reverse order by age

Step 3: Terminal operation on stream[ps2id id='streamTerminalOperation' target=''/]

In contrast to the intermediate operations described above, the terminal operations do not return stream as output.
Terminal operation can produce result as primitive or collection or any Object or may not produce any output. The intermediate operations always precede a terminal operation.
Although there can be multiple intermediate operations, but they must be followed by one and only one terminal operation.

ForEach

This method helps in iterating over all elements of a stream and perform some operation on each item.
Takes Consumer function as input which contains the logic we need to implement. We can implement the consumer function using lambda function or method reference

nameList.stream().forEach(System.out::println);
//Print names with length is greater than 5
nameList.stream().filter(n -> n.length() > 5).forEach(System.out::println);

Collect

One  of the most commonly used terminal operation. It allows to convert stream elements to other containers, like list, set etc

List<User> users = ... // some list of users data
//Collecting data as a list
List<User> userAbove18Age = users.stream().filter(o -> d.age() > 18).collect(Collectors.toList());
//Collection data as a set
Set<User> userAbove18Age = users.stream().filter(o -> d.age() > 18).collect(Collectors.toSet());

ToArray

Similar to the collect() method described above, this terminal operation collects the stream data into containers of arrays. In the example below we can see, the stream data can be collected into a generic array of objects, or to a array of a specific type eg. Student [].

List<Student> students = ... // some list of students data
Object[] objects = students.stream().toArray();
Student[] studentArray= students.stream().toArray(Student[]::new);

Note:-

If we do not specify the class type [eg:  toArray(Student[]::new) ], Array will be created for Object class

Min and Max

Finds the minimum or maximum of the list value form list. We need to provide comparator for comparing the elements.
We can create our own comparator or we can use static methods provided in Comparator interface, like comparing(), comparingInt(),etc

List<Student > students = ... //List of students data
//Return student with highest marks
Student topper = students.stream().max(Comparator.comparing(student::getMarks));
//Return student with shortest height
Student shortStudent = students.stream().min(Comparator.comparing(user::getHeight));

Count

Returns the count of elements in stream as a long value

List<User> userList = ... //List of users data
//Count of all users
Long allUserCount = userList.stream().count();
//Count of users above the age of 18
Long userAbove18= userList.stream().filter(u -> u.getAge() > 18).count();

Reduce

Used to reduce elements to single result by combining or processing all the elements.
Returns optional object means null or something
There are three different ways for reduce operation

  • reduce(BinaryOperator accumulator) : Takes the binary Accumulator function to compute the result object and return the Optional instance

    Integer[] noNumbers = {};
    Optional<Integer> reduce1 = Arrays.stream(noNumbers).reduce((a, b) -> a + b);
    System.out.println(reduce1); // OUTPUT = Optional.empty

    Integer[] numbers = {1,2,3};
    Optional<Integer> reduce2 = Arrays.stream(numbers).reduce((a, b) -> a + b);
    System.out.println(reduce2); // OUTPUT = Optional[6]

  • reduce(T identity, BinaryOperator accumulator) : We need two parameters, Identity which works as Return type and initial and Accumulator function.
    Computation logic includes the initial value while computing.

     

    Integer[] noNumbers = {};
    Integer reduce1 = Arrays.stream(noNumbers).reduce(0, (a, b) -> a + b);
    System.out.println(reduce1); // OUTPUT = 0

    Integer[] numbers = {1,2,3};
    Integer reduce2 = Arrays.stream(numbers).reduce(2, (a, b) -> a + b);
    System.out.println(reduce2); // OUTPUT = 8

  • reduce(U identity, BiFunction accumulator, BinaryOperator combiner); Mainly used for parallel streams, Takes 3 arguments Identity which works as Return type and initial, Accumulator function and Combiner for combining the results of different streams in parallel processing.
    Computation logic includes the initial value while computing.

     

    Integer[] numbers = {1,2,3};
    Integer reduce1 = Arrays.stream(numbers).reduce(0,(a, b) -> a + b,(a, b) -> a + b);
    System.out.println(reduce1); // OUTPUT = 6

    List list= Arrays.asList(numbers);
    Integer reduce2 = list.parallelStream().reduce(0,(a, b) -> a + b,(a, b) -> a + b);
    System.out.println(reduce2); // OUTPUT = 6

Match

This method checks if the returned result matches the criteria. It returns boolean value (True/False) depending upon the result.
There are 3 matching methods that are used to match the output with a certain criteria.

  • anyMatch : Check if at least one record is found
  • allMatch : Check if all records are found
  • noneMatch : Checks if no record found
//Ways to match string starting with "M"
boolean result = userList.stream().anyMatch((u) -> s.startsWith("M"));
boolean result = userList.stream().allMatch ((u) -> s.startsWith("M"));
boolean result = userList.stream().noneMatch ((u) -> s.startsWith("M"));

anyMatch() is the short circuit operation as it will return true once it finds the first true condition and stops further processing.

findFirst

Returns the first result from stream in optional(null or something) type

Optional<user> firstRecord = userList.stream().findFirst()
Or
Optional<user> firstRecord = userList.stream().filter(u -> u.length() > 5).findFirst();

findFirst() is the short circuit operation as it returns the first element and stops further processing.

Fast track reading

  • Java stream produces pipe-lined data to perform different operations on group of elements
  • java.util.stream is package and java.util.stream.Stream is an interface
  • Stream is not a datatype
  • Streams API supports serial as well as parallel operations
  • Stream operations can be classified into three types namely : Creation operation, Intermediate operation, Terminal operation
Creation operationIntermediate operationTerminal operation
These functions create streamsThese are functions operating on streams resulting into output streamThese are functions operating on streams to produce non-stream output
Streams are created from various sources, namely: Collections,Array, Strings, Files also primitive data typesThese take stream element as inputThese take stream element as input
These produce stream as outputThese produce stream as outputThe result of functions performed if any are returned.
There can be only one creation operation in stream pipeline (In the beginning)Any number of intermediate operations chained to each otherThere can be only one terminal operation in stream pipeline (At the end)
eg: empty(), stream(), of, etcex: filter, map, flatmap, sorted, etceg: forEach, collect, toArray, min, max, count, etc

References:

Leave a Reply

Your email address will not be published. Required fields are marked *