Skip to content

Stream API

Introduction

The classes that access the Stream interface in thejava.util.stream package allow a functional way to process data. Streams (so-called streams) represent a sequential set of elements and allow you to perform various operations on these elements. These operations will be discussed in this section.

Streams and collections

Streams are a kind of representation of a collection, but there are some differences between them:

  • Stream is not a data structure that stores elements. It only transfers references of elements from the source, which can be, for example:
    • data structure
    • board
    • collection
    • generator method.
  • Stream operations return a result, but do not modify the source of the Stream, e.g. filtering a stream obtained from a collection creates a new stream with no filtered items, and no item is removed from the original collection.
  • Collections HAVE finite size, streams do not need to be finite size. Operations such as limit(n) or findFirst() allow the computation in streams to be completed within a finite time.
  • Items in streams are visited only once during a stream. Like Iterator, it is necessary to generate a new stream to revisit the same elements of the source, e.g. a collection.
  • Streams are processed lazy, that means, none of the indirect methods are called until one of the terminators is called.

Ways to get streams

In Java, there are many ways to receive streams based on selected collections, arrays, objects:

  • the stream() method returns the stream for the classes available in the Collection API
  • the Arrays.stream (Object[]) method allows you to create streams from arrays
  • the static method Stream.of(T ... values) allows creating streams based on arrays, objects
  • the Stream.generate() static method allows you to create a Stream of elements based on the input [Suppliera] (functional_programming.md#suppliert)
  • file streams can be returned based on the Files class.

The following example shows different ways to create streams:

Stream<Integer> streamOfInts = Arrays.asList(1, 2, 3).stream();
Stream<String> streamOfStrings = Set.of("one", "two", "three").stream();
Stream<Map.Entry<String, Integer>> stream = Map.of("someKeyA", 1, "someKeyB", 2).entrySet().stream();
IntStream arraysStream = Arrays.stream(new int[]{1, 2, 3});
Stream<Double> ofStream = Stream.of(1.1, 2.2, 3.3);
Stream<Integer> generateStream = Stream.generate(() -> new Random().nextInt());
Stream<String> fileLinesStream = Files.lines(Path.of("/tmp/1.txt"));

Operations on streams

Stream operations are divided into two types - intermediate and trailing.

Indirect operations always return a new stream, and they are lazy processed. They include, among others:

  • filter
  • map
  • flatMap
  • peek
  • distinct
  • sorted
  • limit

Post-operations are operations that return the final result. Calling them causes all preceding intermediate functions to be executed. The terminating functions include:

  • toArray
  • collect
  • count
  • reduce
  • forEach
  • forEachOrdered
  • min
  • max
  • anyMatch
  • allMatch
  • noneMatch
  • findAny
  • findFirst

Most stream operations expect an argument, which is usually an functional interface, i.e. it can be implemented with a lambda expression.

Indirect operations

map

The map method waits for input from theFunction <T, R>object. Its task is to convert the stream element to a new element, which may additionally be of a different type.

// creating a stream and processing the input elements of the Integer type to a value three times greater of the Double type
List.of(1, 2, 3).stream()
    .map(streamElem -> streamElem * 3.0);

flatMap

The flatMap method allows you to flatten a nested data structure. This means that if each processed element has an element from which we are able to create a new Stream, then the result of the flatMap operation will be a new singleStream, which was created by combining them into one. The flatMap operation takes the function interfaceFunction <T,? extends Stream <? extends R >> '. The example shows how to create a single stream fromStatisticsobjects and values` fields.

public class FlatMapDemo {

  public static void main(String[] args) {
    final Statistics statisticsA = new Statistics(2.0, List.of(1, 2, 3));
    final Statistics statisticsB = new Statistics(2.5, List.of(2, 3, 2, 3));
    Stream.of(statisticsA, statisticsB)
        .flatMap(statistics -> statistics.getValues().stream()); // Otrzymujemy stream wartości 1, 2, 3, 2, 3, 2, 3
  }
}

class Statistics {
  private double average;
  private List<Integer> values;

  public Statistics(final double average, final List<Integer> values) {
    this.average = average;
    this.values = values;
  }

  public double getAverage() {
    return average;
  }

  public List<Integer> getValues() {
    return values;
  }
}

filter

The filter operation allows you to remove from the stream those elements that do not meet a certain predicate, which is the input argument of the method. The example below shows how to remove odd numbers from a stream of numbers.

final int[] idx = { 0 };
Stream.generate(() -> idx[0]++)
    .limit(10)
    .filter(elem -> elem % 2 == 0); //the following values remain in the stream: 0, 2, 4, 6, 8

sorted

The sorted method will sort the items in the stream. An argument-less version is available that sorts elements * naturally *. If we want to sort elements according to another rule, we should use the overload that uses the Comparator <T> function interface.

Arrays.asList(6, 3, 6, 21, 20, 1).stream()
    .sorted(Comparator.reverseOrder()); // in the stream you will find: 21, 20, 6, 6, 3, 1 - in this order

distinct

The distinct operation allows you to create a stream in which all elements are unique, i.e. we get rid of repetitions, e.g .:

Arrays.asList(3, 6, 6, 20, 21, 21).stream()
    .distinct(); // there will be items in the stream: 3, 6, 20, 21

Ending operations

forEach

The forEach operation represents a functional version of thefor loop. This function calls any operation implemented with the functional interface `Consumer 'on each element of the stream.

List.of(1, 2, 3, 4, 5).stream()
    .forEach(System.out::println);

collect

The collect method allows you to collect stream items to a certain destination. In order to collect items, we need to use the Collector interface, which is not a functional interface. The Collectors class comes in handy, as it contains static methods responsible for the accumulation of stream elements into the indicated structure, e.g.List or Set.

The following examples show the use of the collect method and the variousCollectors:

final List<Integer> listCreatedFromCollectMethod = Stream.generate(() -> new Random().nextInt())
    .limit(10)
    .distinct()
    .filter(elem -> Math.abs(elem) < 1000)
    .collect(Collectors.toUnmodifiableList());
final String sentence = Stream.of("This", "will", "be", "single", "sentence", "but", "without", "some", "words")
    .filter(word -> word.length() > 2)
    .collect(Collectors.joining(" "));
System.out.println(sentence); // outputem będzie "This will single sentence but without some words"

final Map<String, String> wordToUppercasedVersion = Stream.of("Hello", "from", "Stream", "api")
    .collect(Collectors.toMap(Function.identity(), String::toUpperCase));

groupingBy

The collector obtained with groupingBy allows us to create a Map object from the stream. The groupingBy method expects aFunction which becomes a property of the stream of some sort. The result is a map whose keys are the values of the previously mentioned property, and the value is a list of elements that satisfy that property. This is best illustrated by an example, e.g. the code below creates a map that groups words by their length in the stream:

Stream.of("This", "is", "SDA", "the", "best", "academy", "in", "the", "universe")
    .collect(Collectors.groupingBy(String::length))
    .forEach((key, value) -> System.out.println(key + " " + value));

/* the result is:
2 [is, in]
3 [SDA, the, the]
4 [This, best]
7 [academy]
8 [universe]
*/

findFirst

The findFirst method completes stream processing and retrieves the first available item. Since this method can also be called on an empty stream, the return type is Optional, e.g .:

List.of("who", "will", "be", "first").stream()
    .sorted()
    .findFirst() // zwraca Optional
    .ifPresent(System.out::println); // will display "will"

findAny

findAny, likefindFirst, returns a single stream element (as an object wrapped in Optional), but in this case we are not sure which stream element will be returned if there are multiple elements in it.

List.of(7, 21, 13, 4, 8).stream()
    .filter(x -> x % 2 == 0)
    .findAny()
    .ifPresent(System.out::println);

reduce

The reduce operation allows you to get a single result from all stream elements. The reduce method takes two arguments:

  • initial value
  • transformation method, i.e. information on how to change the current result with the next processed item. We save this information using the functional interface ` BiFunction <T, U, R>.

The example shows how to sum the stream elements:

final Integer sum = List.of(2, 5, 9, 19, 14).stream()
    .reduce(0, (currentSum, streamElement) -> currentSum + streamElement); // or Integer::sum
System.out.println(sum); // the result is a sum - 49

Parallel processing

Control statements of type for are sequential in nature. Streams, on the other hand, allow easier [parallelization] (programming_spolbiezne_i_rownolegle.md) operations. To do this, we need to create a special stream. We can create it, for example with:

  • calls to the parallelStream () method on the collection
  • calls to the static StreamSupport.stream () method, specifying true for theparallel argument
  • calls to parallel () on an existing stream.

The next example shows a stream that parallels the processing of elements:

final List<String> result = Arrays.asList("Alice has a cat named Catson".split(" ")).parallelStream()
    .sorted()
    .map(String::toUpperCase)
    .collect(Collectors.toList());

NOTE: There is no difference between the parallel processing of a stream and its sequential form, there is no difference at the level of calling the intermediate and termination methods.

NOTE: You can "transition" from a parallel stream to a sequential stream with the sequential() method.