Complete Guide to Java Stream

A stream is a sequence of elements on which we can perform various kinds of sequential and parallel operations. The Stream API was released in JAVA 8 and can be used to process a collection of objects.

In contrast to collections, a Java stream doesn't function as a data structure; rather, it accepts input from Collections, Arrays, or I/O channels (such as files).

Stream employs internal iterations for processing the elements in the stream. This feature enables us to eliminate the need for verbose constructs such as while, for, and forEach loops. The streams also support pipelining, meaning that we can create chaining of stream operations as most of the stream operation returns a stream only.

In this tutorial, we will explore the different classes and interfaces of the Java Stream API and see various operations that can be done on Java Streams.

Creating a Stream

The java.util.stream package has interfaces and classes designed to support functional style programming on stream elements. Along with the Stream interface which is a stream of objects, there are specialized versions for primitive types like IntStream, LongStream, and DoubleStream.

Streams can be created in multiple ways from various data sources:

Getting a Stream from an Array

We can use .stream() method of the Arrays class to get a stream from an array:

   public static void createStreamFromArray() {
        int[] elements = {3, 5, 98, 34};

        IntStream stream = Arrays.stream(elements);

        System.out.println("Printing Stream from Array::");
        stream.forEach(System.out::println);
    }

In this example, a stream of int elements is created from an int array and are printed using .forEach() method on the stream.

Getting a Stream from a Collection

We can use either .stream() or .parallelStream() methods to get a stream from a collection.

   public static void createStreamFromCollection() {
        List<Integer> nums = List.of(1, 3, 5, 6, 87, 4);

        Stream<Integer> stream = nums.stream();

        Stream<Integer> parallelStream = nums.parallelStream();

        System.out.println("\n\nPrinting Stream from Collection::");
        stream.forEach(System.out::println);

        System.out.println("\n Printing Parallel Stream from Collection::");
        parallelStream.forEach(System.out::println);
    }

In this example, two streams of int elements are created using the .stream() and .parallelStream() methods from a collection of type List and then they are printed by using .forEach() method on the streams. The elements in the stream created with .stream() are processed in series while the elements in the stream created with .parallelStream() are processed in parallel.

Getting a Stream by using .of() method

Streams can also be created by using the static factory method .of() on the stream classes.

public static void streamFromFactoryMehtod() {
        Stream<Integer> stream = Stream.of(7, 4, 99, 2);

        IntStream intStream = IntStream.of(7, 4, 99, 2);

        LongStream longStream = LongStream.of(43, 56, 232, 876, 234);

        System.out.println("\n\nPrinting Stream Factory method::");
        stream.forEach(System.out::println);

        System.out.println("\n\nPrinting Int Stream Factory Method::");
        intStream.forEach(System.out::println);

        System.out.println("\n\nPrinting Long Stream Factory Method::");
        longStream.forEach(System.out::println);

}

In this example, streams of int and long are created using the .of() method on the Stream classes.

Getting stream from files

We can get the lines of the files as stream using Files.lines() method, as shown in the following example:

public static void streamFromFileReading() {

        try (Stream<String> line = Files.lines(file.toPath())){
            System.out.println("\n\nPrinting Stream from file line:: \n");
            line.forEach(System.out::println);
        } catch (IOException e) {
            e.printStackTrace();
            System.err.println(e.getMessage());
        }
}

In this example, we are reading the lines of the file in a stream using the Files.line() method and as we are using try-with-resource it will automatically close the stream after use.

Stream extends BaseStream which has .close() method and BaseStream further extends the AutoClosable interface. Streams whose source is an IO channel (similar to Files.line() as shown in example) will be needing close after use.

The majority of streams are supported by collections, arrays, or generating functions, thus they don't require closing after usage.

Stream Operations

Stream operations can be broadly divided into two categories:

  • Intermediate Operations – These operations are used to transform the stream from one stream to another stream. For example, map() is an intermediate operation which transforms elements of the stream into another by applying a function (called predicate) on each element.

  • Terminal Operations – As the name suggests, terminal operation is the last operation executed on the stream. These operations are executed on stream to get a single result like an object or a primitive type or collection or may not return anything. For example, .collect() is a terminal operation that collects the result into a Collection like List or Map.

The intermediate operations are lazy operations and are not invoked until the terminal operation is invoked.

In the following section, let’s look at the various intermediate and terminal operations that can be performed on stream.

Mapping operations on Stream

Mapping operations are intermediate operations and are used to transform elements of the stream by a predicate function and returns a stream for further processing.

map()

The map() operation takes a function as an input and returns a new stream containing the outcomes of the supplied function applied to each element of the stream.

In this example, we are creating a stream from a list of integers and passing an input function that maps the square of each number in the list.

public static void mapOperation() {
        List<Integer> nums = List.of(1,2,3,4,5,6,7,8,9,10);

        List<String> numsSq = nums.stream().map(num -> {
            //mapping function
            int square = num * num;
            return "Square of " + num + " is " + square;
        }).collect(Collectors.toList());


        System.out.println("Printing num square list");

        numsSq.forEach(System.out::println);
    }

Here in this example, a mapping function is passed as input, and we calculate the square of each number in the list and create a formatted string; the map() operation returns a stream of the formatted string with the square of numbers. Then we are executing .collect() function to convert the stream to a collection.

When we will run the code we will get the following output:

Printing num square list
Square of 1 is 1
Square of 2 is 4
Square of 3 is 9
Square of 4 is 16
Square of 5 is 25
Square of 6 is 36
Square of 7 is 49
Square of 8 is 64
Square of 9 is 81
Square of 10 is 100

flatMap()

flatMap() should be used in the case where the elements of the stream itself have its sequence of elements and we want to create a single stream of these inner elements. flatMap() is a combination of map and flat operation, i.e., it first applies the map function on the elements and then flattens the result.

public static void flatMapOperation() {
        List<String> mobileBrands = List.of("Nokia", "Motrola", "Samsung");
        List<String> laptopBrands = List.of("HP", "Dell", "ASUS"); 

        List<List<String>> brands = List.of(mobileBrands, laptopBrands);

        List<String> brandList = brands.stream()
                                    .flatMap(Collection::stream)
                                    .collect(Collectors.toList());

        System.out.println("\n\nPrinting brand list:: " + brandList);
}

In this example, all the elements of the stream are a List. flatMap() operation helped us to get a list of all the inner elements as shown in the following output:

Printing edibles list:: [Apple, Banana, Peach, Potato, Cauliflower, Cabbage]

Ordering Operation

For ordering a stream we have two methods:

  • Sorted() – this method sorts the stream based on the natural order

  • Sorted(comparator) – This is an overridden method which sorts the stream elements based on the provided comparator instance.

public static void sortedOperation() {
        List<Integer> nums = List.of(90,45,65,23,5,67,23,87);

        Stream<Integer> sorted = nums.stream().sorted();

        System.out.println("\n\nPrinting sorted stream");
        sorted.forEach(System.out::println);
}

public static void sortedWithComparator() {
        List<Integer> nums = List.of(90,45,65,23,5,67,23,87);
        Stream<Integer> sorted = nums.stream().sorted((o1, o2) -> o2-o1);

        System.out.println("\n\nPrinting comparator sorted in desending order stream:: ");

        sorted.forEach(System.out::println);
}

In the method sortedOperation(), we are sorting the integer elements in their natural order i.e., ascending order.

In the method sortedWithComparator(), we are sorting the elements by using a Comparator to sort in descending order.

Comparator is a functional interface that is used to provide an ordering sequence for a collection of objects. For more details on comparator check official Java Docs.

As both sorting methods are intermediate operations, we will need to call a terminal operation to trigger the sorting. Here in this example, we are using the forEach() that triggers the sort.

Matching and Filtering Operation

The stream interface has methods to check and validate whether the elements of the stream satisfy a condition (called the predicate).

anyMatch()

anyMatch() is used to check whether any of the elements in the stream satisfies the condition provided as predicate.

anyMatch() returns a boolean, true if any element satisfies the specified condition else false.

public static void anyMatchOperation() {
        List<Integer> nums = List.of(3, 45, 75, 66, 33, 13,43, 63);

        boolean anyEvenNum = nums.stream().anyMatch(num -> num%2 == 0);

        System.out.println("Any even number present in the list::  " + anyEvenNum);
}

In this example, we are checking if the stream contains any even number, and anyMatch() returns true as 2 is an even number.

allMatch()

allMatch() is used to check whether all the elements of the stream satisfy the condition provided as predicate.

allMatch() returns true if all the elements satisfy the specified condition else returns false.

public static void allMatchOperation() {
        List<Integer> nums = List.of(3, 4, 2, 8, 12, 46);

        boolean allEvenNum = nums.stream().allMatch(num -> num%2 == 0);

        System.out.println("All even number present in the list::  " + allEvenNum);
}

In this example, we are checking that all the elements in the list are even numbers. It will return false as 3 is an odd number.

noneMatch()

noneMatch() is used to check whether none of the elements of the stream satisfy the condition provided as predicate.

noneMatch() returns true if none of the elements satisfies the specified condition else returns false.

public static void noneMatchOperation() {
        List<Integer> nums = List.of(53, 7, 9, 65, 37, 19, 63, 71);

        boolean noneEvenNum = nums.stream().noneMatch(num -> num%2 == 0);

        System.out.println("None of the elements are even number:: " + noneEvenNum);
    }

In this example, we are checking that none of the elements in the list are even numbers. It will return true as all the numbers in the list are odd numbers.

filter()

filter() is an intermediate operation used to filter elements of a stream based on the given condition (predicate).

public static void filterStreamOperation() {
        List<Integer> nums = List.of(8, 12, 62, 34, 25, 15, 61, 34, 45, 35, 14);

        List<Integer> filteredList = nums.stream().filter(num -> num > 30).collect(Collectors.toList());

        System.out.println("Filtered List where num is greater than 30:: " + filteredList);
    }

In this example, filter() operation is applied on the list to get a stream of numbers greater than 30.

findFirst() and findAny()

findFirst() returns an Optional and helps to find the first element in the stream.

public static void findFirstOperation() {
        List<String> data = List.of("Apple", "Banana", "Cake", "Random", "Word");

        Optional<String> firstData = data.stream().findFirst();

        System.out.println("First Element in the list:: " + firstData.get());
    }

    public static void findAnyOperation() {
        List<String> data = List.of("Apple", "Banana", "Cake", "Random", "Word");

        Optional<String> anyData = data.stream().findAny();

        System.out.println("Find Any in the list:: " + anyData.get());
    }

findAny() is a similar method using which we can find any element of the stream irrespective of the position in the stream. The behaviour of the findAny() is nondeterministic as it can return any element of the stream.

Reduction Operations

Reduction operations are the operation that returns a single value by combining the contents of the stream like average, sum, min, max and count. The Stream API also has reduction operations that return collection.

min(), max(), avg(), count(), sum()

These are terminal operations used to get the min, max, average, sum or the number of elements in the stream.

public static void specificReductionOperation() {
        List<Integer> nums = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

        OptionalInt min = nums.stream().mapToInt(i -> i).min();
        OptionalInt max = nums.stream().mapToInt(i -> i).max();
        OptionalDouble avg = nums.stream().mapToInt(i -> i).average();
        long count = nums.stream().mapToInt(i -> i).count();
        int sum = nums.stream().mapToInt(i -> i).sum();

        System.out.println("Min: " + min.getAsInt());
        System.out.println("Max: " + max.getAsInt());
        System.out.println("Avg: " + avg.getAsDouble());
        System.out.println("Count: " + count);
        System.out.println("Sum: " + sum);
    }

Stream API also has two general-purpose reduction operations: reduce() and collect().

reduce()

The reduce() method is a general-purpose reduction operation that helps to reduce the stream to a single value.

The reduce method has three overridden method signatures:

  • Optional<T> reduce(BinaryOperator<T> accumulator);

  • T reduce(T identity, BinaryOperator<T> accumulator);

  • <U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);

Reduce functions terminology

  • Identity – default or initial value

  • Accumulator – it is a functional interface that takes two input parameters: the partial result of the reduction operation and the next element of the stream.

  • Combiner: a stateless function for combining the results of the sub-streams when processing is done in a parallel stream.

public static void reduceOperation() {
        List<Integer> nums = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        Optional<Integer> sum1 = nums.stream().reduce((a,b) -> a+b);

        System.out.println("Sum1 of elements using reduce:: " + sum1);

        Integer sum2 = nums.stream().reduce(10, (a, b) -> a + b);

        System.out.println("Sum2 of elements using reduce:: " + sum2);

        Integer sum3 = nums.parallelStream().reduce(10, (a,b) -> a+b, Integer::sum);

        System.out.println("Sum3 of elements using reduce with combiner:: " + sum3);
    }

Here in the example, we have used all three overridden reduce() operation to get the sum of the list.

collect()

The .collect() operation as seen earlier in examples is one of the most commonly used reduction operations to collect the elements of the stream after completing all the operations

public static void collectOperation() {
        List<Integer> nums = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

        List<Integer> list1 = nums.stream().map(num -> num*2).collect(Collectors.toList());

        System.out.println("Collected list:: " + list1);

    }

Parallel Stream

A Stream can be executed either in a serial or parallel. In the case of parallel processing, the stream is divided into multiple sub-streams and aggregate operations run simultaneously on these sub-streams and subsequently merge the outcomes.

public static void parallelStreamOperation() {
        List<Integer> nums = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

        System.out.println("Parallel Stream printing in no order");
        nums.parallelStream().map(num -> num*2).forEach(System.out::println);

        System.out.println("\n\nParallel Stream printing in order");
        nums.parallelStream().map(num -> num*2).forEachOrdered(System.out::println);

    }

Conclusion

  • A stream is a sequence of elements that allows various sequential and parallel operations.

  • The java.util.stream package provides the classes and interfaces for functional-style operations on element streams. Notably, it offers specialized streams like IntStream, LongStream, and DoubleStream in addition to the general Stream interface.

  • Streams can be acquired from arrays and collections using the stream() method and a Stream can also be obtained via the static factory method on the Stream class.

  • Most streams are associated with collections, arrays, or generation functions and typically do not require manual closure, except for streams sourced from files.

  • Stream operations are categorized as intermediate and terminal. Intermediate operations transform one stream into another, while terminal operations produce a final result, such as a primitive object or a collection.

  • Chaining operations on streams forms a pipeline for executing specific use cases.

  • Streams can be executed in serial or parallel modes, with parallel streams divided into substreams. Aggregate operations process these substreams in parallel and then merge the results.

You can refer the source code used in the article on GitHub.