Java Streams Api - Operations

What are the main components of a Stream?

Components of the stream are:

  1. A data source
  2. (Optional) set of Intermediate Operations to process the data source e.g.
    1. Functions (Map, Flatmap)
    2. Predicates (Filter)
    3. Comparators (Sorted)
  3. Single Terminal Operation that produces the result
    1. forEach
    2. reduce
    3. collect
    4. sum

What are Intermediate and Terminal operations?

Intermediate Operations:

  1. Process the stream elements.
  2. Typically transforms a stream into another stream.
  3. Are lazy, i.e., not executed till a terminal operation is invoked.
  4. Does internal iteration of all source elements.
  5. Any number of operations can be chained in the processing pipeline.
  6. Operations are applied as per the defined order.
  7. Intermediate operations are mostly lambda functions.

Terminal Operations:

  1. Why do we need terminal operators at all?
    1. Because, the behavior of streams is lazy - by default.
  2. Kick-starts the Stream pipeline.
  3. used to collect the processed Stream data.
int count = Stream.of(1, 2, 3, 4, 5)
                .filter(i -> i <4) // Intermediate Operation filter
                .count(); // Terminal Operation count

Aggreate operations

Java Streams Api - Aggregate operations

What are the most commonly used Intermediate operations?

  1. Filter(Predicate<T>) - Allows selective processing of Stream elements. It returns elements that are satisfying the supplied condition by the predicate.

  2. map(Funtion<T, R>) - Returns a new Stream, transforming each of the elements by applying the supplied mapper function. e.g.

    List<String> myList = Stream.of("a", "b")
      .map(String::toUpperCase)
      .collect(Collectors.toList());
    assertEquals(asList("A", "B"), myList);
    
  3. distinct() - Only pass on elements to the next stage, not passed yet.

  4. limit(long maxsize) - Limit the stream size to maxsize.

    private static void limit() {
         Stream<Integer> evenNumInfiniteStream = Stream.iterate(0, n -> n + 2);
    
         List<Integer> newList = evenNumInfiniteStream
                 .limit(10)
                 .collect(Collectors.toList());
    
         log.info(newList.toString());
         // [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
     }
    
  5. skip(long start) - Skip the initial elements till the start.

    private static void skip() {
         List<Integer> newList = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
                 .skip(6)
                 .collect(Collectors.toList());
    
         log.info(newList.toString());
         // [7, 8, 9, 10]
     }
    
  6. peek(Consumer) - Apply a consumer without modification to the stream.

    Returns a stream consisting of the elements of this stream, additionally performing the provided action on each element as elements are consumed from the resulting stream.

    public static void terminal_operators() {
         Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
                 .peek(i -> log.info("received : {}", i))
                 .toList();
    }
    [main] INFO com.my.company.streamsapi.LazinessAndTerminalOperators - received : 1
    [main] INFO com.my.company.streamsapi.LazinessAndTerminalOperators - received : 2
    [main] INFO com.my.company.streamsapi.LazinessAndTerminalOperators - received : 3
    [main] INFO com.my.company.streamsapi.LazinessAndTerminalOperators - received : 4
    [main] INFO com.my.company.streamsapi.LazinessAndTerminalOperators - received : 5
    [main] INFO com.my.company.streamsapi.LazinessAndTerminalOperators - received : 6
    [main] INFO com.my.company.streamsapi.LazinessAndTerminalOperators - received : 7
    [main] INFO com.my.company.streamsapi.LazinessAndTerminalOperators - received : 8
    [main] INFO com.my.company.streamsapi.LazinessAndTerminalOperators - received : 9
    [main] INFO com.my.company.streamsapi.LazinessAndTerminalOperators - received : 10
    
  7. flatMap(mapper) - Transform each element to a stream of its constituent elements and flatten all the streams into a single stream. This method first flattens the input Stream of Streams to a single Stream. After that, it works similarly to the map() method.

    Returns a stream consisting of the results of replacing each element of this stream with the contents of a mapped stream produced by applying the provided mapping function to each element. Each mapped stream is closed after its contents have been placed into this stream. (If a mapped stream is null an empty stream is used, instead.)

    e.g.

    List<List<String>> list = Arrays.asList(
        Arrays.asList("a"),
        Arrays.asList("b"));
    System.out.println(list
      .stream()
      .flatMap(Collection::stream)
      .collect(Collectors.toList()));
    

    The result of such a snippet will be flattened to [a, b].

  8. boxed() - To convert from a stream of primitives to a stream of objects, these classes provide boxed() method that returns a Stream consisting of the elements of the given stream, each boxed to an object of the corresponding wrapper class.

    Returns a Stream consisting of the elements of this stream, each boxed to an Integer.

    Variations

    IntStream
    LongStream
    DoubleStream
    

    e.g.

    private static void boxed() {
         Stream<Integer> stream = IntStream.of(1, 2, 3, 4, 5).boxed();
    
         //Compilation issue
         // List<Integer> list1 = IntStream.of(1,2,3,4,5).collect(Collectors.toList());
    
         //Works fine
         List<Integer> list2 = IntStream.of(1, 2, 3, 4, 5)
                 .boxed()
                 .collect(Collectors.toList());
     }
    

What is the stateful intermediate operation? Give some examples of stateful intermediate operations.

To complete some of the intermediate operations, some state is to be maintained, and such intermediate operations are called stateful intermediate operations. Parallel execution of these types of operations is complex.

For Eg: sorted() , distinct() , limit() , skip() etc.

Sending data elements to further steps in the pipeline stops till all the data is sorted for sorted() and stream data elements are stored in temporary data structures.

What is the most common type of Terminal operations?

  1. collect() - Collects single result from all elements of the stream sequence.
  2. reduce() - Produces a single result from all elements of the stream sequence
    1. count() - Returns the number of elements on the stream.
    2. min() - Returns the min element from the stream.
    3. max() - Returns the max element from the stream.
  3. Search/Query operations
    1. anyMatch() , noneMatch() , allMatch() , … - Short-circuiting operations.
    2. Takes a Predicate as input for the match condition.
    3. Stream processing will be stopped, as and when the result can be determined.
  4. Iterative operations
    1. forEach() - Useful to do something with each of the Stream elements. It accepts a consumer.
    2. forEachOrdered() - It is helpful to maintain order in parallel streams.

What is the difference between findFirst() and findAny()?

  1. findFirst()
    1. Returns the first element in the Stream
    2. Deterministic in nature
  2. findAny()
    1. Return any element from the Stream
    2. Non-deterministic in nature

Links to this note