- Published on
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
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
// 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 = [ .. ]
.
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.
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.
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 ]
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.
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 = [ .. ]
Double avgSal = empList.stream()
.mapToDouble(Employee::getSalary)
.average()
.orElseThrow(NoSuchElementException::new);
Reduction Operations:
(Identity, Accumulator, Combiner).
// 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.
// 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.
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 = [ .. ]
String empNames = empList.stream()
.map(Employee::getName)
.collect(Collectors.joining(", "))
.toString();
3. Order of Execution in stream operation
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.
Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> {
System.out.println("filter: " + s);
return true;
})
.forEach(s -> System.out.println("forEach: " + s))
// 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.
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));
// output
filter: d2
filter: a2
filter: b1
filter: b3
filter: c
map: a2
forEach: A2
For more info See: