Java 8 Streams

Streams are one of the most powerful features of Java 8 which has changed the way we write java programs/process

What are streams ?
As per official docs , Stream API provides class with which we can support functional-style operations like map , reduce and collect on stream of elements .

In imperative style of programming we write a lot of boiler plate code which is at times unnecessary for small operations like printing a list / array etc . With Java 8 streams this boiler plate code can be reduced to one line and also let Java do the heavy lifting.

For example , I am trying to extract a list of list of first names and print them.

@Getter
@Setter
public class Person {
    private String firstName;
    private String lastName;
    private Integer age;
    private String state;
    private String city;
}
class StreamDemo{
  public static void main(String ... args){
  List<Person> people = new ArrayList<>();
  people.add(new Person("john","doe",21,"FL","Orlando");
  people.add(new Person("jane","doe",29,"FL","miami");
  people.add(new Person("Kim","washington",29,"SF","redwood"));
  Iterator it = people.getIterator(); 
  List<String> firstNames = new ArrayList();
  while(it.hasNext()){
    Person person= it.next();
    firstNames.add(person.getFirstName());
  }
  // 3-4 lines to print the first name 
}}

If you carefully look at the above code snippet , your main business logic is to extract the first names and then print them. Even before you can get to your main business logic , you need to take care of iteration , creation of new list and iterate and store them .

Stream operations can be divided into two types , intermediate operations and terminal operations, both of them together forming a stream pipeline . A stream pipelineconsists of a Source , zero or more intermediate operations and a terminal operation .

Source

A stream has a source , which can be either a normal stream or a parallel stream. You can get a stream from a Collection object , an Array or I/O channel. This step returns a stream on which we can apply Intermediate Operations. Whenever we create a stream we need to close it by using a terminal operation . For the purpose of this illustration , I am just using the forEach and show various ways of creating a stream .

Arrays

private static void streamFromArrays() {
     String [] stringArray = new String[3];
     stringArray[0] ="One";
     stringArray[1] ="Two";
     stringArray[2] ="Three";
     Arrays.stream(stringArray).forEach(System.out::println);
 }
 //Output 
 One
 Two
 Three

Collections

private static void streamFromCollections() {
     String [] array = new String[]{"Hat","Coat","Glasses"};
     List listOfObjects = Arrays.asList(array);
     listOfObjects.stream().forEach(System.out::println);
 }

Using Stream.of()

private static void streamFromStreamOf() {
     System.out.println("Printing streams using StreamOf");
     String [] array = new String[]{"Cat","Bat","Dog"};
     Stream.of(array).forEach(System.out::println);
 }

Using stream builder

private static void streamsUsingBuilder() {
    System.out.println("Printing stream using builder");
    Stream.Builder<String> streamBuilder = Stream.builder();
    Stream<String> stringStream = 
    streamBuilder.add("Housten1")
                  .add("Hudson")
private static void streamsUsingBuilder() {
     System.out.println("Printing stream using builder");
     Stream.Builder streamBuilder = Stream.builder();
     Stream stringStream = 
     streamBuilder.add("Housten1")
                   .add("Hudson")
                   .add("Jenkins")
                   .build();
     stringStream.forEach(System.out::println);
 }

Using Iterate

We can create a stream of infinite sequential ordered elements produced by applying a function f to an initial element e. The resulting steam will have a sequence like e , f(e) ,f(f(e)) ……

private static void streamsUsingInfinetIterator() {
     System.out.println("Printing streams using infinite  iterator");
         Stream.iterate(2,(Integer i)-> i*i )
               .forEach(System.out::println);
 }

As the above piece of code runs infinitely, we have to force stop the application. To prevent this we will use one intermediate operation called limit which will stop the executions after a specified number of iterations .

private static void streamsUsingInfinetIterator() {
     System.out.println("Printing streams using infinite  iterator");
         Stream.iterate(1,(Integer i)-> i+i )
                 .limit(5)
                 .forEach(System.out::println);
 }

Using Generator

We can create a stream by using a generator which takes in a Supplier and generates elements infinitely. Just like Iterate , we need to limit the results using limit .

private static void streamFromGenerator(){
     System.out.println("Stream From generator");
     Stream.generate(Math::random)
             .limit(10)
             .forEach(System.out::println);
 }

Intermediate Operations

Intermediate operations specify how to create another stream from the existing stream. Most of the times a stream operation is piped through multiple intermediate operations and then finally closed by using a terminal operation. Any operation that returns a stream is an intermediate operation where as all of the terminal operation return objects of different types .

When we execute a method, for example, system.out.println(“Hello World”); The moment the line gets executed , JVM will print the line on the screen. However , in streams JVM will not execute the stream immediately. All the steps(Intermediate Operations) in the stream define how the elements in the stream are to be transformed rather than doing the transformation. The moment we run a terminal operation JVM kicks in and performs all the operations . These methods are lazy methods.

filter()

Filter method takes in a predicate and evaluates the boolean value. If it is true then the element is passed on to the next step of the pipeline . We can combine multiple filters .

private static void filterExample(){
     System.out.println("Filter example");
       Stream.iterate(0,n->n+1)
               .limit(10)
               .filter(n->n%2==0)
               .forEach(System.out::println);
 }

The beauty of Java 8 is you can use the predicate functional interface and pass that as a variable to the Streams which you can use in the filter .

private static void filterWithPredicateExample(){
private static void filterWithPredicateExample(){
     Predicate evenNumberFilter = n->n%2==0;
     System.out.println("Filter example with predicate");
     Stream.iterate(0,n->n+1)
             .limit(10)
             .filter(evenNumberFilter)
             .forEach(System.out::println);
 }

map()

This is the most commonly used operation in which a function converts a value of one type into another , this function is applied to all the elements in the stream .

Image for post

If you observe, on the left side we have an object that has two fields A and B.It gets transformed by a function and returns an object which has only one field. Not only this, we can do a number of things like converting lower case letters into upper case letters etc. One very important point to note is the function should not have any side effect. Side effect in the sense it should not disturb any other values during its operation outside the stream, like adding to list, etc . Below is a simple example .

private static void mapExample(){
     System.out.println("Map example");
     String [] names = new String []{"a","b","c"};
       Arrays.stream(names)
             .map(String::toUpperCase)
             .forEach(System.out::println);
 }

Let’s move on to a scenario where you are using JPA to get records from database and at times we need to convert it to a DTO before we can send it as response to an API call . In the normal Java way it takes more than one line of code to get the desired output. Lets look at how map helps us here :

@Getter
 @Setter
 @AllArgsConstructor
 public class StudentEntity {
 String firstName; String lastName; Integer age; String internalFields; String someOtherFields;
 }@Getter
 @Setter
 @ToString
 public class StudentDTO {
     String firstName;
     String lastName;
 }
 private static void mapObjectExample(){
     System.out.println("Map example");
     List entityList = new ArrayList<>();
     entityList.add( new StudentEntity("John",
             "Doe",
             22,
             "Some value",
             "Another value"));
     entityList.add(new StudentEntity("Jane",
             "Doe",
             22,
             "Confidential Value",
             "Some other confidential value"));
     entityList.stream()
               .map(obj->{
                   StudentDTO dto = new StudentDTO();
                   dto.setFirstName(obj.getFirstName());
                   dto.setLastName(obj.getLastName());
                   return dto;
               }).forEach(System.out::println);
 }

we can refactor the above step even further . If you see the map function it is taking a lambda as argument. Let us move it to its own class for the sake of maintenance .

public class StudentMapper implements Function {
     @Override
     public StudentDTO apply(StudentEntity studentEntity) {
         StudentDTO dto = new StudentDTO();
         dto.setFirstName(studentEntity.getFirstName());
         dto.setLastName(studentEntity.getLastName());
         return  dto;
     }
 }

And the stream line can be further reduced to one line .

entityList.stream()
           .map(new StudentMapper())
           .forEach(System.out::println);

This way we define DTO , Entity and its corresponding mapping class and we are all set to go. Responsibility of each class is evenly distributed and separated clearly .

flatMap()

Let us consider a simple filter example where you are streaming a 2d array and filter a value which is in the array .

private static void flatMapRequirement(){
     System.out.println("Why flat map is needed ?");
     Integer [][] twoDarray = new Integer[][]{{1,1,3,4},{5,6,7,9}};
     Stream.of(twoDarray).filter(i-> i>4 ).forEach(System.out::println);
 }

If you are using an IDE you would have already have a red line underneath the predicate .

error: bad operand types for binary operator ‘>’
  Stream.of(twoDarray).filter(i-> i>4 ).forEach(System.out::println);
  ^
  first type: Integer[]
  second type: int

This is because the operations like filter, sum ,and distinct do not support arrays . Before we can use them, we need to flatten them into a stream and then apply the above operations on the resulting stream.

We need to flatten 2d array into 1d array :

private static void flatMapRequirement(){
     System.out.println("Why flat map is needed ?");
     Integer [][] twoDarray = new Integer[][]{{1,1,3,4},{5,6,7,9}};
       Stream.of(twoDarray)
               .flatMap(x->Arrays.stream(x))
               .filter(i-> i>4)
               .forEach(System.out::println);
 }
 //x->Arrays.stream(x) takes sub arrays ie twoDarray0 
 //twoDarray1 and converts them to a stream 

Similarly , if you have multiple levels of nesting, you can always use one more flatMap operation in the pipeline and flatten your objects .

Sorted

Streams provide a beautiful method to sort elements in a stream . Sorted method takes in a Comparator and sorts the elements. In Java 8, like icing on the cake, Comparator has many default static methods that we can directly use and let Java do the heavy lifting .

private static void sortingUsingStreams(){
     System.out.println("Printing streams ");
     Integer[] array = new Integer[]{1,99,23,77,2,3,4,897};
     Stream.of(array).sorted(Comparator.naturalOrder())
                      .forEach(System.out::println);
 }

We can sort based on the reverse order :

private static void usingReverseOrder(){
     System.out.println("Printing streams reverse order");
     Integer[] array = new Integer[]{1,99,23,77,2,3,4,897};
     Stream.of(array).sorted(Comparator.reverseOrder())
             .forEach(System.out::println);
 }

Let us try the same operation with objects :

private static void usingObjects(){
     System.out.println("Printing sorted objects ");
     List entityList = new ArrayList<>();
     entityList.add(new StudentEntity("Jane",
             "Doe",
             29,
             "Confidential Value",
             "Some other confidential value"));
     entityList.add( new StudentEntity("John",
             "Doe",
             22,
             "Some value",
             "Another value"));
 entityList.stream()           .sorted(Comparator.comparingInt(StudentEntity::getAge))           .forEach(System.out::println);
 }

We can add one more sorting criterion without breaking a sweat by using thenComparing default method :

entityList.stream()
         .sorted(Comparator.comparingInt(StudentEntity::getAge)
         .thenComparing(StudentEntity::getFirstName))
         .forEach(System.out::println);

This will allow us to focus on what rather than how .

Distinct

One of the common day to day operations is to find out distinct elements. Streams makes it even more easier to extract distinct elements .

private static void gettingDistinctObjects(){
     System.out.println("Getting to distinct objects");
     List names = new ArrayList<>();
     names.add("A");
     names.add("B");
     names.add("B");
     names.add("A");
     names.add("C");
     names.add("K");
     names.add("P");
     names.add("Z");
     names.add("A");
     names.add("Y");
     names.add("K");
     names.add("P");
     names.add("Z");
 names.stream().distinct( ).forEach(System.out::println);
 }

Filter and map are stateless, meaning the output will be the same if we interchange the order . Where as in the distinct and sorted are stateful result of the operation changes based on the previous operation

Terminal operations

Terminal operations may traverse stream to produce a result or a side effect . After the terminal operation is performed , the stream is considered as consumed . You cannot and will not be able to use the stream again . If you need to use the stream again, you need to initialize it from the source . The terminal operations are eager methods , they complete the traversal before returning the values. Only the terminal operator iterator and spliterator do not return , but they are provided as escape hatch to enable arbitrary client controlled pipeline .

Consider the example below :

List<Integer> intList= new ArrayList<>();
IntStream.range(1,100).filter(x->x%2==0).forEach(intList::add);

In this , technically speaking, the code snippet is going to create a stream and will filter out all the even values and then it will add then into an array list . THIS IS A SIDE EFFECT , this should not be done. When you perform this using parallel streams, you will not be able to guarantee the order of execution as arrayList is not thread safe and you will get unexpected results. To avoid these situations ,Streams provides collect method .

Collect

Collect helps you to accumulate all the values in the stream and return the accumulated value. Collectors itself is a huge topic that cannot be covered in a 10 minute read. However , Java has made it easy for us by providing methods which we can use directly.

we can re-write the above function the proper way :

ListintList = Stream.iterate(0,i->i+1)
                                .limit(100)
                                .filter(x->x%2==0)
                                .collect(Collectors.toList());

Minimum and Maximum

This is a common day to day operation. We would have written this logic several time, Streams offers this functionality out of the box. The functions min() and max() take a predicate as an argument and will get you the minimum and maximum values within the stream. They are beautifully wrapped inside an Optional object. Optional handles null values gracefully .

private static void streamMinAndMax(){
     Integer [] array = new Integer[]{1,2,3422,767868,232,87878};
     List list= Arrays.asList(array);
     Integer min = list.stream()
                       .max(Comparator.naturalOrder())
                       .get();
     System.out.println("Min value is "+ min);
     Integer max = list.stream()
                       .min(Comparator.naturalOrder())
                       .get();
     System.out.println("maximum number is"+max);
 }

In the above example we are using Optional type which ensures that when an empty list is passed, you will not get a null value. Going a step further , we can return a default value if you get a null .

Integer max = list.stream()
                   .min(Comparator.naturalOrder())
                   .orElse(0);

Reduce

We use reducers to combine a collection of values into a single result . A reduce() function takes in a Identity and an accumulator. Accumulator is a BinaryOperator , a function that combines two values .

Consider the example below :

Image for post
private static void reduceTwo(){
     System.out.println("Reduce");
     Integer [] array = new Integer[] {1,2,3,4,5};
     List list= Arrays.asList(array);
     Integer total = list.stream().reduce(0,(sum,x)-> sum+x);
     System.out.println("Total is "+total);
 }

This code is equivalent to initializing a variable sum to 0( identity ) , iterate through list of elements and add the each elements value to the sum variable .

We can have intermediate operations as well .

private static void reduce(){
     System.out.println("Reduce");
     Integer [] array = new Integer[] {1,88,8937,8989,232322};
     List list= Arrays.asList(array);
     Integer total = list.stream().filter(n->n%2==0).reduce(0,(sum,x)-> sum+x);
     System.out.println("Total is "+total);
 }
srnyapathi
srnyapathi
Articles: 41