Introduction
Introduced in Java 8, the Stream API is used to process collections of objects. A stream is a sequence of objects that supports various methods which can be pipelined to produce the desired result.
Stream provides following features:
- It does not store elements. It simply conveys elements from a source such as a data structure, an array, or an I/O channel, through a pipeline of computational operations.
- It is functional in nature. Operations performed on a stream does not modify it’s source. For example, filtering a Stream obtained from a collection produces a new Stream without the filtered elements, rather than removing elements from the source collection.
- It is lazy and evaluates code only when required.
- The elements of a stream are only visited once during the life of a stream. Like an Iterator, a new stream must be generated to revisit the same elements of the source.
- Stream operations can either be executed sequential or parallel.
Collections and Stream
- 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 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 expression.
- 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.
Stream Creation
There are many ways to create a stream instance of different sources. Once created, the instance will not modify its source, therefore allowing the creation of multiple instances from a single source.
// Empty Stream, the empty() method should be used in case of a creation of an empty stream. Stream<String> streamEmpty = Stream.empty(); // Stream of Collection, Stream can also be created of any type of Collection (Collection, List, Set). Collection<String> collection = Arrays.asList("a", "b", "c"); Stream<String> streamOfCollection = collection.stream(); // Stream of Array, Array can also be a source of a Stream. Stream<String> streamOfArray = Stream.of("a", "b", "c"); // 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<Object>. Stream<String> streamBuilder = Stream.<String>builder().add("a").add("b").add("c").build(); // One of the most common ways to obtain a Stream is from a Java Collection. This example first creates a Java List, then adds three Java Strings to it. Finally, the example calls the stream() method to obtain a Stream instance. List<String> items = new ArrayList<String>(); items.add("one"); items.add("two"); items.add("three"); Stream<String> stream = items.stream();
Stream Pipeline
To perform a sequence of operations over the elements of the data source and aggregate their results, three parts are needed – the source, intermediate (non terminal) operation(s) and a terminal operation. Intermediate operations return a new modified stream. If more than one modification is needed, intermediate operations can be chained. A stream by itself is worthless, the real thing a user is interested in is a result of the terminal operation, which can be a value of some type or an action applied to every element of the stream. Only one terminal operation can be used per stream.
// Java Stream example which contains both a non-terminal and a terminal operation. The conversion of the elements to lowercase does not actually affect the count of elements. The conversion part is just there as an example of a non-terminal operation. import java.util.ArrayList; import java.util.List; import java.util.stream.Stream; public class StreamExamples { public static void main(String[] args) { List<String> stringList = new ArrayList<String>(); stringList.add("ONE"); stringList.add("TWO"); stringList.add("THREE"); Stream<String> stream = stringList.stream(); // map() method of the Stream interface is a non-terminal operation. It sets a lambda expression on the stream which converts each element to lowercase. // count() method is a terminal operation. This call starts the iteration internally, which will result in each element being converted to lowercase and then counted. long count = stream .map((value) -> { return value.toLowerCase(); }) .count(); System.out.println("count = " + count); } }
The non-terminal stream operations are operations that transform or filter the elements in the stream. When you add a non-terminal operation to a stream, you get a new stream back as result. The new stream represents the stream of elements resulting from the original stream with the non-terminal operation applied.
Intermediate Operations
- filter()
The Java Stream filter() can be used to filter out elements from a Java Stream. The filter method takes a Predicate which is called for each element in the stream. If the element is to be included in the resulting Stream, the Predicate should return true. If the element should not be included, the Predicate should return false.Stream<String> longStringsStream = stream.filter((value) -> { return value.length() >= 3; });
- map()
The Java Stream map() method converts (maps) an element to another object. For instance, if you had a list of strings it could convert each string to lowercase, uppercase, or to a substring of the original string, or something completely else.List<String> list = new ArrayList<String>(); Stream<String> stream = list.stream(); Stream<String> streamMapped = stream.map((value) -> value.toUpperCase());
- flatMap()
Stream flatMap() methods maps a single element into multiple elements. The idea is that you “flatten” each element from a complex structure consisting of multiple internal elements, to a “flat” stream consisting only of these internal elements.List<String> stringList = new ArrayList<String>(); stringList.add("One flew over the cuckoo's nest"); stringList.add("To kill a muckingbird"); stringList.add("Gone with the wind"); Stream<String> stream = stringList.stream(); stream.flatMap((value) -> { String[] split = value.split(" "); return (Stream<String>) Arrays.asList(split).stream();}) .forEach((value) -> System.out.println(value));
This Java Stream flatMap() example first creates a List with 3 strings containing book titles. Then a Stream for the List is obtained, and flatMap() called. The flatMap() operation called on the Stream has to return another Stream representing the flat mapped elements. In the example above, each original string is split into words, turned into a List, and the stream obtained and returned from that List. Note that this example finishes with a call to forEach() which is a terminal operation. This call is only there to trigger the internal iteration, and thus flat map operation. If no terminal operation was called on the Stream chain, nothing would have happened. No flat mapping would actually have taken place.
- distinct()
The Java Stream distinct() method is a non-terminal operation that returns a new Stream which will only contain the distinct elements from the original stream. Any duplicates will be eliminated.List<String> stringList = new ArrayList<String>(); stringList.add("one"); stringList.add("two"); stringList.add("three"); stringList.add("one"); Stream<String> stream = stringList.stream(); List<String> distinctStrings = stream.distinct().collect(Collectors.toList()); System.out.println(distinctStrings);
In this example the element one appears 2 times in the original stream. Only the first occurrence of this element will be included in the Stream returned by distinct().
- limit()
Stream limit() method can limit the number of elements in a stream to a number given to the limit() method as parameter. The limit() method returns a new Stream which will at most contain the given number of elements.List<String> stringList = new ArrayList<String>(); stringList.add("one"); stringList.add("two"); stringList.add("three"); stringList.add("one"); Stream<String> stream = stringList.stream(); stream.limit(2).forEach(element -> { System.out.println(element); });
This example first creates a Stream, then calls limit() on it, and then calls forEach() with a lambda that prints out the elements in the stream. Only the two first elements will be printed because of the limit(2) call.
- sorted()
Stream sorted() method use to sort the stream elements by passing Comparator argument.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]
Terminal Operations
The terminal operations of the Stream interface typically return a single value. Once the terminal operation is invoked on a Stream, the iteration of the Stream and any of the chained streams will get started. A terminal operation typically does not return a new Stream instance. Thus, once you call a terminal operation on a stream, the chaining of Stream instances from non-terminal operation ends.
- anyMatch()
Stream anyMatch() method is a terminal operation that takes a single Predicate as parameter, starts the internal iteration of the Stream, and applies the Predicate parameter to each element. If the Predicate returns true for any of the elements, the anyMatch() method returns true. If no elements match the Predicate, anyMatch() will return false.List<String> stringList = new ArrayList<String>(); stringList.add("One flew over the cuckoo's nest"); stringList.add("To kill a muckingbird"); stringList.add("Gone with the wind"); Stream<String> stream = stringList.stream(); boolean anyMatch = stream.anyMatch((value) -> { return value.startsWith("One"); }); System.out.println(anyMatch);
In the example above, the anyMatch() method call will return true, because the first string element in the stream starts with “One”.
- allMatch()
Stream allMatch() method is a terminal operation that takes a single Predicate as parameter, starts the internal iteration of elements in the Stream, and applies the Predicate parameter to each element. If the Predicate returns true for all elements in the Stream, the allMatch() will return true. If not all elements match the Predicate, the allMatch() method returns false.List<String> stringList = new ArrayList<String>(); stringList.add("One flew over the cuckoo's nest"); stringList.add("To kill a muckingbird"); stringList.add("Gone with the wind"); Stream<String> stream = stringList.stream(); boolean allMatch = stream.allMatch((value) -> { return value.startsWith("One"); }); System.out.println(allMatch);
In the example above, the allMatch() method will return false, because only one of the strings in the Stream starts with “One”.
- noneMatch()
Stream noneMatch() method is a terminal operation that will iterate the elements in the stream and return true or false, depending on whether no elements in the stream matches the Predicate passed to noneMatch() as parameter. The noneMatch() method will return true if no elements are matched by the Predicate, and false if one or more elements are matched.List<String> stringList = new ArrayList<String>(); stringList.add("abc"); stringList.add("def"); Stream<String> stream = stringList.stream(); boolean noneMatch = stream.noneMatch((element) -> {return "xyz".equals(element);}); System.out.println("noneMatch = " + noneMatch);
- collect()
Stream collect() method is a terminal operation that starts the internal iteration of elements, and collects the elements in the stream in a collection or object of some kind.List<String> stringList = new ArrayList<String>(); stringList.add("One flew over the cuckoo's nest"); stringList.add("To kill a muckingbird"); stringList.add("Gone with the wind"); Stream<String> stream = stringList.stream(); List<String> stringsAsUppercaseList = stream.map(value -> value.toUpperCase()).collect(Collectors.toList()); System.out.println(stringsAsUppercaseList);
The collect() method takes a Collector (java.util.stream.Collector) as parameter. Implementing a Collector requires some study of the Collector interface. Luckily, the Java class java.util.stream.Collectors contains a set of pre-implemented Collector implementations you can use, for the most common operations. In the example above, it was the Collector implementation returned by Collectors.toList() that was used. This Collector simply collects all elements in the stream in a standard Java List.
- count()
Stream count() method is a terminal operation which starts the internal iteration of the elements in the Stream, and counts the elements.List<String> stringList = new ArrayList<String>(); stringList.add("One flew over the cuckoo's nest"); stringList.add("To kill a muckingbird"); stringList.add("Gone with the wind"); Stream<String> stream = stringList.stream(); long count = stream.flatMap((value) -> { String[] split = value.split(" "); return (Stream<String>) Arrays.asList(split).stream();}).count(); System.out.println("count = " + count);
This example first creates a List of strings, then obtain the Stream for that List, adds a flatMap()operation for it, and then finishes with a call to count(). The count() method will start the iteration of the elements in the Stream which will result in the string elements being split up into words in the flatMap() operation, and then counted. The final result that will be printed out is 14.
- findAny()
Stream findAny() method can find a single element from the Stream. The element found can be from anywhere in the Stream. There is no guarantee about from where in the stream the element is taken.List<String> stringList = new ArrayList<String>(); stringList.add("one");stringList.add("two"); stringList.add("three");stringList.add("one"); Stream<String> stream = stringList.stream(); Optional<String> anyElement = stream.findAny(); System.out.println(anyElement.get());
Notice how the findAny() method returns an Optional. The Stream could be empty – so no element could be returned. You can check if an element was found via the Optional isPresent() method.
- findFirst()
Stream findFirst() method finds the first element in the Stream, if any elements are present in the Stream. The findFirst() method returns an Optional from which you can obtain the element, if present.List<String> stringList = new ArrayList<String>(); stringList.add("one"); stringList.add("two"); stringList.add("three"); stringList.add("one"); Stream<String> stream = stringList.stream(); Optional<String> result = stream.findFirst(); System.out.println(result.get());
You can check if the Optional returned contains an element via its isPresent() method.
- forEach()
Stream forEach() method starts the internal iteration of the elements in the Stream, and applies a Consumer (java.util.function.Consumer) to each element in the Stream. The forEach() method returns void.List<String> stringList = new ArrayList<String>(); stringList.add("one"); stringList.add("two"); stringList.add("three"); stringList.add("one"); Stream<String> stream = stringList.stream(); stream.forEach(element -> { System.out.println(element); });
- min()
Stream min() method returns the smallest element in the Stream. Which element is the smallest is determined by the Comparator implementation you pass to the min()method.List<String> stringList = new ArrayList<String>(); stringList.add("abc"); stringList.add("def"); Stream<String> stream = stringList.stream(); Optional<String> min = stream.min((val1, val2) -> {return val1.compareTo(val2);}); String minString = min.get(); System.out.println(minString);
If the Streamis empty, the Optional get() method will throw a NoSuchElementException.
- max()
Stream max() method returns the largest element in the Stream. Which element is the largest is determined by the Comparator implementation you pass to the max()method.List<String> stringList = new ArrayList<String>(); stringList.add("abc"); stringList.add("def"); Stream<String> stream = stringList.stream(); Optional<String> max = stream.max((val1, val2) -> {return val1.compareTo(val2);}); String maxString = max.get(); System.out.println(maxString);
- reduce()
Stream reduce() method can reduce all elements in the stream to a single element.List<String> stringList = new ArrayList<String>(); stringList.add("One flew over the cuckoo's nest"); stringList.add("To kill a muckingbird"); stringList.add("Gone with the wind"); Stream<String> stream = stringList.stream(); Optional<String> reduced = stream.reduce((value, combinedValue) -> {return combinedValue + " + " + value;}); System.out.println(reduced.get());
This Optional contains the value (if any) returned by the lambda expression passed to the reduce() method. You obtain the value by calling the Optional get() method.
- toArray()
Stream toArray() method is a terminal operation that starts the internal iteration of the elements in the stream, and returns an array of Object containing all the elements.List<String> stringList = new ArrayList<String>(); stringList.add("One flew over the cuckoo's nest"); stringList.add("To kill a muckingbird"); stringList.add("Gone with the wind"); Stream<String> stream = stringList.stream(); Object[] objects = stream.toArray();
Functional Interfaces in Java 8 Stream
Some of the commonly used functional interfaces in the Java 8 Stream API methods are:
- Function and BiFunction: Function represents a function that takes one type of argument and returns another type of argument. 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.
- Predicate and BiPredicate: It represents a predicate against which elements of the stream are tested. This is used to filter elements from the java stream. Just like Function, there are primitive specific interfaces for int, long and double.
- Consumer and BiConsumer: It represents an operation that accepts a single input argument and returns no result. It can be used to perform some action on all the elements of the java stream.
- Supplier: Supplier represent an operation through which we can generate new values in the stream.