ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Stream
    StaticPL/JAVA 2019. 8. 18. 20:25

    1. Overview

    Added from JAVA 8. A stream is a sequence of objects that supports various methods that can be pipelined to manipulate results. Stream operations are either intermediate or terminal. Intermediate operations return a stream so we can chain multiple intermediate operations without using semicolons. Terminal operations are either void or return a non-stream result.

    2. Features

    • Do not change the original data structure, but only provide the result as per the pipelined methods
    • To perform a sequence of operations over the elements of the data source and aggregate their results, three parts are needed. the source, intermediate operations, and terminal operation.
    • Some of the intermediate operations are lazily executed and returns a stream as a result. for the more, intermediate operations can be pipelined again. 
    • A stream takes input from the collections, arrays, or i/o channels.
    • Perform parallelism in a functional style and easily switch from the non-parallel streams which are executed within a single thread.

    2.1 on-interfering

    A function is non-interfering when it does not modify the underlying data source of the stream, e.g. in the above example no lambda expression does modify myList by adding or removing elements from the collection.

    2.2 stateless

    A function is stateless when the execution of the operation is deterministic, e.g. in the above example, no lambda expression depends on any mutable variables or states from the outer scope which might change during execution.

    2.3 Laziness and Ordering

    An important characteristic of intermediate operations is laziness. Look at this sample where a terminal operation is missing:

    Stream.of("d2", "a2", "b1", "b3", "c")
        .filter(s -> {
            System.out.println("filter: " + s);
            return true;
        });

    When executing this code snippet, nothing is printed to the console. That is because intermediate operations will only be executed when a terminal operation is present.

    Stream.of("d2", "a2", "b1", "b3", "c")
        .filter(s -> {
            System.out.println("filter: " + s);
            return true;
        })
        .forEach(s -> System.out.println("forEach: " + s));
        
        
    // output
    filter:  d2
    forEach: d2
    filter:  a2
    forEach: a2
    filter:  b1
    forEach: b1
    filter:  b3
    forEach: b3
    filter:  c
    forEach: c

    The order of the result might be surprising. A naive approach would be to execute the operations horizontally one after another on all elements of the stream. But instead, each element moves along the chain vertically. The first string "d2" passes filter then forEach, only then the second string "a2" is processed.

    This behavior can reduce the actual number of operations performed on each element, as we see in the next example:

    Stream.of("d2", "a2", "b1", "b3", "c")
        .map(s -> {
            System.out.println("map: " + s);
            return s.toUpperCase();
        })
        .anyMatch(s -> {
            System.out.println("anyMatch: " + s);
            return s.startsWith("A");
        });
    
    // map:      d2
    // anyMatch: D2
    // map:      a2
    // anyMatch: A2

    The operation anyMatch returns true as soon as the predicate applies to the given input element. This is true for the second element passed "A2". Due to the vertical execution of the stream chain, map has only to be executed twice in this case. So instead of mapping all elements of the stream, map will be called as few as possible.

    2.4 Stateful operation

    Sorting is a special kind of intermediate operation. It's a so-called stateful operation since in order to sort a collection of elements you have to maintain state during ordering.

    Executing this example results in the following console output:

    Stream.of("d2", "a2", "b1", "b3", "c")
        .sorted((s1, s2) -> {
            System.out.printf("sort: %s; %s\n", s1, s2);
            return s1.compareTo(s2);
        })
        .filter(s -> {
            System.out.println("filter: " + s);
            return s.startsWith("a");
        })
        .map(s -> {
            System.out.println("map: " + s);
            return s.toUpperCase();
        })
        .forEach(s -> System.out.println("forEach: " + s));
        
    //output
    sort:    a2; d2
    sort:    b1; a2
    sort:    b1; d2
    sort:    b1; a2
    sort:    b3; b1
    sort:    b3; d2
    sort:    c; b3
    sort:    c; d2
    filter:  a2
    map:     a2
    forEach: A2
    filter:  b1
    filter:  b3
    filter:  c
    filter:  d2

    First, the sort operation is executed on the entire input collection. In other words, sorted is executed horizontally. So in this case sorted is called eight times for multiple combinations on every element in the input collection.

    Once again we can optimize the performance by reordering the chain:

    Stream.of("d2", "a2", "b1", "b3", "c")
        .filter(s -> {
            System.out.println("filter: " + s);
            return s.startsWith("a");
        })
        .sorted((s1, s2) -> {
            System.out.printf("sort: %s; %s\n", s1, s2);
            return s1.compareTo(s2);
        })
        .map(s -> {
            System.out.println("map: " + s);
            return s.toUpperCase();
        })
        .forEach(s -> System.out.println("forEach: " + s));
    
    // filter:  d2
    // filter:  a2
    // filter:  b1
    // filter:  b3
    // filter:  c
    // map:     a2
    // forEach: A2

    In this example sorted is never been called because filter reduces the input collection to just one element. So the performance is greatly increased for larger input collections.

    2.5 Reusing Streams

    Stream<String> stream =
        Stream.of("d2", "a2", "b1", "b3", "c")
            .filter(s -> s.startsWith("a"));
    
    stream.anyMatch(s -> true);    // ok
    stream.noneMatch(s -> true);   // exception

    To overcome this limitation we have to to create a new stream chain for every terminal operation we want to execute, e.g. we could create a stream supplier to construct a new stream with all intermediate operations already set up:

    Supplier<Stream<String>> streamSupplier =
        () -> Stream.of("d2", "a2", "b1", "b3", "c")
                .filter(s -> s.startsWith("a"));
    
    streamSupplier.get().anyMatch(s -> true);   // ok
    streamSupplier.get().noneMatch(s -> true);  // ok

    3. Operations

    • Intermediate Operations
      • map
      • filter
      • sorted
    • terminal Operations
      • collect
      • forEach
      • reduce

    4. Different kind of streams

    Streams can be created from various data sources, especially collections. Lists and Sets support new methods stream() and parallelStream() to either create a sequential or a parallel stream. Parallel streams are capable of operating on multiple threads and will be covered in a later section of this tutorial. We focus on sequential streams for now:

    4. References

    https://www.baeldung.com/java-8-streams

    https://www.geeksforgeeks.org/stream-in-java/

    https://futurecreator.github.io/2018/08/26/java-8-streams/

    https://winterbe.com/posts/2014/07/31/java8-stream-tutorial-examples/

    5. Practices

    https://github.com/demyank88/JAVA8_Practices/tree/master/src/com/madhusudhan/j8/streams

     

    'StaticPL > JAVA' 카테고리의 다른 글

    Threadlocal  (0) 2019.08.19
    Thread and Runnable  (0) 2019.08.18
    Difference between Abstract Class and Interface  (0) 2019.08.18
    JAVA Collection Framework  (0) 2019.08.18
    String, Char, StringBuilder, and StringBuffer  (0) 2019.08.18

    댓글

Designed by Tistory.