1 Overview
Stream represents a sequence of objects from a source, which supports aggregate operations. Following are the characteristics of a Stream:
Sequence of elements − A stream provides a set of elements of specific type in a sequential manner. A stream gets/computes elements on demand. It never stores the elements.
Source − Stream takes Collections, Arrays, or I/O resources as input source.
Aggregate operations − Stream supports aggregate operations like filter, map, limit, reduce, find, match, and so on.
Pipelining − Most of the stream operations return stream itself so that their result can be pipelined. These operations are called intermediate operations and their function is to take input, process them, and return output to the target. collect() method is a terminal operation which is normally present at the end of the pipelining operation to mark the end of the stream.
Automatic iterations − Stream operations do the iterations internally over the source elements provided, in contrast to Collections where explicit iteration is required.
2 Java Stream vs. Collection
All of us have watch online videos on youtube or some other such website. When you start watching video, a small portion of file is first loaded into your computer and start playing. You don’t need to download complete video before start playing it. This is called streaming. I will try to relate this concept with respect to collections and differentiate with Streams.
At the basic level, the difference between Collections and Streams has to do with when things are computed. A Collection is an in-memory data structure, which holds all the values that the data structure currently has—every element in the Collection has to be computed before it can be added to the Collection. A Stream is a conceptually fixed data structure, in which elements are computed on demand. This gives rise to significant programming benefits. The idea is that a user will extract only the values they require from a Stream, and these elements are only produced—invisibly to the user—as and when required. This is a form of a producer-consumer relationship.
In java, java.util.Stream represents a stream on which one or more operations can be performed. Stream operations are either intermediate or terminal. While terminal operations return a result of a certain type, intermediate operations return the stream itself so you can chain multiple method calls in a row. Streams are created on a source, e.g. a java.util.Collection like lists or sets (maps are not supported). Stream operations can either be executed sequential or parallel.
Based on above points, if we list down the various characteristics of Stream, they will be as follows:
- Not a data structure
- Designed for lambdas
- Do not support indexed access
- Can easily be outputted as arrays or lists
- Lazy access supported
- Parallelizable
3 Different ways to create streams
3.1 Stream.of(val1, val2, val3….)
public class StreamBuilders
{
public static void main(String[] args)
{
Stream<Integer> stream = Stream.of(1,2,3,4,5,6,7,8,9);
stream.forEach(p -> System.out.println(p));
}
}
3.2 Stream.of(arrayOfElements)
public class StreamBuilders
{
public static void main(String[] args)
{
Stream<Integer> stream = Stream.of( new Integer[]{1,2,3,4,5,6,7,8,9} );
stream.forEach(p -> System.out.println(p));
}
}
3.3 List.stream()
public class StreamBuilders
{
public static void main(String[] args)
{
List<Integer> list = new ArrayList<Integer>();
for(int i = 1; i< 10; i++){
list.add(i);
}
Stream<Integer> stream = list.stream();
stream.forEach(p -> System.out.println(p));
}
}
3.4 Stream.generate() or Stream.iterate()
The generate() method accepts a Supplier for element generation. As the resulting stream is infinite, developer should specify the desired size or the generate() method will work until it reaches the memory limit:
Stream<String> streamGenerated =
Stream.generate(() -> "element").limit(10);
The code above creates a sequence of ten strings with the value – “element”.
Another way of creating an infinite stream is by using the iterate() method:
Stream<Integer> streamIterated = Stream.iterate(40, n -> n + 2).limit(20);
The first element of the resulting stream is a first parameter of the iterate() method. For creating every following element the specified function is applied to the previous element. In the example above the second element will be 42.
3.5 Stream of String
String can also be used as a source for creating a stream.
With the help of the chars() method of the String class. Since there is no interface CharStream in JDK, the IntStream is used to represent a stream of chars instead.
public class StreamBuilders
{
public static void main(String[] args)
{
IntStream stream = "12345_abcdefg".chars();
stream.forEach(p -> System.out.println(p));
//OR
//The following example breaks a String into sub-strings according to specified RegEx:
Stream<String> stream = Stream.of("A$B$C".split("\\$"));
stream.forEach(p -> System.out.println(p));
}
}
3.6 Stream.builder()
When builder is used the desired type should be additionally specified in the right part of the statement, otherwise the build() method will create an instance of the Stream:
Stream<String> streamBuilder =
Stream.<String>builder().add("a").add("b").add("c").build();
3.7 Stream of File
Java NIO class Files allows to generate a Stream of a text file through the lines() method. Every line of the text becomes an element of the stream:
Path path = Paths.get("C:\\file.txt");
Stream<String> streamOfStrings = Files.lines(path);
Stream<String> streamWithCharset =
Files.lines(path, Charset.forName("UTF-8"));
The Charset can be specified as an argument of the lines() method.
4 Convert streams to collections
Please note that it is not a true conversion. It’s just collecting the elements from the stream into a collection or array.
4.1 Convert Stream to List – Stream.collect( Collectors.toList() )
public class StreamBuilders {
public static void main(String[] args){
List<Integer> list = new ArrayList<Integer>();
for(int i = 1; i< 10; i++){
list.add(i);
}
Stream<Integer> stream = list.stream();
List<Integer> evenNumbersList = stream.filter(i -> i%2 == 0).collect(Collectors.toList());
System.out.print(evenNumbersList);
}
}
4.2 Convert Stream to array – Stream.toArray( EntryType[]::new )
public class StreamBuilders {
public static void main(String[] args){
List<Integer> list = new ArrayList<Integer>();
for(int i = 1; i< 10; i++){
list.add(i);
}
Stream<Integer> stream = list.stream();
Integer[] evenNumbersArr = stream.filter(i -> i%2 == 0).toArray(Integer[]::new);
System.out.print(evenNumbersArr);
}
}
There are plenty of other ways also to collect stream into set, map or into multiple ways. Just go through Collectors class and try to keep them in mind.
5 Core stream operations
Stream abstraction have a long list of useful functions for you. I am not going to cover them all, but I plan here to list down all most important ones, which you must know first hand.
Before moving ahead, lets build a collection of String beforehand. We will build out example on this list, so that it is easy to relate and understand.
List<String> memberNames = new ArrayList<>();
memberNames.add("Amitabh");
memberNames.add("Shekhar");
memberNames.add("Aman");
memberNames.add("Rahul");
memberNames.add("Shahrukh");
memberNames.add("Salman");
memberNames.add("Yana");
memberNames.add("Lokesh");
These core methods have been divided into 2 parts given below:
5.1 Intermediate operations
Intermediate operations return the stream itself so you can chain multiple method calls in a row. Let’s learn important ones.
5.1.1 Stream.filter()
Filter accepts a predicate to filter all elements of the stream. This operation is intermediate which enables us to call another stream operation (e.g. forEach) on the result.
memberNames.stream().filter((s) -> s.startsWith("A"))
.forEach(System.out::println);
Output:
Amitabh
Aman
5.1.2 Stream.map()
The intermediate operation map converts each element into another object via the given function. The following example converts each string into an upper-cased string. But you can also use map to transform each object into another type.
memberNames.stream().filter((s) -> s.startsWith("A"))
.map(String::toUpperCase)
.forEach(System.out::println);
Output:
AMITABH
AMAN
5.1.3 Stream.sorted()
Sorted is an intermediate operation which returns a sorted view of the stream. The elements are sorted in natural order unless you pass a custom Comparator.
memberNames.stream().sorted()
.map(String::toUpperCase)
.forEach(System.out::println);
Output:
AMAN
AMITABH
LOKESH
RAHUL
SALMAN
SHAHRUKH
SHEKHAR
YANA
Keep in mind that sorted does only create a sorted view of the stream without manipulating the ordering of the backed collection. The ordering of memberNames is untouched.
5.2 Terminal operations
Terminal operations return a result of a certain type instead of again a Stream.
5.2.1 Stream.forEach()
This method helps in iterating over all elements of a stream and perform some operation on each of them. The operation is passed as lambda expression parameter.
memberNames.forEach(System.out::println);
5.2.2 Stream.collect()
collect() method used to receive elements from a steam and store them in a collection and mentioned in parameter function.
List<String> memNamesInUppercase = memberNames.stream().sorted()
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.print(memNamesInUppercase);
Outpout: [AMAN, AMITABH, LOKESH, RAHUL, SALMAN, SHAHRUKH, SHEKHAR, YANA]
5.2.3 Stream.match()
Various matching operations can be used to check whether a certain predicate matches the stream. All of those operations are terminal and return a boolean result.
boolean matchedResult = memberNames.stream()
.anyMatch((s) -> s.startsWith("A"));
System.out.println(matchedResult);
matchedResult = memberNames.stream()
.allMatch((s) -> s.startsWith("A"));
System.out.println(matchedResult);
matchedResult = memberNames.stream()
.noneMatch((s) -> s.startsWith("A"));
System.out.println(matchedResult);
Output:
true
false
false
5.2.4 Stream.count()
Count is a terminal operation returning the number of elements in the stream as a long.
long totalMatched = memberNames.stream()
.filter((s) -> s.startsWith("A"))
.count();
System.out.println(totalMatched);
Output: 2
5.2.5 Stream.reduce()
This terminal operation performs a reduction on the elements of the stream with the given function. The result is an Optional holding the reduced value.
Optional<String> reduced = memberNames.stream()
.reduce((s1,s2) -> s1 + "#" + s2);
reduced.ifPresent(System.out::println);
Output: Amitabh#Shekhar#Aman#Rahul#Shahrukh#Salman#Yana#Lokesh
6 Parallelism in Java Steam
Before Java 8, parallelization was complex. Emerging of the ExecutorService and the ForkJoin simplified developer’s life a little bit, but they still should keep in mind how to create a specific executor, how to run it and so on. Java 8 introduced a way of accomplishing parallelism in a functional style.
The API allows creating parallel streams, which perform operations in a parallel mode. When the source of a stream is a Collection or an array it can be achieved with the help of the parallelStream() method:
Stream<Product> streamOfCollection = productList.parallelStream();
boolean isParallel = streamOfCollection.isParallel();
boolean bigPrice = streamOfCollection
.map(product -> product.getPrice() * 12)
.anyMatch(price -> price > 200);
If the source of stream is something different than a Collection or an array, the parallel() method should be used:
IntStream intStreamParallel = IntStream.range(1, 150).parallel();
boolean isParallel = intStreamParallel.isParallel();
Under the hood, Stream API automatically uses the ForkJoin framework to execute operations in parallel. By default, the common thread pool will be used and there is no way (at least for now) to assign some custom thread pool to it. This can be overcome by using a custom set of parallel collectors.
When using streams in parallel mode, avoid blocking operations and use parallel mode when tasks need the similar amount of time to execute (if one task lasts much longer than the other, it can slow down the complete app’s workflow).
The stream in parallel mode can be converted back to the sequential mode by using the sequential() method:
IntStream intStreamSequential = intStreamParallel.sequential();
boolean isParallel = intStreamSequential.isParallel();
参考:
《Java 8 Stream API》
《Java 8 - Streams》
《The Java 8 Stream API Tutorial》