Before we look into Java 8 Stream API Examples, let’s see why it was required. Suppose we want to iterate over a list of integers and find out sum of all the integers greater than 10.
Prior to Java 8, the approach to do it would be:
1
2
3
4
5
6
7
8
9
10
11
|
private
static
int
sumIterator(List<Integer> list) {
Iterator<Integer> it = list.iterator();
int
sum =
0
;
while
(it.hasNext()) {
int
num = it.next();
if
(num >
10
) {
sum += num;
}
}
return
sum;
}
|
There are three major problems with the above approach:
To overcome all the above shortcomings, Java 8 introduces Stream API. We can use Stream API to implement internal iteration, that is better because java framework is in control of the iteration.
Internal iteration provides several features such as sequential and parallel execution, filtering based on the given criteria, mapping etc.
Most of the Stream API method arguments are functional interfaces, so lambda expressions work very well with them. Let’s see how can we write above logic in a single line statement.
1
2
3
|
private
static
int
sumStream(List<Integer> list) {
return
list.stream().filter(i -> i >
10
).mapToInt(i -> i).sum();
}
|
Notice that above program utilizes java framework iteration strategy, filtering and mapping methods and would increase efficiency.
First of all we will look into the core concepts of Stream API and then we will go through some examples for understanding most commonly used methods.
A collection is an in-memory data structure to hold values and before we start using collection, all the values should have been populated. Whereas a Stream is a data structure that is computed on-demand.
Stream doesn’t store data, it operates on the source data structure (collection and array) and produce pipelined data that we can use and perform specific operations. Such as we can create a stream from the list and filter it based on a condition.
Stream operations use functional interfaces, that makes it a very good fit for functional programming using lambda expressions. As you can see in the above example that using lambda expressions make our code readable and short.
Stream internal iteration principle helps in achieving lazy-seeking in some of the stream operations. For example filtering, mapping, or duplicate removal can be implemented lazily, allowing higher performance and scope for optimization.
Streams are consumable, so there is no way to create a reference to stream for future usage. Since the data is on-demand, it’s not possible to reuse the same stream multiple times.
Stream support sequential as well as parallel processing, parallel processing can be very helpful in achieving high performance for large collections.
All the Stream API interfaces and classes are in the java.util.stream
package. Since we can use primitive data types such as int, long in the collections using auto-boxing and these operations could take a lot of time, there are specific classes for these – IntStream
, LongStream
and DoubleStream
.
Some of the commonly used functional interfaces in the Stream API methods are:
Function<t, r="">
is the generic form where T is the type of the input to the function and R is the type of the result of the function. For handling primitive types, there are specific Function interfaces – ToIntFunction
, ToLongFunction
, ToDoubleFunction
, ToIntBiFunction
, ToLongBiFunction
,ToDoubleBiFunction
, LongToIntFunction
, LongToDoubleFunction
, IntToLongFunction
,IntToDoubleFunction
etc. Some of the Stream methods where Function
or it’s primitive specialization is used are:
Function
, there are primitive specific interfaces for int, long and double. Some of the Stream methods where Predicate
or BiPredicate
specializations are used are:
Some of the Stream methods where Consumer
, BiConsumer
or it’s primitive specialization interfaces are used are:
Supplier
argument are:
Optional is a container object which may or may not contain a non-null value. If a value is present,isPresent()
will return true and get()
will return the value. Stream terminal operations return Optional object. Some of these methods are:
For supporting parallel execution in Stream API, Spliterator
interface is used. Spliterator trySplit
method returns a new Spliterator that manages a subset of the elements of the original Spliterator.
Stream API operations that returns a new Stream are called intermediate operations. Most of the times, these operations are lazy in nature, so they start producing new stream elements and send it to the next operation. Intermediate operations are never the final result producing operations. Commonly used intermediate operations are filter
and map
.
Stream API operations that returns a result or produce a side effect. Once the terminal method is called on a stream, it consumes the stream and after that we can’t use stream. Terminal operations are eager in nature i.e they process all the elements in the stream before returning the result. Commonly used terminal methods are forEach
, toArray
, min
, max
, findFirst
, anyMatch
, allMatch
etc. You can identify terminal methods from the return type, they will never return a Stream.
An intermediate operation is called short circuiting, if it may produce finite stream for an infinite stream. For example limit()
and skip()
are two short circuiting intermediate operations.
A terminal operation is called short circuiting, if it may terminate in finite time for infinite stream. For example anyMatch
, allMatch
, noneMatch
, findFirst
and findAny
are short circuiting terminal operations.
I have covered almost all the important parts of the Java Stream API. It’s exciting to use this new API features and let’s see it in action with some examples.
There are several ways through which we can create a stream from array and collections. Let’s look into these with simple examples.
Stream.of()
to create a stream from similar type of data. For example, we can create Stream of integers from a group of int or Integer objects.
1
|
Stream<Integer> stream = Stream.of(
1
,
2
,
3
,
4
);
|
Stream.of()
with an array of Objects to return the stream. Note that it doesn’t support autoboxing, so we can’t pass primitive type array.
1
2
3
4
5
|
Stream<Integer> stream = Stream.of(
new
Integer[]{
1
,
2
,
3
,
4
});
//works fine
Stream<Integer> stream1 = Stream.of(
new
int
[]{
1
,
2
,
3
,
4
});
//Compile time error, Type mismatch: cannot convert from Stream<int[]> to Stream<Integer>
|
stream()
to create sequential stream and parallelStream()
to create parallel stream.
1
2
3
4
5
6
7
8
|
List<Integer> myList =
new
ArrayList<>();
for
(
int
i=
0
; i<
100
; i++) myList.add(i);
//sequential stream
Stream<Integer> sequentialStream = myList.stream();
//parallel stream
Stream<Integer> parallelStream = myList.parallelStream();
|
Stream.generate()
and Stream.iterate()
methods to create Stream.
1
2
|
Stream<String> stream1 = Stream.generate(() -> {
return
"abc"
;});
Stream<String> stream2 = Stream.iterate(
"abc"
, (i) -> i);
|
Arrays.stream()
and String.chars()
methods.
1
2
|
LongStream is = Arrays.stream(
new
long
[]{
1
,
2
,
3
,
4
});
IntStream is2 =
"abc"
.chars();
|
There are several ways through which we can get a Collection or Array from a Stream.
collect()
method to get List, Map or Set from stream.
1
2
3
4
5
6
|
Stream<Integer> intStream = Stream.of(
1
,
2
,
3
,
4
);
List<Integer> intList = intStream.collect(Collectors.toList());
System.out.println(intList);
//prints [1, 2, 3, 4]
intStream = Stream.of(
1
,
2
,
3
,
4
);
//stream is closed, so we need to create it again
Map<Integer,Integer> intMap = intStream.collect(Collectors.toMap(i -> i, i -> i+
10
));
System.out.println(intMap);
//prints {1=11, 2=12, 3=13, 4=14}
|
toArray()
method to create an array from the stream.
1
2
3
|
Stream<Integer> intStream = Stream.of(
1
,
2
,
3
,
4
);
Integer[] intArray = intStream.toArray(Integer[]::
new
);
System.out.println(Arrays.toString(intArray));
//prints [1, 2, 3, 4]
|
Let’s look into commonly used Stream intermediate operations example.
1
2
3
4
5
6
7
|
List<Integer> myList =
new
ArrayList<>();
for
(
int
i=
0
; i<
100
; i++) myList.add(i);
Stream<Integer> sequentialStream = myList.stream();
Stream<Integer> highNums = sequentialStream.filter(p -> p >
90
);
//filter numbers greater than 90
System.out.print(
"High Nums greater than 90="
);
highNums.forEach(p -> System.out.print(p+
" "
));
//prints "High Nums greater than 90=91 92 93 94 95 96 97 98 99 "
|
1
2
3
4
5
|
Stream<String> names = Stream.of(
"aBc"
,
"d"
,
"ef"
);
System.out.println(names.map(s -> {
return
s.toUpperCase();
}).collect(Collectors.toList()));
//prints [ABC, D, EF]
|
1
2
3
4
5
6
7
|
Stream<String> names2 = Stream.of(
"aBc"
,
"d"
,
"ef"
,
"123456"
);
List<String> reverseSorted = names2.sorted(Comparator.reverseOrder()).collect(Collectors.toList());
System.out.println(reverseSorted);
// [ef, d, aBc, 123456]
Stream<String> names3 = Stream.of(
"aBc"
,
"d"
,
"ef"
,
"123456"
);
List<String> naturalSorted = names3.sorted().collect(Collectors.toList());
System.out.println(naturalSorted);
//[123456, aBc, d, ef]
|
1
2
3
4
5
6
7
8
9
|
Stream<List<String>> namesOriginalList = Stream.of(
Arrays.asList(
"Pankaj"
),
Arrays.asList(
"David"
,
"Lisa"
),
Arrays.asList(
"Amit"
));
//flat the stream from List<String> to String stream
Stream<String> flatStream = namesOriginalList
.flatMap(strList -> strList.stream());
flatStream.forEach(System.out::println);
|
Let’s look at some of the terminal operations example.
1
2
3
4
|
Stream<Integer> numbers = Stream.of(
1
,
2
,
3
,
4
,
5
);
Optional<Integer> intOptional = numbers.reduce((i,j) -> {
return
i*j;});
if
(intOptional.isPresent()) System.out.println(
"Multiplication = "
+intOptional.get());
//120
|
1
2
3
|
Stream<Integer> numbers1 = Stream.of(
1
,
2
,
3
,
4
,
5
);
System.out.println(
"Number of elements in stream="
+numbers1.count());
//5
|
1
2
|
Stream<Integer> numbers2 = Stream.of(
1
,
2
,
3
,
4
,
5
);
numbers2.forEach(i -> System.out.print(i+
","
));
//1,2,3,4,5,
|
1
2
3
4
5
6
7
8
9
10
11
|
Stream<Integer> numbers3 = Stream.of(
1
,
2
,
3
,
4
,
5
);
System.out.println(
"Stream contains 4? "
+numbers3.anyMatch(i -> i==
4
));
//Stream contains 4? true
Stream<Integer> numbers4 = Stream.of(
1
,
2
,
3
,
4
,
5
);
System.out.println(
"Stream contains all elements less than 10? "
+numbers4.allMatch(i -> i<
10
));
//Stream contains all elements less than 10? true
Stream<Integer> numbers5 = Stream.of(
1
,
2
,
3
,
4
,
5
);
System.out.println(
"Stream doesn't contain 10? "
+numbers5.noneMatch(i -> i==
10
));
//Stream doesn't contain 10? true
|
1
2
3
4
5
|
Stream<String> names4 = Stream.of(
"Pankaj"
,
"Amit"
,
"David"
,
"Lisa"
);
Optional<String> firstNameWithD = names4.filter(i -> i.startsWith(
"D"
)).findFirst();
if
(firstNameWithD.isPresent()){
System.out.println(
"First Name starting with D="
+firstNameWithD.get());
//David
}
|
Stream API brings a lot of new stuffs to work with list and arrays, but it has some limitations too.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
package
com.journaldev.java8.stream;
import
java.util.ArrayList;
import
java.util.Arrays;
import
java.util.List;
import
java.util.stream.Stream;
public
class
StatefulParallelStream {
public
static
void
main(String[] args) {
List<Integer> ss = Arrays.asList(
1
,
2
,
3
,
4
,
5
,
6
,
7
,
8
,
9
,
10
,
11
,
12
,
13
,
14
,
15
);
List<Integer> result =
new
ArrayList<Integer>();
Stream<Integer> stream = ss.parallelStream();
stream.map(s -> {
synchronized
(result) {
if
(result.size() <
10
) {
result.add(s);
}
}
return
s;
}).forEach( e -> {});
System.out.println(result);
}
}
|
If we run above program, you will get different results because it depends on the way stream is getting iterated and we don’t have any order defined for parallel processing. If we use sequential stream, then this problem will not arise.
That’s all for Stream API in Java. I am looking forward to use this feature and make the code readable with better performance through parallel processing.