Java 8 comes with some prominent features like Lambda Expressions, Method References. And Streams are also an important concept that we should comprehend.
This tutorial will help you have a deep view of Java 8 Streams: what they are, ways to create them, how they work with intermediate operations, terminal operation…
I. Overview
1. What is Java 8 Stream?
A stream is an abstract concept that represents a sequence of objects created by a source, it’s neither a data structure nor a collection object where we can store items. So we can’t point to any location in the stream, we just interact with items by specifying the functions.
This is an example of a Stream:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9); // get List from Stream Operation List<String> result = numbers.stream() .filter(i -> (i % 2) == 0) .map(i -> "[" + i + "]") .collect(Collectors.toList()); System.out.println(result); |
Run the code above, the console shows:
[[2], [4], [6], [8]] |
Now, we have concept of using a Stream is to enable functional-style operations on streams of elements. Those operations are composed into a stream pipeline which consists of:
Source
> Intermediate Operations
> Terminal Operation
– a source (in the example, it is a collection – List, but it is also an array, a generator function, an I/O channel…)
– intermediate operations (which transform current stream into another stream at the current chain, in the example, filter is the first operation and map is the second one)
– a terminal operation (which produces a result or side-effect, in the example, it is collect)
We will dive deeper into those things in the next parts of this tutorial.
2. Ways to create a Java 8 Stream
We can obtain a stream in many ways with various kind of sources:
2.1 Use stream() & parallelStream()
– From a Collection via the stream() and parallelStream() methods. The example above created a stream by this way:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9); Stream<Integer> streamInt = numbers.stream(); |
– From an array via Arrays.stream(Object[]):
int[] arr_numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; IntStream streamArr = Arrays.stream(arr_numbers); |
2.2 Use static factory methods Stream.of()
– From static factory methods on the stream classes: Stream.of(Object[]), IntStream.range(int, int), Stream.iterate(Object, UnaryOperator)…
Stream<String> streamOf = Stream.of("java", "sample", "approach", ".com"); IntStream streamRange = IntStream.range(0, 10); Stream<String> streamIter = Stream.iterate(0, n -> n + 3).limit(10); Stream<String> emptyStream = Stream.empty(); Stream<Double> streamGen = Stream.generate(Math::random).limit(10); |
2.3 Java 8 Stream with Files
– From methods in java.nio.file.Files:
try { long numberWords = java.nio.file.Files .lines(Paths.get("file.txt"), Charset.defaultCharset()) .flatMap(line -> Arrays.stream(line.split(" ."))).distinct().count(); } catch (IOException e) { System.out.println("IOException when reading or getting data from file"); } |
2.4 Use methods in the JDK
– From methods in the JDK:
String name = "Java Sample Approach"; Stream<String> streamWords = Pattern.compile("\\W").splitAsStream(name); |
II. How a Java 8 Stream works?
1. Lazy
Streams are lazy. The Stream mechanism make all computations to the source data are only performed when the terminal operation is executed, and source elements are consumed only when they are needed.
Let’s return to the first example to understand how lazy it is.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9); // get List from Stream Operation List<String> result = numbers.stream() .filter(i -> (i % 2) == 0) .map(i -> "[" + i + "]") .collect(Collectors.toList()); System.out.println(result); |
Now we separate the terminal operation method collect into new statement, and insert a timer to delay the time after calling method map:
List<Integer> newNumbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9); Stream<Integer> lazyStream = newNumbers.stream() .filter(i -> { System.out.println("filter: [" + i + "]"); return (i % 2) == 0; }) .map(i -> { System.out.println("map: [" + i + "]"); return i; }); for (int i = 1; i <= 3; i++) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("running... " + i + " sec"); } lazyStream.collect(Collectors.toList()); |
Check the result:
running... 1 sec running... 2 sec running... 3 sec filter: [1] filter: [2] map: [2] filter: [3] filter: [4] map: [4] filter: [5] filter: [6] map: [6] filter: [7] filter: [8] map: [8] filter: [9] |
We can determine 2 clear things now:
– System only activates the command inside filter and map method:
{ System.out.println(...); return ...;}
after we call collect method.
– The intermediate operation, such as filter and map method, runs for each item which is passed into it immediately, and this operation is independent over the quantity of stream elements. If a item passes filter, it will come into the next method map without waiting for any item in the stream:
.....[5][4][3][2][1] stream >> intermediate operations .....
..[1] >> filter not pass
..[2] >> filter pass >> come into map without waiting for [3] to examine
..[3] >> filter not pass
..[4] >> filter pass >> come into map without waiting for [5] to examine
They all show the laziness of Stream’s behaviour.
2. Intermediate Operations
With Java 8 Stream, each intermediate operation return another Stream which allows us to call next operation in a sequence. All operations of this kind will not be executed until a terminal operation is invoked.
2.1 Java 8 – filter
Stream<T> filter(Predicate<? super T> predicate)
filter returns a new stream which elements match the given predicate.
The example gets a new stream with even numbers from list of numbers:
List<Integer> ftnumbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9); Stream<Integer> ftstream = ftnumbers.stream().filter(i -> (i % 2) == 0); |
2.2 Java 8 – map
<R> Stream<R> map(Function<? super T, ? extends R> mapper)
map returns a stream which elements are the results of applying the given transform function.
The example gets a new stream which all number items are transformed from Integer to String.
List<Integer> mapnumbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9); Stream<String> mapstream = mapnumbers.stream().map(i -> i.toString()); |
2.3 Java 8 – flatMap
<R> Stream<R> flatMap(Function<? super T,? extends Stream<? extends R>> mapper)
map is can only transform ONE object of a stream into exactly ONE object of another stream. But what if we wanna transform ONE into MORE or even NONE object?
flatMap can do that. Each object will be transformed into zero, one or multiple objects under streams’ formation.
class Foo { public String name; public List<String> bars = new ArrayList<>(); public Foo(String name, List<String> bars) { super(); this.name = name; this.bars = bars; } } List<Foo> fooList = new ArrayList<Foo>(); List<String> l1 = Arrays.asList("Java", "Sample", "Approach"); List<String> l2 = Arrays.asList("Java Tecnology", "Spring Framework", "Sample Code"); fooList.add(new Foo("foo1", l1)); fooList.add(new Foo("foo2", l2)); Stream<String> fooStream = fooList.stream().flatMap(foo -> { System.out.println("-- Foo: " + foo.name); return foo.bars.stream(); }); fooStream.forEach(System.out::println); |
Run the code, and the result in console:
-- Foo: foo1 Java Sample Approach -- Foo: foo2 Java Tecnology Spring Framework Sample Code |
Each Foo object is transferred into a stream of String by using flatMap method.
2.4 Java 8 – distinct
Stream<T> distinct()
distinct returns a stream consisting of the distinct elements (according to Object.equals(Object)) of this stream.
List<Integer> distnumbers = Arrays.asList(1, 1, 2, 3, 3, 4, 5, 6, 6); Stream<Integer> diststream = distnumbers.stream().distinct(); diststream.forEach(System.out::print); |
Result:
123456 |
2.5 Java 8 – sorted
Stream<T> sorted()
sorted returns a stream which elements are sorted according to natural order. If they are not Comparable, a java.lang.ClassCastException may be thrown when the terminal operation is executed.
If we wanna sort Objects with Custom Type, we can implement our own Comparator for the method:
sorted(Comparator<? super T> comparator)
List<Integer> sortnumbers = Arrays.asList(8, 9, 2, 5, 3, 4, 5, 6, 6); Stream<Integer> sortstream = sortnumbers.stream().sorted(); sortstream.forEach(System.out::print); |
Result:
234556689 |
2.6 Java 8 – limit
Stream<T> limit(long maxSize)
limit returns a stream which quantity of elements is limited to maxSize in length.
List<Integer> limnumbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9); Stream<Integer> limstream = limnumbers.stream().limit(5); limstream.forEach(System.out::print); |
Result:
12345 |
3. Terminal Operation
Terminal operation produces a result such as primitive value, a collection or a side-effect. After completing terminal operation, the stream pipeline is considered consumed, so it can no longer be used. If trying to invoke again using the consumed stream, it will throw a runtime exception:
java.lang.IllegalStateException: stream has already been operated upon or closed
Stream<String> streamSource = Stream.of("java", "sample", "approach", ".com"); // perform terminal operation by forEach method streamSource.forEach(System.out::print); // the stream is already closed, so this code will throw exception at runtime streamSource.forEach(System.out::print); |
So, if you need to traverse the same data source again, you must return to the data source to get a new stream.
3.1 Java 8 – forEach
void forEach(Consumer<? super T> action)
forEach performs an action for each stream element.
The example iterates over each element using forEach:
Stream.of("java", "sample", "approach", ".com").forEach(System.out::print); |
– toArray
Object[] toArray()
<A> A[] toArray(IntFunction<A[]> generator)
toArray returns an array containing stream elements.
Object[] objects = Stream.of(1, 2, 3, 4, 5).toArray(); String[] upstringArr = (String[]) Stream.of("java", "sample", "approach", ".com").map(s -> s.toUpperCase()) .toArray(String[]::new); |
3.2 Java 8 – Matching Method
Java 8 Streams has several matching methods that can be used for checking a provided predicate in each element of a stream. The methods’ names themselves indicate what they can do:
boolean allMatch(Predicate<? super T> predicate)
boolean anyMatch(Predicate<? super T> predicate)
boolean noneMatch(Predicate<? super T> predicate)
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9); boolean isAllNumbersLargerThanFive = numbers.stream() .allMatch(i -> i > 5); System.out.println(isAllNumbersLargerThanFive); // false boolean hasNumberLargerThanFive = numbers.stream() .anyMatch(i -> i > 5); System.out.println(hasNumberLargerThanFive); // true boolean isNoneNumberLargerThanTen = numbers.stream() .noneMatch(i -> i > 10); System.out.println(isNoneNumberLargerThanTen); // true |
3.3 Java 8 – collect
<R,A> R collect(Collector<? super T,A,R> collector)
collect transforms stream elements into another container such as a List. Java 8 supports many built-in collectors with class Collectors such as toList, groupingBy, toMap, joining. In most cases, we don’t need to implement any collectors.
List<Customer> customers = Arrays.asList( new Customer("Jack", "Smith"), new Customer("Adam", "Johnson"), new Customer("David", "Green"), new Customer("Robert", "Green")); List<Customer> filterLastName = customers.stream().filter(c -> c.getLastName().startsWith("Green")) .collect(Collectors.toList()); System.out.println(filterLastName); // [Customer[firstName='David', lastName='Green'], // Customer[firstName='Robert', lastName='Green']] Map<String, List<Customer>> groupByLastName = customers.stream() .collect(Collectors.groupingBy(c -> c.getLastName())); groupByLastName.forEach( (lastName, customer) -> System.out.println(lastName + ": " + customer)); // Johnson: [Customer[firstName='Adam', lastName='Johnson']] // Smith: [Customer[firstName='Jack', lastName='Smith']] // Green: [Customer[firstName='David', lastName='Green'], Customer[firstName='Robert', lastName='Green']] Map<String, String> lastNameMap = customers.stream() .collect(Collectors.toMap(c -> "lastName " + c.getLastName(), c -> c.getFirstName(), (firstName1, firstName2) -> firstName1 + "|" + firstName2)); System.out.println(lastNameMap); // {lastName Green=David|Robert, lastName Johnson=Adam, lastName Smith=Jack} String joining = customers.stream().filter(c -> c.getLastName().contains("Green")).map(c -> c.getFirstName()) .collect(Collectors.joining(", ", "In Customer Database: [", "] have the same lastName Green.")); System.out.println(joining); // In Customer Database: [David, Robert] have the same lastName Green. |
3.4 Java 8 – reduce
reduce processes all elements of the stream and produces a single result.
reduce(BinaryOperator<T> accumulator)
reduce(T identity, BinaryOperator<T> accumulator)
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9); numbers.stream() .reduce((i1, i2) -> i1 > i2 ? i1 : i2) .ifPresent(i ->System.out.println("max: " + i)); // max: 9 Integer total = numbers.stream().reduce(0, (i1, i2) -> i1 + i2); System.out.println("total: " + total); // total: 45 |
3.5 Java 8 – min and max
Optional
Optional
min and max return the minimum or maximum element of the stream according to the provided Comparator.
OptionalInt minimum = IntStream.of(1, 2, 3).min(); System.out.println("min: " + minimum); // min: OptionalInt[1] Optional<String> lastName = Stream.of("A", "B", "C").max(new Comparator<String>() { @Override public int compare(String o1, String o2) { return o1.compareTo(o2); } }); System.out.println(lastName); // Optional[C] |
III. Source Code
Technology:
– Java 8
– Eclipse Mars.1 Release (4.5.1)
java8stream
Last updated on March 3, 2018.