Java 8 – Introduction to Stream API

In this article, we will discuss Stream API – one of the new feature introduced in Java 1.8 version along with Functional Interface and Lambda expression

1. Shortcomings of earlier Java version :

Before understanding what is Stream in Java and why we need it, we have to look at shortcomings of earlier versions till Java 1.7

1.1 Collection framework :-

  • To represent/store group of objects/elements into single entity into some underlying data structure then we should obviously go for Collection framework like List or Set
  • To process these group of objects/elements like sorting, filtering based on some condition, mapping based on some operation then we have to explicitly write number of lines of code

1.2 Arrays :-

  • Arrays helps to represent/store fixed size of objects/elements of same data-type which doesn’t either grow nor shrinks unlike Collection
  • Again, to process Arrays for filtering, sorting, counting, iterating, finding, etc. we have to explicitly write number of lines of code

Collection and Arrays stores objects/elements but their processing like sorting, filtering, counting, iterating, etc. isn’t feasible directly and we have to write many lines of code to achieve this manually.

But Stream API introduced in Java 1.8 version comes to rescue to process objects/elements stored in Collection/Arrays in a elegant way.

2. What is Stream in Java 8 ? And why we need Stream ?

  • Stream can be defined as sequence of elements supporting aggregate operations
  • In other words, Stream are sequence of elements from source like Collection/Arrays for data processing like filtering, sorting, counting, finding, iterating, etc.
  • These data processing can be operated/executed in sequential or parallel mode
  • Stream support ordering in compliant with source provided like for example if the source is ArrayList which maintains insertion order so Stream preserves this order even after processing
  • Note: Stream aren’t Collection, Stream only does processing on Collection

3. Features or characteristics of Stream API :

  • Data-Structure :- Stream neither is a data-structure nor it stores data, only thing it does is data processing based on the operation specified
  • Consumes source :- For Stream to process data, it needs source and it could be either Collection or Arrays or any I/O resources or Stream generator function
  • Processing :- In short, Stream consume data-sources and performs some operations on it and finally produces desired results but never modifies original data-sources i.e.; it is untouched
  • Operations :- Stream operation divided into intermediate and terminal operations
  • Intermediate operation :- It returns Stream itself, so we can chain multiple operations and final output of these operations are Stream only for example filter(Predicate), map(Function), sorted(Comparator), etc.
  • Laziness :- Stream intermediate operations are lazy and computation on the source-data is only performed when the terminal operation is initiated, and source elements are consumed only as needed
  • Terminal operation :- It returns final output other than Stream for example count(), forEach(), collect(), min(), max(), etc. After initiating terminal operation, Stream are no longer available and it is said to be consumed
  • Stream pipeline :- A pipeline in Stream API composed of 3 things namely source, zero or more intermediate operations and terminal operation
  • Stream Parallelism :- Operation can be executed sequentially in single thread or parallel in multiple threads taking leverage of multi-core processor

4. Stream API examples :

4.1 Stream example to get EVEN number from List of Integer

package net.bench.resources.stream.example;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class StreamFilterEvenNumbers {

	public static void main(String[] args) {

		System.out.println("Java 8 Stream example to filter only even number : \n");

		// List of Integer - source
		List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);

		// Stream filter to get only EVEN numbers
		List<Integer> filterEvenNumbers = numbers // original source
				.stream() // 1. get stream from source
				.filter(i -> i%2 == 0) // 2. intermediate operation to get even numbers
				.collect(Collectors.toList()); // 3. terminal operation to produce result

		// print to console using Java 8 forEach
		filterEvenNumbers.forEach(i -> System.out.println(i));
	}
}

Output:

Java 8 Stream example to filter only even number : 

2
4
6
8

4.2 Stream example to get String with length greater than 5 from List of Strings

package net.bench.resources.stream.example;

import java.util.ArrayList;
import java.util.List;

public class StreamFilterNames {

	public static void main(String[] args) {

		System.out.println("Java 8 Stream example to filter"
				+ " names having more than 5 characters : \n");

		// List of String - source
		List<String> names = new ArrayList<>();

		// add few names to String list using add() method
		names.add("Sachin");
		names.add("Warne");
		names.add("Pietersen");
		names.add("McCullum");
		names.add("Jonty");
		names.add("Richards");
		names.add("Ranatunga");

		// Stream filter to get names whose length is greater than 5 and also print to console

		names // original source
		.stream() // 1. get stream from source
		.filter(s -> s.length() > 5) // 2. intermediate operation to get names with length>5
		.forEach(name -> System.out.println(name)); // 3. terminal operation to print
	}
}

Output:

Java 8 Stream example to filter names having more than 5 characters : 

Sachin
Pietersen
McCullum
Richards
Ranatunga

4.3 Stream example to map each number to its square value from List of Integer

package net.bench.resources.stream.example;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class StreamMapSquareExample {

	public static void main(String[] args) {

		System.out.println("Java 8 Stream example to Square each numbers : \n");

		// List of Integer - source
		List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);

		// Stream map example to Square each number
		List<Integer> square = numbers // original source
				.stream() // 1. get stream from source
				.map(i -> i*i) // 2. intermediate operation to Square each numbers
				.collect(Collectors.toList()); // 3. terminal operation to collect result

		// print to console using Java 8 forEach
		square.forEach(i -> System.out.println(i));
	}
}

Output:

Java 8 Stream example to Square each numbers : 

1
4
9
16
25
36
49
64
81

4.4 Stream example – combination of filter() and map() methods

package net.bench.resources.stream.example;

import java.util.ArrayList;
import java.util.List;

public class StreamFilterMapExample {

	public static void main(String[] args) {

		System.out.println("To Upper Case names having length more than 5 : \n");

		// List of String - source
		List<String> names = new ArrayList<>();

		// add few names to String list using add() method
		names.add("Sachin");
		names.add("Warne");
		names.add("Pietersen");
		names.add("McCullum");
		names.add("Jonty");
		names.add("Richards");
		names.add("Ranatunga");

		// Stream filter to get names whose length is greater than 5 and 
		// and map to upper case for each of the filtered names and 
		// also print to console

		names // original source
		.stream() // 1. get stream from source
		.filter(s -> s.length() > 5) // 2.1 intermediate operation to get names with length>5
		.map(s -> s.toUpperCase()) // 	2.2 intermediate operation to convert to UpperCase
		.forEach(name -> System.out.println(name)); // 3. terminal operation to print
	}
}

Output:

ToUpperCase names having length more than 5 : 

SACHIN
PIETERSEN
MCCULLUM
RICHARDS
RANATUNGA

4.5 Stream example – sorting alphabetically and converting to upper case

package net.bench.resources.stream.example;

import java.util.ArrayList;
import java.util.List;

public class StreamSortedUpperCaseExample {

	public static void main(String[] args) {

		System.out.println("Sorted names in natual ordering and"
				+ " converting to Upper Case : \n");

		// List of String - source
		List<String> names = new ArrayList<>();

		// add few names to String list using add() method
		names.add("Sachin Tendulkar");
		names.add("Anil Kumble");
		names.add("Sourav Ganguly");
		names.add("Javagal Srinath");
		names.add("Rahul Drvid");
		names.add("Nayan Mongia");
		names.add("VVS Laxman");

		// Stream sorted - to sort alphabetically 
		// and map to upper case for each of the filtered names and 
		// also print to console

		names // original source
		.stream() // 1. get stream from source
		.sorted() // 2.1 intermediate operation to sort in natural ordering
		.map(String::toUpperCase) // 	2.2 intermediate operation to convert to UpperCase
		.forEach(name -> System.out.println(name)); // 3. terminal operation to print
	}
}

Output:

Sorted names in natural ordering and converting to Upper Case : 

ANIL KUMBLE
JAVAGAL SRINATH
NAYAN MONGIA
RAHUL DRVID
SACHIN TENDULKAR
SOURAV GANGULY
VVS LAXMAN

4.6 Stream example – to get distinct values from list of names

package net.bench.resources.stream.example;

import java.util.Arrays;
import java.util.List;

public class StreamDistinctExample {

	public static void main(String[] args) {

		System.out.println("Disticnt names after removing duplicates : \n");

		// List of String - source
		List<String> names = Arrays.asList(
				"Vijay", 
				"Vikram",
				"Ajith", 
				"Vijay", 
				"Suriya",
				"Ajith",
				"Dhanush"
				);

		// Stream distinct() method to remove duplicates

		names // original source
		.stream() // 1. get stream from source
		.distinct() // 2. intermediate operation to get unique names
		.forEach(name -> System.out.println(name)); // 3. terminal operation to print console


		System.out.println("\nSize of original List and"
				+ " Stream count after removing duplicates : \n");

		// to get Stream count of unique values 
		long count = names // original source 
				.stream() // 	1. get source
				.distinct() // 	2 intermediate operation to get distinct names
				.count(); // 	3.terminal operation to get count

		System.out.println("Original Names list size : " + names.size());

		System.out.println("Stream count of unique names : " + count);
	}
}

Output:

Distinct names after removing duplicates : 

Vijay
Vikram
Ajith
Suriya
Dhanush

Size of original List and stream count after removing duplicates : 

Original Names list size : 7
Stream count of unique names : 5

5. Parallel and Sequential Stream API examples :

  • Below example prints list of names to console both sequentially and parallel after set of intermediate operations (Filter -> Sorting -> ToUpperCase) and final terminal operation (printing to console) for producing result
  • Let us find out difference in between these two execution mechanism
  • Serial execution :- Time difference from start to end of serial execution is 21 nano seconds, as it executes sequentially in a single thread
  • Parallel execution :- Time difference from start to end of parallel execution is 13 nano seconds, as it executes in parallel in multiple threads taking leverage of multi-core processor of the system where it is executed
  • Note :- Although time difference between serial and parallel execution isn’t big as we are analyzing with small set of values. But if we extend same example for large amount of values like 10000 elements inside List then time difference could be seen very easily
package net.bench.resources.stream.example;

import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;

public class ParallelStreamExample {

	public static void main(String[] args) {

		System.out.println("Names = Filter -> Sort -> UpperCase -> Print : \n");

		// List of String - source-data
		List<String> names = Arrays.asList(
				"VijayKanth", 
				"Vikram",
				"Ajith", 
				"Vijay", 
				"Suriya",
				"RajiniKanth",
				"Dhanush"
				);

		// 1.1 get start time before SERIAL Stream operation starts

		LocalDateTime startDateTime = LocalDateTime.now();
		System.out.println("SERIAL Stream start time : " + startDateTime);

		// 1.2 Serial Stream => filtering - sorting - map - print to console

		names // original source
		.stream() // 1. get SERIAL stream from source
		.filter(s -> s.length() > 5) // 2.1 filter names having length greater than 5
		.sorted() // 2.2 alphabetically sort after filtering
		.map(String::toUpperCase) // 2.3 convert to upper case
		.forEach(System.out::println); // 3. terminal operation to print to console

		// 1.3 get end time after SERIAL Stream operation completes

		LocalDateTime endDateTime = LocalDateTime.now();
		System.out.println("SERIAL Stream end time : " + endDateTime);

		// 1.4 calculate time difference

		Duration duration = Duration.between(startDateTime, endDateTime);
		long diff = Math.abs(duration.getNano());

		System.out.println("\nSerial Stream time difference : " + diff + "\n\n");



		// 2.1 get start time before PARALLEL Stream operation starts

		startDateTime = LocalDateTime.now();
		System.out.println("PARALLEL Stream start time : " + startDateTime);

		// 2.2 Parallel Stream => filtering - sorting - map - print to console

		names // original source
		.parallelStream() // 1. get PARALLEL stream from source
		.filter(s -> s.length() > 5) // 2.1 filter names having length greater than 5
		.sorted() // 2.2 alphabetically sort after filtering
		.map(String::toUpperCase) // 2.3 convert to upper case
		.forEach(System.out::println); // 3. terminal operation to print to console

		// 2.3 get end time after PARALLEL Stream operation completes

		endDateTime = LocalDateTime.now();
		System.out.println("PARALLEL Stream end time : " + endDateTime);

		// 2.4 calculate time difference
		duration = Duration.between(startDateTime, endDateTime);
		diff = Math.abs(duration.getNano());

		System.out.println("\nParallel Stream time difference : " + diff);
	}
}

Output:

Names = Filter -> Sort -> UpperCase -> Print : 

SERIAL Stream start time : 2020-04-26T18:49:57.583
DHANUSH
RAJINIKANTH
SURIYA
VIJAYKANTH
VIKRAM
SERIAL Stream end time : 2020-04-26T18:49:57.604

Serial Stream time difference : 21000000


PARALLEL Stream start time : 2020-04-26T18:49:57.607
SURIYA
VIJAYKANTH
VIKRAM
DHANUSH
RAJINIKANTH
PARALLEL Stream end time : 2020-04-26T18:49:57.620

Parallel Stream time difference : 13000000

6. Stateful v/s Stateless intermediate operation :

  • Intermediate operation further divided into stateless and stateful
  • Stateless intermediate operation :- These operation doesn’t retain state of previously processed elements/objects and next element’s processing is independent of any other elements in the stream
  • Stateless example :- Stream methods such as filter(), map(), etc. are intermediate stateless operations
  • Stateful intermediate operation :- In Stateful intermediate operation, each elements of stream is very important for producing final result
  • Stateful example :- Stream methods such as sorted(), distinct(), etc. couldn’t be calculated independently and every elements of Stream is important to derive at the final result for sorting or finding distinct elements

7. Intermediate operation v/s Terminal operation :

As we have seen in earlier section that, operation are divided into Intermediate and Terminal operation. There are wide range of useful Stream API (function/method) available. We will categorize them below,

7.1 Intermediate operation :

7.2 Terminal operation :

References:

Happy Coding !!
Happy Learning !!

Java 8 - How to create Stream ?
Java 8 - Method and Constructor References