In this part of the Java tutorial, we will work with streams. Streams vastly improve the processing of data in Java.
Java stream definition
is a sequence of elements from a source that supports sequential and parallel aggregate operations. The common aggregate operations are: filter, map, reduce, find, match, and sort. The source can be a collection, IO operation, or array, which provides data to a stream.
A Java collection is an in-memory data structure with all elements contained within memory while a stream is a data structure with all elements computed on demand. In contrast to collections, which are iterated explicitly (external iteration), stream operations do the iteration behind the scenes for us. Since Java 8, Java collections have a stream()
method that returns a stream from a collection.
The Stream
interface is defined in java.util.stream
package.
An operation on a stream produces a result without modifying its source.
Characteristics of a stream
- Streams do not store data; rather, they provide data from a source such as a collection, array, or IO channel.
- Streams do no modify the data source. They transform data into a new stream, for instance, when doing a filtering operation.
- Many stream operations are lazily-evaluated. This allows for automatic code optimizations and short-circuit evaluation.
- Stream can be infinite. Method such as
limit()
allow us to get some result from infinite streams. - The elements of a stream can be reached only once during the life of a stream. Like an
Iterator
, a new stream must be generated to revisit the same elements of the source. - Streams have methods, such as
forEach()
andforEachOrdered()
, for internal iteration of stream elements. - Streams support SQL-like operations and common functional operations, such as filter, map, reduce, find, match, and sorted.
Java stream pipeline
A stream pipeline consists of a source, intermediate operations, and a terminal operation. Intermediate operations return a new modified stream; therefore, it is possible to chain multiple intermediate operations. Terminal operations, on the other hand, return void or a value. After a terminal operation it is not possible to work with the stream anymore. Short-circuiting a terminal operation means that the stream may terminate before all values are processed. This is useful if the stream is infinite.
Intermediate operations are lazy. They will not be invoked until the terminal operation is executed. This improves the performance when we are processing larger data streams.
Java creating streams
Streams are created from various sources such as collections, arrays, strings, IO resources, or generators.
package com.zetcode; import java.util.Arrays; import java.util.List; import java.util.stream.IntStream; import java.util.stream.Stream; public class CreatingStreams { public static void main(String[] args) { List<String> words = Arrays.asList("pen", "coin", "desk", "chair"); String word = words.stream().findFirst().get(); System.out.println(word); Stream<String> letters = Arrays.stream(new String[]{ "a", "b", "c"}); System.out.printf("There are %d letters%n", letters.count()); String day = "Sunday"; IntStream istr = day.codePoints(); String s = istr.filter(e -> e != 'n').collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append).toString(); System.out.println(s); } }
In this example, we work with streams created from a list, array, and a string.
List<String> words = Arrays.asList("pen", "coin", "desk", "chair");
A list of strings is created.
String word = words.stream().findFirst().get();
With the stream
method, we create a stream from a list collection. On the stream, we call the findFirst()
method which returns the first element of the stream. (It returns an Optional
from which we fetch the value with the get()
method.)
Stream<String> letters = Arrays.stream(new String[]{ "a", "b", "c"}); System.out.printf("There are %d letters%n", letters.count());
We create a stream from an array. The count()
method of the stream returns the number of elements in the stream.
String day = "Sunday"; IntStream istr = day.codePoints(); String s = istr.filter(e -> e != 'n').collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append).toString(); System.out.println(s);
Here we create a stream from a string. We filter the characters and build a new string from the filtered characters.
$ java com.zetcode.CreatingStreams pen There are 3 letters Suday
This is the output.
There are three Stream
specializations: IntStream
, DoubleStream
, and LongStream
.
package com.zetcode; import java.util.stream.DoubleStream; import java.util.stream.IntStream; import java.util.stream.LongStream; public class CreatingStreams2 { public static void main(String[] args) { IntStream integers = IntStream.rangeClosed(1, 16); System.out.println(integers.average().getAsDouble()); DoubleStream doubles = DoubleStream.of(2.3, 33.1, 45.3); doubles.forEachOrdered(e -> System.out.println(e)); LongStream longs = LongStream.range(6, 25); System.out.println(longs.count()); } }
The example works with the three aforementioned specializations.
IntStream integers = IntStream.rangeClosed(1, 16); System.out.println(integers.average().getAsDouble());
A stream of integers is created with the IntStream.rangeClosed()
method. We print their average to the console.
DoubleStream doubles = DoubleStream.of(2.3, 33.1, 45.3); doubles.forEachOrdered(e -> System.out.println(e));
A stream of double values is created with the DoubleStream.of()
method. We print the ordered list of elements to the console utilizing the forEachOrdered()
method.
LongStream longs = LongStream.range(6, 25); System.out.println(longs.count());
A strem of long integers is created with the LongStream.range()
method. We print the count of the elements with the count()
method.
$ java com.zetcode.CreatingStreams2 8.5 2.3 33.1 45.3 19
This is the output of the example.
The Stream.of()
method returns a sequential ordered stream whose elements are the specified values.
package com.zetcode; import java.util.Comparator; import java.util.stream.Stream; public class CreatingStreams3 { public static void main(String[] args) { Stream<String> colours = Stream.of("red", "green", "blue"); String col = colours.skip(2).findFirst().get(); System.out.println(col); Stream<Integer> nums = Stream.of(3, 4, 5, 6, 7); int maxVal = nums.max(Comparator.naturalOrder()).get(); System.out.println(maxVal); } }
In the example, we create two streams with the Stream.of()
method.
Stream<String> colours = Stream.of("red", "green", "blue");
A stream of three strings is created.
String col = colours.skip(2).findFirst().get();
With the skip()
method, we skip two elements and find the only one left with the findFirst()
method.
Stream<Integer> nums = Stream.of(3, 4, 5, 6, 7); int maxVal = nums.max(Comparator.naturalOrder()).get();
We create a stream of integers and find its maximum number.
$ java com.zetcode.CreatingStreams3 blue 7
This is the output.
Other methods to create streams are: Stream.iterate()
and Stream.generate()
.
package com.zetcode; import java.util.Random; import java.util.stream.Stream; public class CreatingStreams4 { public static void main(String[] args) { Stream<Integer> s1 = Stream.iterate(5, n -> n * 2).limit(10); s1.forEach(System.out::println); Stream.generate(new Random()::nextDouble) .map(e -> (e * 10)) .limit(5) .forEach(System.out::println); } }
In the example, we create two streams with Stream.iterate()
and Stream.generate()
.
Stream<Integer> s1 = Stream.iterate(5, n -> n * 2).limit(10); s1.forEach(System.out::println);
The Stream.iterate()
returns an infinite sequential ordered stream produced by iterative application of a function to an initial element. The initial element is called a seed. The second element is generated by applying the function to the first element. The third element is generated by applying the function on the second element etc.
Stream.generate(new Random()::nextDouble) .map(e -> (e * 10)) .limit(5) .forEach(System.out::println);
A stream of five random doubles is created with the Stream.generate()
method. Each of the elements is multiplied by ten. In the end, we iterate over the stream and print each element to the console.
$ java com.zetcode.CreatingStreams4 5 10 20 40 80 160 320 640 1280 2560 8.704675577530493 5.732011478196306 3.8978402578067515 3.6986033299500933 6.0976417139147205
This is the output.
It is possible to create a stream from a file.
package com.zetcode; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.stream.Stream; public class CreatingStreams5 { public static void main(String[] args) throws IOException { Path path = Paths.get("/home/janbodnar/myfile.txt"); Stream<String> stream = Files.lines(path); stream.forEach(System.out::println); } }
The example reads a file and prints its contents using streams.
Path path = Paths.get("/home/janbodnar/myfile.txt");
A Path
object is created with Paths.get()
method. A Path
object is used to locate a file in a file system.
Stream<String> stream = Files.lines(path);
From the path, we create a stream using Files.lines()
method; each of the elements of the stream is a line.
stream.forEach(System.out::println);
We go through the elements of the stream and print them to the console.
Internal and external iteration
Depending on who controls the iteration process, we distinguish between external and internal iteration. External iteration, also known as active or explicit iteration, is handled by the programmer. Until Java 8, it was the only type of iteration in Java. For external iteration, we use for and while loops. Internal iteration, also called passive or implicit iteration, is controlled by the iterator itself. Internal iteration is available in Java streams.
package com.zetcode; import java.util.Arrays; import java.util.Iterator; import java.util.List; public class ExternalIteration { public static void main(String[] args) { List<String> words = Arrays.asList("pen", "coin", "desk", "eye", "bottle"); Iterator it = words.iterator(); while (it.hasNext()) { System.out.println(it.next()); } } }
In the code example, we retrieve and iterator object from a list of strings. Using iterator's hasNext()
and next()
method in a while loop, we iterate over the elements of the list.
In the following example, we iterate the same list using an external iteration.
package com.zetcode; import java.util.Arrays; import java.util.List; public class InternalIteration { public static void main(String[] args) { List<String> words = Arrays.asList("pen", "coin", "desk", "eye", "bottle"); words.stream().forEach(System.out::println); } }
In the example, we create a stream from a list. We use the stream's forEach()
to internally iterate over the stream elements.
Java stream filtering
Filtering streams of data is one of the most important abilities of streams. The filter()
method is an intermediate operation which returns a stream consisting of the elements of a stream that match the given predicate. A predicate is a method that returns a boolean value.
package com.zetcode; import java.util.Arrays; import java.util.stream.IntStream; public class FilterStream { public static void main(String[] args) { IntStream nums = IntStream.rangeClosed(0, 25); int[] vals = nums.filter(e -> e > 15).toArray(); System.out.println(Arrays.toString(vals)); } }
The code example creates a stream of integers. The stream is filtered to contain only values greater than fifteen.
IntStream nums = IntStream.rangeClosed(0, 25);
With IntStream
, a stream of twenty six integers is created. The rangeClose()
method creates a stream of integers from a bound of two values; these two values (start and end) are both included in the range.
int[] vals = nums.filter(e -> e > 15).toArray();
We pass a lambda expression (e -> e > 15
) into the filter()
function; the expression returns true for values greater than 15. The toArray()
is a terminal operation that transforms a stream into an array of integers.
System.out.println(Arrays.toString(vals));
The array is printed to the console.
$ java com.zetcode.FilterStream [16, 17, 18, 19, 20, 21, 22, 23, 24, 25]
The example produces this output.
The next example produces a list of event numbers.
package com.zetcode; import java.util.stream.IntStream; public class FilterStream2 { public static void main(String[] args) { IntStream nums = IntStream.rangeClosed(0, 30); nums.filter(FilterStream2::isEven).forEach(System.out::println); } private static boolean isEven(int e) { return e % 2 == 0; } }
To get even numbers from a stream, we pass an isEven()
method reference to the filter()
method.
nums.filter(FilterStream2::isEven).forEach(System.out::println);
The double colon (::) operator is used to pass a method reference. The forEach()
method is a terminal operation that iterates over the elements of the stream. It takes a method reference to the System.out.println()
method.
Skipping and limiting elements
The skip(n)
method skip the first n elements of the stream and the limit(m)
method limits the number of elements in the stream to m.
package com.zetcode; import java.util.stream.IntStream; public class SkipLimit { public static void main(String[] args) { IntStream s = IntStream.range(0, 15); s.skip(3).limit(5).forEach(System.out::println); } }
The example creates a stream of fifteen integers. We skip the first three elements with the skip()
method and limit the number of elements to ten values.
$ java com.zetcode.SkipLimit 3 4 5 6 7
This is the output of the example.
Java stream sorting elements
The sorted()
method sorts the elements of this stream, according to the provided Comparator
.
package com.zetcode; import java.util.Comparator; import java.util.stream.IntStream; public class Sorting { public static void main(String[] args) { IntStream nums = IntStream.of(4, 3, 2, 1, 8, 6, 7, 5); nums.boxed().sorted(Comparator.reverseOrder()) .forEach(System.out::println); } }
The example sorts integer elements in a descending order. The boxed()
method converts IntStream
to Stream<Integer>
.
$ java com.zetcode.Sorting 8 7 6 5 4 3 2 1
This is the output of the example.
The next example shows how to compare a stream of objects.
package com.zetcode; import java.util.Arrays; import java.util.Comparator; import java.util.List; class Car { private String name; private int price; public Car(String name, int price ) { this.name = name; this.price = price; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getPrice() { return price; } public void setPrice(int price) { this.price = price; } @Override public String toString() { return "Car{" + "name=" + name + ", price=" + price + '}'; } } public class Sorting2 { public static void main(String[] args) { List<Car> cars = Arrays.asList(new Car("Citroen", 23000), new Car("Porsche", 65000), new Car("Skoda", 18000), new Car("Volkswagen", 33000), new Car("Volvo", 47000)); cars.stream().sorted(Comparator.comparing(Car::getPrice)) .forEach(System.out::println); } }
The example sorts cars by their price.
List<Car> cars = Arrays.asList(new Car("Citroen", 23000), new Car("Porsche", 65000), new Car("Skoda", 18000), new Car("Volkswagen", 33000), new Car("Volvo", 47000));
A list of cars is created.
cars.stream().sorted(Comparator.comparing(Car::getPrice)) .forEach(System.out::println);
A stream is generated from a list using the stream()
method. We pass a reference to the Car's
getPrice()
method, which is used when comparing cars by their price.
$ java com.zetcode.Sorting2 Car{name=Skoda, price=18000} Car{name=Citroen, price=23000} Car{name=Volkswagen, price=33000} Car{name=Volvo, price=47000} Car{name=Porsche, price=65000}
This is the output of the example.
Java stream unique values
The distinct()
method returns a stream consisting of unique elements.
package com.zetcode; import java.util.Arrays; import java.util.stream.IntStream; public class UniqueElements { public static void main(String[] args) { IntStream nums = IntStream.of(1, 1, 3, 4, 4, 6, 7, 7); int a[] = nums.distinct().toArray(); System.out.println(Arrays.toString(a)); } }
The example removes duplicate values from a stream of integers.
IntStream nums = IntStream.of(1, 1, 3, 4, 4, 6, 7, 7);
There are three duplicate values in the stream.
int a[] = nums.distinct().toArray();
We remove the duplicates with the distinct()
method.
$ java com.zetcode.UniqueElements [1, 3, 4, 6, 7]
This is the output of the example.
Java stream mapping
It is possible to change elements into a new stream; the original source is not modified. The map()
method returns a stream consisting of the results of applying the given function to the elements of a stream. The map()
is an itermediate operation.
package com.zetcode; import java.util.Arrays; import java.util.stream.IntStream; public class Mapping { public static void main(String[] args) { IntStream nums = IntStream.of(1, 2, 3, 4, 5, 6, 7, 8); int[] squares = nums.map(e -> e * e).toArray(); System.out.println(Arrays.toString(squares)); } }
We map a transformation function on each element of the stream.
int[] squares = nums.map(e -> e * e).toArray();
We apply a lamda expression (e -> e * e
) on the stream: each of the elements is squared. A new stream is created which is transformed into an array with the toArray()
method.
$ java com.zetcode.Mapping [1, 4, 9, 16, 25, 36, 49, 64]
This is the output of the example.
In the next example, we transform a stream of strings.
package com.zetcode; import java.util.stream.Stream; public class Mapping2 { public static void main(String[] args) { Stream<String> words = Stream.of("cardinal", "pen", "coin", "globe"); words.map(Mapping2::capitalize).forEach(System.out::println); } private static String capitalize(String word) { word = word.substring(0, 1).toUpperCase() + word.substring(1).toLowerCase(); return word; } }
We have a stream of strings. We capitalize each of the strings of the stream.
words.map(Mapping2::capitalize).forEach(System.out::println);
We pass a reference to the capitalize()
method to the map()
method.
$ java com.zetcode.Mapping2 Cardinal Pen Coin Globe
This is the output of the example.
Java stream reductions
A is a terminal operation that aggregates a stream into a type or a primitive.
package com.zetcode; import java.util.stream.IntStream; public class Reduction { public static void main(String[] args) { IntStream nums = IntStream.of(1, 2, 3, 4, 5, 6, 7, 8); int maxValue = nums.max().getAsInt(); System.out.printf("The maximum value is: %d%n", maxValue); } }
Getting a maximum value from a stream of integers is a reduction operations.
int maxValue = nums.max().getAsInt();
With the max()
method, we get the maximum element of the stream. The method returns an Optional
from which we get the integer using the getAsInt()
method.
$ java com.zetcode.Reduction The maximum value is: 8
This is the output of the example.
A custom reduction can be created with the reduce()
method.
package com.zetcode; import java.util.stream.IntStream; public class Reduction2 { public static void main(String[] args) { IntStream nums = IntStream.of(1, 2, 3, 4, 5, 6, 7, 8); int product = nums.reduce((a, b) -> a * b).getAsInt(); System.out.printf("The product is: %d%n", product); } }
The example returns a product of the integer elements in the stream.
$ java com.zetcode.Reduction The product is: 40320
This is the output of the example.
Java stream collection operations
A is a terminal reduction operation which reduces elements of a stream into a Java collection, string, value, or specific grouping.
package com.zetcode; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; class Car { private String name; private int price; public Car(String name, int price) { this.name = name; this.price = price; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getPrice() { return price; } public void setPrice(int price) { this.price = price; } @Override public String toString() { return "Car{" + "name=" + name + ", price=" + price + '}'; } } public class Collecting { public static void main(String[] args) { List<Car> cars = Arrays.asList(new Car("Citroen", 23000), new Car("Porsche", 65000), new Car("Skoda", 18000), new Car("Volkswagen", 33000), new Car("Volvo", 47000)); List<String> names = cars.stream().map(Car::getName) .filter(name -> name.startsWith("Vo")) .collect(Collectors.toList()); for (String name: names) { System.out.println(name); } } }
The example creates a stream from a list of car object, filters the cars by the their name, and returns a list of matching car names.
List<String> names = cars.stream().map(Car::getName) .filter(name -> name.startsWith("Vo")) .collect(Collectors.toList());
At the end of the pipeline, we use the collect()
method to transform
$ java com.zetcode.Collecting Volkswagen Volvo
This is the output of the example.
In the next example, we use the collect()
method to group data.
package com.zetcode; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; public class Collecting2 { public static void main(String[] args) { List<String> items = Arrays.asList("pen", "book", "pen", "coin", "book", "desk", "book", "pen", "book", "coin"); Map<String, Long> result = items.stream().collect( Collectors.groupingBy( Function.identity(), Collectors.counting() )); for (Map.Entry<String, Long> entry : result.entrySet()) { String key = entry.getKey(); Long value = entry.getValue(); System.out.format("%s: %d%n", key, value); } } }
The code example groups elements by their occurrence in a stream.
Map<String, Long> result = items.stream().collect( Collectors.groupingBy( Function.identity(), Collectors.counting() ));
With the Collectors.groupingBy()
method, we count the occurrences of the elements in the stream. The operation returns a map.
for (Map.Entry<String, Long> entry : result.entrySet()) { String key = entry.getKey(); Long value = entry.getValue(); System.out.format("%s: %d%n", key, value); }
We go through the map and print its key/value pairs.
$ java com.zetcode.Collecting2 desk: 1 book: 4 pen: 3 coin: 2
This is the output of the example.
This part of the Java tutorial covered streams.