Contents

A Guide To Java Stream API

Stream connects to the source of collections data structure and can performs specific operations on it. It was added in Java 8 under the Package name java.util.stream.

Introduction

What does it offers

  • It can Efficiently process large amounts of data.
  • Supports Parallel operations, to leverage multi core processors.
  • Lazy handling of pipeline operations & avoids unnecessary intermediate computations.
  • It represents a sequence of elements and supports different kind of operations to perform computations upon those elements.

Code Example

1
2
3
4
5
6
List<String> myList = Arrays.asList("a1", "a2", "b1", "c2", "c1");
myList.stream()
    .filter(s -> s.startsWith("c"))
    .map(String::toUpperCase)
    .sorted()
    .forEach(System.out::println);

Code Analysis:

  • From the code, we have a list of string data - "a1", "a2", "b1", "c2", "c1".
  • Filtering string that starts with character “c”
  • converting it to uppercase i.e “C1”
  • Sorting the list
  • Printing out each elements

Its a general overview. Lets understand more on streams creation, initialization, operations and execution.

1. Creating Streams

1
2
3
4
5
6
7
8
9
// Initializing array of String
String[] arr = new String[]{"a", "b", "c"};
// Coverting the array into stream
Stream<String> stream = Arrays.stream(arr); 

// Or can also

// Initializing the stream
Stream<String> streamOfArray = Stream.of("a", "b", "c");

2. Stream Operations

forEach:
forEach() it loops over the stream elements. Lets say we have List<Product> productList = [ .. ] .

1
2
productsList.stream()
	.forEach(p -> p.setDiscount(true)); 

Map:
map() produces a new stream after applying a function to each element of the original stream. The new stream could be of different type.

1
2
3
List<Double> newEmployeeList = employeesList.stream()
	.map((employee) -> employee.getSalary()) 
	.collect(Collectors.toList()); 

Filter:
filter() produces a new stream of the elements that satisfy the given condition.

1
2
3
List<Employee> highSalEmpList = employeesList.stream()
	.filter((employee) -> employee.getSalary() > 50000)
	.collect(Collectors.toList()); 

flatMap:
For the complex data structures like-
Stream<List<String>>
flatMap() helps us to flatten the data structure. Lets say we have structure like this - [ [1,2,3],[4,5,6],[7,8,9] ] which has “two levels”. In simple - Flattening means transforming it as : [ 1,2,3,4,5,6,7,8,9 ]

1
2
3
4
5
6
7
8
List<List<String>> namesNested = Arrays.asList( 
	Arrays.asList("Jeff", "Bezos"), 
	Arrays.asList("Bill", "Gates"), 
	Arrays.asList("Mark", "Zuckerberg"));

List<String> namesFlatStream = namesNested.stream()
	.flatMap(Collection::stream)
	.collect(Collectors.toList()); 

Matching:
anyMatch(), allMatch(), noneMatch().
Lets say we have a collection of a color pencils.

1
2
3
4
5
6
7
8
9
List<String> colorPencils = Arrays.asList("greenPencil", "greyPencil", "blackPencil", "bluePencil", "redPencil", "greenPencil", "bluePencil");
boolean isGreenPencilAvailable = colorPencils.stream()
		.anyMatch(element -> element.contains("greenPencil")); // true

boolean isAllPencilsGreen = colorPencils.stream()
		.allMatch(element -> element.contains("greenPencil")); // false

boolean isGreenPencilsNotPresent = colorPencils.stream()
		.noneMatch(element -> element.contains("greenPencil")); // false

Specialized Operations:
Operation like sum(), average(), range(). Let’s say we have List<Employee> empList = [ .. ]

1
2
3
4
Double avgSal = empList.stream()
	.mapToDouble(Employee::getSalary)
	.average()
	.orElseThrow(NoSuchElementException::new); 

Reduction Operations:
(Identity, Accumulator, Combiner).

1
2
3
4
// with Identity and Accumulator 
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
int result = numbers.stream()
	.reduce(0, (subtotal, element) -> subtotal + element); 

Code Analysis:

  • reduce ( startingValue, binaryOperator/accumulator-fuction)

  • startingValue = 0, is the identity, also the default result if stream is empty and only initialized at first

  • subtotal is the accumulator storing the final value after each operation.

  • 1st iteration: since startingValue = 0 than (subtotal/accumulator = 0, element = 1). arguments setted (subtotal, element)

  • operation: -> subtotal + element 0 + 1 = 1, now returning 1. subtotal = 1

  • 2nd iteration: (subtotal = 1, element = 2). Operation: 1 + 2 = 3

  • 3nd iteration: (subtotal = 3, element = 3). Operation: 3 + 3 = 6

  • Final result = 5

This was a example using identity and accumulator.
With Identity, Accumulator and Combiner - three arguments is used in parallel processing. Combiner works with parallel stream only, otherwise there is nothing to combine.

1
2
3
4
5
// with Identity, Accumulator and Combiner
List<Integer> list2 = Arrays.asList(5, 6, 7);
int res = list2.parallelStream()
	.reduce(1, (s1, s2) -> s1 * s2, (p, q) -> p * q);
// output 210 

Collect:
Collect is used to get stuff out of the stream once we are done with all the processing.

1
2
3
4
List<Employee> employees = empList.stream()
	.collect(Collectors.toList()); 
Set<Employee> employees = empList.stream()
	.collect(Collectors.toSet()); 

Joining:
Collectors.joining() will insert the delimiter between the two String elements of the stream. Lets say we have List<Employee> empList = [ .. ]

1
2
3
4
String empNames = empList.stream()
	.map(Employee::getName)
	.collect(Collectors.joining(", "))
	.toString();  

3. Order of Execution in stream operation

1
2
3
4
5
Stream.of("d2", "a2", "b1", "b3", "c")
	.filter(s -> {
				    System.out.println("filter: " + s);
					return true;
			    }); 

When executing this code snippet, nothing is printed to the console. That is because intermediate operations will only be executed when a terminal operation is present.

1
2
3
4
5
6
Stream.of("d2", "a2", "b1", "b3", "c")
	.filter(s -> {
				    System.out.println("filter: " + s);
					return true;
			    })
	.forEach(s -> System.out.println("forEach: " + s)) 
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// output
filter: d2 
forEach: d2 
filter: a2 
forEach: a2 
filter: b1 
forEach: b1 
filter: b3 
forEach: b3 
filter: c 
forEach: c 

The order of the execution is very important to understand in stream operations.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Stream.of("d2", "a2", "b1", "b3", "c")
	.filter(s -> {
					System.out.println("filter: " + s);
					return s.startsWith("a");
			    })
	.sorted((s1, s2) -> {
						    System.out.printf("sort: %s; %s\n", s1, s2);
							return s1.compareTo(s2);
			            })
    .map(s -> {
                System.out.println("map: " + s);
                return s.toUpperCase();
            })
    .forEach(s -> System.out.println("forEach: " + s));
1
2
3
4
5
6
7
8
// output
filter: d2
filter: a2
filter: b1
filter: b3
filter: c
map: a2
forEach: A2 

For more info See: