Java series: Java Stream API – common Q&A
In the following article we will go through the Java Streams API. We will tackle streams creation, and the operations allowed on streams. The post will also address streams application and common questions about Streams. Each answer provides a supporting example.
1. What is a stream in Java?
A stream in java is a sequence of data, where the data isn’t generated at the beginning of the execution, but it is created when it is needed. Unlike lists, where we can access each element at any time, each of the elements in the stream will be processed once and we cannot access it anymore. Streams are more expressive than lists, because we only specify our intent in the stream pipeline, unlike with other structures, where we have to describe the way in which the result is formed.
A stream pipeline is the method concatenation on a stream i.e. the set of operations that will be executed on the stream. This is because streams offer a natural way to apply functions to the stream, by using lambdas. Both streams and lambdas were introduced in Java 8 and by combining them, we can manage to accomplish a lot of things with just a few lines of code.
2. Creating streams in Java
Let’s start with creating a stream of elements. The package that provides the utilities for working with streams is called java.util.stream. By using a simple import we can use all of the functions defined for the streams.
import java.util.stream.*;
The following example shows how to create an empty stream:
Stream<String> stream=Stream.empty();
We can also create a stream with a finite number of elements:
Stream<String> stream=Stream.of("one");
The of() method signature is:
Streams.of(T… values)
which means that it accepts varargs as a parameter, so we can create a stream with multiple elements. Varargs (…) is short for variable-length-arguments and this means, that the method can take 0,1 or more (comma separated) values as a parameter.
Stream<Integer> streamInt=Stream.of(1); Stream<String> stream=Stream.of("one","two","three","four");
Since streams are fairly new, the most common way to create a stream is by using an already created list. The list interface has added two new methods to support transforming lists to streams:
Stream<Object> stream=list.stream(); Stream<Object> stream=list.parallelStream();
Parallel streams need to be used carefully, because by creating a parallel stream we create an additional overhead for synchronizing and delegating the stream elements.
In order to add elements to a stream that is already created we can use two different approaches. The first one uses the concat() method and concatenates two streams into a resulting stream. If we want to add only a single element to the stream, we still have to create a single element stream:
Stream<String> result=Stream.concat(stream,Stream.of("five"));
Streams can be finite or infinite. Infinite streams only know how to generate the next element in the stream and the element will be generated only if needed.
Stream<Double> doubleStream=Stream.generate(Math::random); Stream<Integer> stream=Stream.iterate(2,i->i+2);
The lines above generate infinite streams and if we try to print all of the objects in the stream, the program will run forever. In order to avoid that, we can use the limit() method, which will return a finite stream with only the first N elements of the infinite stream. In the first stream we use the random function, which will generate a stream of random double numbers. The second example creates a stream in which we specify the first element (this is the first parameter or seed) and the second parameter in the method is a lambda function, which will be used to create the successive elements. In this case, each member will be greater by two than the previous element.
3. The Stream Pipeline
Each of the methods in the stream pipeline processes one stream element and passes it to the next method and each element is processed only ones. To perform a stream pipeline of operations on a stream of elements, we need the source of the stream, zero or more intermediate operations and a terminal operation. We can filter, sort or modify the input stream with the intermediate operations, while the terminal operations form the output. A crucial terminal operation is the reduction, which results in a single primitive or a single object.
We will look at some of the most used operations on streams:
a. count()
Count is a reduction because it takes all of the elements in the stream and returns a single long value. The method has no parameters and can be used in the following manner:
long count=stream.count();
It is important to know that the count() method works only on finite streams, for infinite streams the program will never end.
b. min() and max()
The methods min and max allow passing a comparator to the method and according to the order declared in the comparator, the pipeline will return the minimum (or maximum) element. Both methods are reductions because they return a single element. Because the minimum or the maximum value may not be present, since a stream can be empty, the return value for the methods is Optional.
The Optional<T> object can contain an object or not and we can simply check that before extracting the object:
Optional<String> optional=streams.max(String::compareTo); if(optional.isPresent()){ System.out.println(optional.get()); }
In this case, the comparator is specified by using the compareTo function defined in the String class. The “::” (double colon) operator is used to reference methods in Java 8, when we want to use the method as an argument, which requires a functional interface.
c. findAny() and findFirst()
Find any is useful for parallel streams. It gives Java the freedom to find any occurrence of the element in the stream and return a and return the element that has been found. These methods are not reductions because they don’t have to iterate the whole stream, they just return the first/any of the elements.
Both methods return an Optional object, that may be empty since the stream might not contain the element that we search for. The output for the following example is 2.
Stream<Integer> stream = Stream.of(2, 4, 6, 8, 10); stream.findAny().ifPresent(out::println);
d. allMatch(),anyMatch(), noneMatch()
The methods allMatch, anyMatch and noneMatch search the stream according to the predicate that is passed to the method. They are not reductions (for the same reasons as find methods) because they do no look at all elements necessarily.
boolean anyMatch(Predicate <? super T> predicate)
e. ForEach()
The forEach method loops through the elements of the stream. If the stream is infinite the method does not terminate. This method is the only method that is a terminal operation and returns void i.e. nothing. Also, it is mostly used when we want to print the contents of the stream by passing the method System.out::print as an argument to the forEach method.
void forEach(Consumer <? super T> action)
It is important to mention that for loops are not available for streams, so the following won’t compile:
Stream<String> stream=list.stream(); for(String s: stream){ System.out.println(s); }
The correct syntax for printing out the stream elements is:
stream.forEach(System.out::println);
f. reduce()
A reduction method, that returns a single value as a result from the stream pipeline. Useful examples would be string concatenation and multiplication.
Stream<Integer> stream=Stream.of(2,4,6,8,10); Integer result=stream.reduce(1,(a,b)->a*b); System.out.println(result);
The example above will print out 3840, because (a,b)->a*b instructs the compiler to multiply all of the elements in the stream.
g. collect()
The collect() method is a special kind of reduction, which is called mutable reduction. This is because the collect method allows us to specify the way that we want the elements of the stream to be collected and also, the output of the collect operation as well. This method allows us to convert streams into different data structures and is one of the most useful methods provided in the API. The method takes three input paramethers: The first one is a Supplier, while the other two arguments are of type BiConsumer. Common usages include converting a stream to an ArrayList or a StringBuilder. Following is a simple example for the collect method:
Stream stream = Stream.of("a", "b", "c", "c","d"); StringBuilder concatenated= stream.collect(StringBuilder::new, StringBuilder::append, StringBuilder:append)
The first argument is a Supplier i.e. the structure that will hold the result, the second argument describes how to add a single element to the result and the third argument describes how to add whole data structures to the result.
The collect() method also offers an overloaded method signature, which accepts only one parameter. The example below shows how can we get a list from a stream using the collect() method:
Stream<Integer> stream=Stream.of(2,4,6,8,10); List<Integer> list = stream.collect(Collectors.toList());
With the collect() method, we can add additional logic by creating a more complex Collector object i.e. the object we pass as an argument. The method Collector.of() takes a varargs argument and we can pass as many lambdas as we need.
List<String> strings = new ArrayList<>(); strings.add("one"); strings.add("two"); strings.add("three"); String output = strings.stream().collect(Collector.of( StringBuilder::new, (sb, str) -> sb.append(str).append(":" + str.length()+", "), StringBuilder::append, StringBuilder::toString));
The last example shows how can we get any type of a Collection from a stream:
Stream<Integer> stream = Stream.of(2, 4, 6, 8, 10); HashSet<Integer> hashSet=stream.collect(Collectors.toCollection(HashSet::new));
h. filter()
The filter method returns a stream of elements that match the expression, passed as an argument to the method. Here we give an example that will filter the stream so that only even numbers are printed out.
Stream<Integer> stream=Stream.of(2,3,4,5,6,7,8,9); stream.filter(x->x%2==0).forEach(System.out::println);
i. Limit() and skip()
The limit method retains the first n elements of the first, where n is the argument passed to the method. As mentioned above, this method can be used for both finite and infinite streams. The result of the limit method is always a finite stream. The skip() method does the opposite, it skips the first n elements and in the resulting stream we have all but the first n elements. We can easily get the first 10 even numbers using limit:
Stream<Integer> stream=Stream.iterate(2,n->n*2); stream.limit(10).forEach(System.out::println);
j. sorted()
Takes the elements in the stream and sorts them by the default sorting order for the data type. If the stream contains String elements, they will be sorted alphabetically. The sorted method has two signatures, the first one does not take parameters, while the second one accepts a Comparator object. In the comparator object we can specify how we want the stream to be sorted. We can additionally add the method thenComparing() to comparator argument so we will specify the order for the comparators.
The following example takes a stream of students and orders them by their score, if the score is greater than 60. We also use the Comparator method reversed(), so we can have the best scores first. If two students have the same score, next we compare their names by using the Comparator method thenComparing() .
public class Student { private String name; private Integer score; private Integer age; private String groupName; // getters, setters & a constructor }
The Student class is given above. In the main method we populate a list of students and create a stream from the list:
List<Student> students = new ArrayList<>();
students.add(new Student("Jack", 60, 22, "Regular")); students.add(new Student("Jim", 80, 22, "Regular")); students.add(new Student("Annie", 83, 22, "Regular")); students.add(new Student("Rose", 46, 22, "Regular")); students.add(new Student("Maria", 55, 22, "Regular")); students.add(new Student("Daniel", 89, 22, "Regular")); students.add(new Student("Adam", 61, 22, "Regular")); students.add(new Student("Diana", 73, 22, "Regular")); students.stream(). filter(s->s.getScore()>60). sorted( Comparator.comparing(Student::getScore). reversed(). thenComparing(Student::getName)). limit(3).forEach(System.out::println);
k. peek()
The peek method does not change the stream and accesses only the first element of the stream. This is tricky, because peek will get the first element and will send it to the next method in the pipeline. The same will happen for all of the elements in the stream. The operation peek() is not a terminal operation so it won’t return anything.
We mentioned before that we can have a terminal operation without an intermediary operation, but not vice versa. If we remove the forEach() method at the end of the next example, the application will not do anything because it is missing a terminal operation.
Stream<Integer> stream = Stream.of(2, 4, 6, 8, 10); stream.peek(a -> System.out.println(a+" ")). filter(a -> a > 4). peek(a -> System.out.println("Filtered:" + a)). forEach(a->a+=3);
The output is the following:
2 4 6 Filtered:6 8 Filtered:8 10 Filtered:10
l. distinct()
The distinct method returns a stream with duplicate values removed.
Stream<Integer> stream=Stream.of(9,4,6,2,7,3,6,32,1,4,6,3,22,35,76,2,6); stream.distinct().map(a->a+" ").forEach(System.out::print);
In the example above we also used the map() method. It may be one of the most common intermediate operations. It returns a new stream consisting of the results of applying the given function to the elements of this stream. It takes a Lambda function for parameter, which is the function that is applied on the stream. The result of the execution is:
9 4 6 2 7 3 32 1 22 35 76
Subscribe below and never miss a story from our Java series: “Good practices and recommendations”.
Cover PHOTO by Glenn Carstens-Peters on Unsplash