Java Streams Terminal Operations with Examples

In this article, we will learn Java Streams Terminal Operations such as AnyMatch, Collectors, Count, FindAny, FindFirst, Min, Max, NoneMatch, and AllMatch. Java streams should be ended with a terminal operation and we will have many options to use based on our requirements. Let’s learn these terminal operations with examples.

Java Streams Terminal Operations with Examples

There are many terminal operations for streams in java and I will start with the AnyMatch definition and example and then we will proceed with the others.

anyMatch() Terminal Operation

The anyMatch()  method returns whether any elements of this stream match the provided predicate. It may not evaluate the predicate on all elements if not necessary for determining the result. If the stream is empty then false is returned and the predicate is not evaluated.
This is a short-circuiting terminal operation which means it can allow computations on infinite streams to complete in finite time. Let’s do an example and see how it performs.

public class StreamAnyMatch {
    List<String> texts = new ArrayList<>();

    @BeforeEach
    public void setup() {
        texts.add("SW Test Academy!");
        texts.add("Development");
        texts.add("DevOps");
        texts.add("Test Culture");
        texts.add("Real test Examples");
    }

    @Test
    public void anyMatchExample() {
        boolean result = texts.stream()
            .filter(text -> !text.isEmpty())
            .map(String::toLowerCase)
            .anyMatch(text -> text.contains("test"));

        System.out.println(result);
    }
}

Output

In this test, we have a texts list that comprises multiple elements and in our test method, first, we filter non-empty text elements, then we transform the filtered elements to lower case, and finally, we are checking if there are any elements that contain “test”. If yes, the AnyMatch() method returns true, if not it returns false. In our test, the texts list has elements that contain “test” and AnyMatch() returns true for this test.

anymatch stream example

collect() Terminal Operation

We can collect the stream elements as List, Map, and Set with the collect() method. For a more advanced explanation, I am also sharing some important parts of its official description below.

The collect() method performs a mutable reduction operation on the elements of this stream using a Collector. A Collector encapsulates the functions used as arguments to collect(Supplier, BiConsumer, BiConsumer), allowing for reuse of collection strategies and composition of collect operations such as multiple-level grouping or partitioning.

For Parallel Execution: If the stream is parallel, and the Collector is concurrent, and either the stream is unordered or the collector is unordered, then a concurrent reduction will be performed. When executed in parallel, multiple intermediate results may be instantiated, populated, and merged so as to maintain the isolation of mutable data structures. Therefore, even when executed in parallel with non-thread-safe data structures (such as ArrayList), no additional synchronization is needed for a parallel reduction,

I want to show some basic usages of collect() terminal operation in examples below.

@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class StreamCollectors {
    List<Integer> numbers = new ArrayList<>();
    List<String>  texts   = new ArrayList<>();

    @BeforeEach
    public void setup(TestInfo testInfo) {
        System.out.println("Test name: " + testInfo.getDisplayName());

        Collections.addAll(numbers, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

        texts.add("Ronaldo");
        texts.add("Messi");
        texts.add("Zlatan");
        texts.add("Pele");
    }

    @AfterEach
    public void tearDown() {
        numbers.clear();
        texts.clear();
        System.out.println("");
    }

    @Test
    @Order(1)
    public void streamCollectorsToListTest() {
        List<Integer> numberList = numbers.stream()
            .filter(number -> number < 8) //Filter numbers which are smaller than 8.
            .filter(number -> number % 2 == 0) //Filter even numbers.
            .skip(1) //Skip the first filtered number.
            .map(number -> number * number) //Transform the number to number*number
            .collect(Collectors.toList());

        System.out.println("Number List: " + numberList);
    }

    /**
     * Set does not have duplicate elements.
     */
    @Test
    @Order(2)
    public void streamCollectorsToSetNumbersTest() {
        List<Integer> numbers = new ArrayList<>();
        Collections.addAll(numbers, 3, 3, 2, 4, 4, 1, 1, 6, 5);

        System.out.println("Number List: " + numbers);

        Set<Integer> numberSet = numbers.stream()
            .filter(number -> number < 6)
            .collect(Collectors.toSet());

        System.out.println("Number Set: " + numberSet);
    }

    @Test
    @Order(3)
    public void streamCollectorsToSetTextsTest() {
        texts.add("Ronaldo");
        texts.add("Ronaldo");
        texts.add("Pele");
        System.out.println("Text List: " + texts);

        Set<String> textSet = texts.stream()
            .filter(text -> text.length() < 12)
            .collect(Collectors.toSet());

        System.out.println("Text Set: " + textSet);
    }

    /**
     * Joining the elements with our without delimiters.
     */
    @Test
    @Order(4)
    public void streamCollectorsJoiningTest() {
        System.out.println("Text List: " + texts);

        String joinedText = texts.stream()
            .filter(text -> text.length() < 12)
            .collect(Collectors.joining(" - "));

        System.out.println("Joined Text: " + joinedText);
    }

    /**
     * Grouping elements with defined rules.
     */
    @Test
    @Order(5)
    public void streamCollectToGroupingByLengthTest() {
        texts.add("Ozil");
        texts.add("Zidane");
        texts.add("Iniesta");
        System.out.println("Text List: " + texts);

        //Group By Length
        Map<Integer, List<String>> groupByLength = texts.stream()
            .collect(Collectors.groupingBy(String::length));

        //Group By Contains
        Map<Boolean, List<String>> groupByContainsCharZ = texts.stream()
            .map(String::toLowerCase)
            .collect(Collectors.groupingBy(text -> text.contains("z")));

        //Group By Last Character
        Map<Character, List<String>> groupByLastCharacter = texts.stream()
            .collect(Collectors.groupingBy(text -> text.charAt(text.length() - 1)));

        System.out.println("groupBy Length: " + groupByLength);
        System.out.println("groupBy Contains Character Z: " + groupByContainsCharZ);
        System.out.println("groupBy Last Character: " + groupByLastCharacter);
    }
}

Output

As seen the test results below;

  • In the first example, we collect the stream as a list and print it.
  • In the second and third examples, we collect the stream as a set and print it. Sets do not have duplicate elements.
  • In the fourth example, we use the joining() method and we joined the stream elements with the” – ” delimiter.
  • In the fifth example, we group the stream elements with specific rules like string length, elements that contain “z”, and group by their last character respectively.

collect() terminate operation in streams

count() Terminal Operation

The count() method returns the count of elements in a stream. The example of count() terminal operation is as follows.

public class StreamCount {
    List<Integer> numbers = new ArrayList<>();

    @BeforeEach
    public void setup() {
        Collections.addAll(numbers, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
    }

    @Test
    public void streamCountTest() {
        long count = numbers.stream()
            .filter(number -> number < 8) //Filter numbers which are smaller than 8.
            .filter(number -> number % 2 == 0) //Filter even numbers.
            .skip(1) //Skip the first filtered number.
            .count(); //Count the rest of the numbers.
        System.out.println("Count: " + count);
    }
}

Output

As seen in the test result below, the numbers stream has two filters that filter the even numbers smaller than 8, and then skip the first one and with the count method, we are counting them. So, the numbers which pass through the filter are 2, 4, 6 and the code skips the number 2, and the final elements of the stream are 4 and 6 and the count is 2. 

count() terminal operation in streams

findAny() Terminal Operation

The findAny() method returns an Optional describing some element of the stream, or an empty Optional if the stream is empty. The behavior of this operation is explicitly nondeterministic; it is free to select any element in the stream. This is to allow for maximal performance in parallel operations; the cost is that multiple invocations on the same source may not return the same result. If we need a stable result, then we should use findFirst() method instead. I will show some examples for both findAny() and findFirst() methods below.

@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class StreamFindAny {
    List<String> texts = new ArrayList<>();

    @BeforeEach
    public void setup(TestInfo testInfo) {
        System.out.println("Test name: " + testInfo.getDisplayName());

        texts.add("Ronaldo");
        texts.add("Messi");
        texts.add("Zlatan");
        texts.add("Pele");
        texts.add("Iniesta");
        texts.add("Zidane");
        texts.add("Ozil");
    }

    @AfterEach
    public void tearDown() {
        texts.clear();
        System.out.println("");
    }

    //Find any of the element which satisfies the condition. Just returns one element from the stream. It does parallel processing.
    @Test
    @Order(1)
    public void findAnyTest() {
        Instant start = Instant.now();
        Optional<String> elementContainsCharZ = texts.stream()
            .map(String::toLowerCase)
            .filter(text -> text.contains("z"))
            .findAny();

        elementContainsCharZ.ifPresent(System.out::println);
        Instant end = Instant.now();
        System.out.println("Elapsed time of findAny: " + Duration.between(start, end).toNanos());
    }

    //Returns the first element. It does synchronous processing.
    @Test
    @Order(2)
    public void findFirstTest() {
        Instant start = Instant.now();
        Optional<String> elementContainsCharZ = texts.stream()
            .map(String::toLowerCase)
            .filter(text -> text.contains("z"))
            .findFirst();

        elementContainsCharZ.ifPresent(System.out::println);
        Instant end = Instant.now();
        System.out.println("Elapsed time of findFirst: " + Duration.between(start, end).toNanos());
    }

    //FindFirst with parallel. Here we have bad performance. Try to do parallel operation when there are
    //some IO operations or long operations. Parallel does not always guarantee the best performance.
    //For broken link tests, parallel improves the performance  a lot because we are hitting the links in parallel.
    @Test
    @Order(3)
    public void findFirstWithParallelTest() {
        Instant start = Instant.now();
        Optional<String> elementContainsCharZ = texts.stream()
            .parallel()
            .map(String::toLowerCase)
            .filter(text -> text.contains("z"))
            .findFirst();

        elementContainsCharZ.ifPresent(System.out::println);
        Instant end = Instant.now();
        System.out.println("Elapsed time of findFirst with Parallel: " + Duration.between(start, end).toNanos());
    }
}

Output

We have a list of footballers and in the first example, the code first transforms all string elements to lower case, then it filters the footballers who contain “z” in their names. In the first example, the code finds any of the element which satisfies this condition and whenever it finds the satisfying condition, it does a short-circuiting and finishes the operation. The findAny() method is a short-circuit terminal operation.

The second and third examples are functionally the same. The codes in these examples find the first footballer whose name contains “z” and this is Zlatan. In the second example, the stream runs synchronously and in the third example, the stream runs in parallel.

Parallel execution does not always guarantee the best performance and in the third example,  we have a bad performance. We should use parallel stream executions for example when we have some IO operations or similar conditions. For example, if we test the broken links of a website, parallel execution improves the performance a lot because we are hitting the links in parallel, getting the results, and processing them. In that case, parallelism performs much better. However, in these kinds of simple examples, parallel execution runs slower than synchronous execution as you can see below.

findAny() terminal operation in java

findFirst() Terminal Operation

The findFirst() method returns an Optional describing the first element of this stream, or an empty Optional if the stream is empty. If the stream has no encounter order, then any element may be returned. This method is a short-circuiting terminal operation too as like findAny(). Now, time to do some examples.

@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class StreamFindFirst {
    List<Integer> numbers = new ArrayList<>();

    @BeforeEach
    public void setup(TestInfo testInfo) {
        System.out.println("Test name: " + testInfo.getDisplayName());
        Collections.addAll(numbers, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
    }

    @AfterEach
    public void tearDown() {
        numbers.clear();
        System.out.println("");
    }

    @Test
    @Order(1)
    public void streamCountTest() {
        Optional<Integer> firstFoundNumber = numbers.stream()
            .filter(number -> number < 8) //Filter numbers which are smaller than 8.
            .filter(number -> number % 2 == 0) //Filter even numbers.
            .skip(1) //Skip the first filtered number.
            .findFirst();

        //Traditional Style
        if(firstFoundNumber.isPresent()) {
            System.out.println(firstFoundNumber.get());
        }

        //Functional Style
        firstFoundNumber.ifPresent(System.out::println);
    }

    @Test
    @Order(2)
    public void streamCountTestWithException() {
        Optional<Integer> firstFoundNumber = Optional.of(numbers.stream()
            .filter(number -> number < 8) //Filter numbers which are smaller than 8.
            .filter(number -> number % 2 == 0) //Filter even numbers.
            .skip(4) //Skip the first 4 filtered number.
            .findFirst()
            .orElseThrow(NoSuchElementException::new));

        //Functional Style
        firstFoundNumber.ifPresent(System.out::println);
    }
}

Output

In the first example, the code finds the first even number which is smaller than 8, it should be normally 2 but because of the skip() method the code skips the 2 and the first number which satisfies the condition is 4. We printed the result with both functional and non-functional styles. 

In the second example, the code cannot find any element that satisfies stream conditions and because of that, it throws a “NoSuchElementException”.

findFirst() terminal operation in java streams

min() and max() Terminal Operations

The min() method returns the minimum element of this stream according to the provided Comparator. The max() method returns the maximum element of this stream according to the provided Comparator. Now, examples time. 

@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class StreamMinMax {
    List<Integer> numbers = new ArrayList<>();

    @BeforeEach
    public void setup(TestInfo testInfo) {
        System.out.println("Test name: " + testInfo.getDisplayName());
        Collections.addAll(numbers, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
    }

    @AfterEach
    public void tearDown() {
        numbers.clear();
        System.out.println("");
    }

    /**
     * ReverseOrder is DESCENDING order.
     * NaturalOrder is ASCENDING order.
     */
    @Test
    @Order(1)
    public void minTest() {
        Optional<Integer> min = numbers.stream()
            .min(Comparator.naturalOrder());

        min.ifPresent(System.out::println);
    }

    @Test
    @Order(2)
    public void minReverseWayTest() {
        Optional<Integer> min = numbers.stream()
            .min(Comparator.reverseOrder());

        min.ifPresent(System.out::println);
    }

    @Test
    @Order(3)
    public void maxTest() {
        Optional<Integer> max = numbers.stream()
            .max(Comparator.naturalOrder());

        max.ifPresent(System.out::println);
    }

    @Test
    @Order(4)
    public void maxReverseWayTest() {
        Optional<Integer> max = numbers.stream()
            .max(Comparator.reverseOrder());

        max.ifPresent(System.out::println);
    }
}

Output

In the first example, the code finds the minimum number in Ascending order and the minimum number is 1. (Min operation with naturalOrder).

In the second example, the code finds the minimum number in reverse order which is the inverse of minimum operation that’s why it prints 10. (Inverse of Min because of reverseOrder).

In the third example, the code finds the maximum number with natural (ascending) order and which is 10. (Max operation with naturalOrder).

In the fourth example, max with reverse order is a kind of the inverse of finding a max number which we did in the third example, and the code prints 1. (Inverse of Max because of reverseOrder).

min() and max() terminal operations in java streams

noneMatch() and allMatch() Terminal Operation

The noneMatch() method returns whether no elements of this stream match the provided predicate. May not evaluate the predicate on all elements if not necessary for determining the result. If the stream is empty then true is returned and the predicate is not evaluated.

The allMatch() method returns whether all elements of this stream match the provided predicate. May not evaluate the predicate on all elements if not necessary for determining the result. If the stream is empty then true is returned and the predicate is not evaluated

@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class StreamNoneAndAllMatch {
    List<String> texts = new ArrayList<>();

    @BeforeEach
    public void setup(TestInfo testInfo) {
        System.out.println("Test name: " + testInfo.getDisplayName());
        texts.add("SW Test Academy!");
        texts.add("Development");
        texts.add("DevOps");
        texts.add("Test Culture");
        texts.add("Real test Examples");
    }

    @Test
    @Order(1)
    public void noneMatchExample() {
        boolean result = texts.stream()
            .filter(text -> !text.isEmpty())
            .map(String::toLowerCase)
            .noneMatch(text -> text.contains("TEST"));

        System.out.println(result);
    }

    @Test
    @Order(2)
    public void allMatchExample() {
        boolean result = texts.stream()
            .filter(text -> !text.isEmpty())
            .map(String::toLowerCase)
            .allMatch(text -> text.contains("TEST"));

        System.out.println(result);
    }
}

Output

In the first example, the code filters non-empty string elements, then the code transforms them to lower case and then tries to match “TEST” in the elements of the stream, and no elements of this stream do not match with this condition, thus it returns true.

In the second example, the code filters non-empty string elements, then the code transforms them to lower case and then tries to match “TEST” in the elements of the stream, and all elements of this stream do not match with this condition, thus it returns false.

allMatch() and noneMatch() examples in java

GitHub Project

https://github.com/swtestacademy/java-functional/tree/main/src/test/java/functional/stream/terminaloperations

In this article, I explained the most important Java Streams Terminal Operations with examples. I hope you enjoyed reading it. Hope to see you in the next articles.

Thanks,
Onur Baskirt

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.