Skip to content

Latest commit

 

History

History
777 lines (707 loc) · 25.3 KB

01-using-streams.asc

File metadata and controls

777 lines (707 loc) · 25.3 KB

Using Streams

Objective
Describe the Stream interface and pipelines; create a stream by using the Arrays.stream() and IntStream.range() methods; identify the lambda operations that are lazy.

One of the biggest things about Java 8 is Streams. A Stream is a data stream. The data can be Strings, numbers, or any other object. This data goes through a series of operations, and the set of these operations is called pipeline. Lambda expressions almost always represent these operations. So it is essential to have mastered the whole chapter on lambda, as all those concepts will now be used to form a Stream.

From the following examples, this explanation will become more evident.

Creating a Stream

Typically, a Stream is created from a dataset, such as a list or other collection type. The purpose of the certification makes explicit that it is necessary to know the Arrays.stream() and IntStream.range() methods. But in addition to these, some other common ways to create a Stream will be presented.

  1. You can create a stream from an Array using the Arrays.stream() method.

    src/org/j6toj8/streams/usingstreams/Stream_ArraysStream.java
    link:../../../src/org/j6toj8/streams/usingstreams/Streams_ArraysStream.java[role=include]
    console output
    A
    B
    C
  2. You can create a stream from a number range using the IntStream.range() method.

    src/org/j6toj8/streams/usingstreams/Stream_IntRangeStream.java
    link:../../../src/org/j6toj8/streams/usingstreams/Streams_IntRangeStream.java[role=include]
    console output
    0
    1
    2
    3

    Note that the first argument (number 0) is inclusive, while the second argument (number 4) is exclusive. Therefore the console output has only the numbers 0 to 3.

  3. You can create a Stream from a list.

    src/org/j6toj8/streams/usingstreams/Streams_ListStream.java
    link:../../../src/org/j6toj8/streams/usingstreams/Streams_ListStream.java[role=include]
    console output
    A
    B
    C
  4. You can create a Stream from specific elements using the Stream.of method.

    src/org/j6toj8/streams/usingstreams/Streams_Of.java
    link:../../../src/org/j6toj8/streams/usingstreams/Streams_Of.java[role=include]
    console output
    A
    B
    1
    2
    3.0
    4.0

    In this case a Stream was created that contains: String, Character, Integer,Long, Float and Double.

Streams operations

Operations done on a Stream will form its pipeline. Operations that can be performed on a Stream are divided into Intermediate Operations and Final Operations. Stream can contain numerous intermediate operations, but only one final operation. In the previous examples the only operation used was forEach, which is a final operation. Other operations will be presented below.

Intermediate Operations
  1. You can ignore elements of a stream with the skip operation.

    src/org/j6toj8/streams/usingstreams/Stream_Skip.java
    link:../../../src/org/j6toj8/streams/usingstreams/Streams_Skip.java[role=include]
    console output
    2
    3

    Note that in this case elements 0 and 1 were ignored, as they are the first two elements of Stream. This was due to the existence of the skip operation.

  2. You can limit the amount of elements that will be processed using the limit operation.

    src/org/j6toj8/streams/usingstreams/Streams_Limit.java
    link:../../../src/org/j6toj8/streams/usingstreams/Streams_Limit.java[role=include]
    console output
    0
    1

    In this case only the first 2 elements were printed on the console because the limit operation limited the amount of elements to be processed.

  3. You can filter Stream elements using the filter operation.

    src/org/j6toj8/streams/usingstreams/Streams_Filter.java
    link:../../../src/org/j6toj8/streams/usingstreams/Streams_Filter.java[role=include]
    console output
    0
    2

    In this case only the even elements were printed, as the filter operation limited to those that have remainder division by 2 equal to 0.

  4. You can filter out repeated Stream elements using the distinct operation.

    src/org/j6toj8/streams/usingstreams/Streams_Distinct.java
    link:../../../src/org/j6toj8/streams/usingstreams/Streams_Distinct.java[role=include]
    console output
    A
    B
    C
    F

    Note that in this case the repeated stream elements ("A" and "B") were ignored and only presented once.

    The distinct operation uses the equals and hashCode methods, so make sure they are implemented correctly if you are using an object type that you create. In the example were used objects of type String, which already have this implementation by default.

  5. You can apply a transformation to Stream elements using the map operation.

    src/org/j6toj8/streams/usingstreams/Streams_Map.java
    link:../../../src/org/j6toj8/streams/usingstreams/Streams_Map.java[role=include]
    console output
    0
    2
    4
    6

    Note that in this case, the elements underwent a transformation, which was multiplication by 2 before being printed on the console.

  6. You can sort the elements of a Stream using the sorted operation.

    src/org/j6toj8/streams/usingstreams/Streams_Sorted.java
    link:../../../src/org/j6toj8/streams/usingstreams/Streams_Sorted.java[role=include]
    console output
    A
    A
    B
    B
    C
    F
    G
    T
    Y

    In this case, all elements are sorted using the natural order of the String objects, as they already implement the Comparable interface and are presented in alphabetical order. There is also a version of the sort method that takes as its argument an implementation of Comparator if you want to sort it otherwise.

  7. You can observe the elements passing through a Stream using the peek operation.

    src/org/j6toj8/streams/usingstreams/Streams_Peek.java
    link:../../../src/org/j6toj8/streams/usingstreams/Streams_Peek.java[role=include]
    console output
    Peek: G
    ForEach: G
    Peek: T
    ForEach: T
    Peek: Y
    ForEach: Y
    Peek: A
    ForEach: A

    The peek operation works only to observe what is going through Stream. It can be very useful for performing debug or log. In this case, the elements are being printed twice on the console because the peek and forEach methods are both doing the same action. However, in real applications, usually the final operation will not be a forEach, so it will make sense to use peek.

  8. You can transform a Stream from multiple Arrays into a single continuous Stream using the flatMap method.

    src/org/j6toj8/streams/usingstreams/Streams_FlatMap.java
    link:../../../src/org/j6toj8/streams/usingstreams/Streams_FlatMap.java[role=include]
    console output
    A
    B
    C
    D
    E
    F
    G
    H
    I

    Note that in this case there are 3 distinct Arrays. Then create a Stream containing 3 Arrays. The typical scenario would be that each Stream element was an Array object. However, using the flatMap operation, a Stream is created for each of these Arrays, which are joined and form a single continuous Stream.

Final operations
  1. You can perform a final action for each Stream element using the forEach operation, as shown in the previous examples.

  2. You can retrieve the largest and smallest value of a Stream using the max and min final operations. And it is also possible to retrieve the number of elements of a Stream using the count final operation.

    src/org/j6toj8/streams/usingstreams/Streams_MaxMinCount.java
    link:../../../src/org/j6toj8/streams/usingstreams/Streams_MaxMinCount.java[role=include]
    console output
    Max: 9
    Min: 1
    Count: 8

    For max and min operations, it is necessary to pass as an argument which comparator will be used. Since numbers have a natural order, that is, they implement the Comparable interface, it is possible to use a comparator that uses that natural order, which is Comparator.naturalOrder(). If it is a type of object that does not have a natural order, it is necessary to pass another implementation of Comparator as an argument.

    The max and min operations return Optional because if Stream is empty it will be empty Optional. Since Java 8, with the addition of the Optional class, this has been preferred over returning null as it facilitates functional programming. The count operation does not need an Optional, because even with an empty Stream will return 0.

  3. You can get the first element of the Stream using the final findFirst operation, or any element with findAny.

    src/org/j6toj8/streams/usingstreams/Streams_FindFirstAny.java
    link:../../../src/org/j6toj8/streams/usingstreams/Streams_FindFirstAny.java[role=include]
    console output
    First: 7
    Any: 7

    In this case, because Stream is sequential and not parallel, both results are equal. In parallel Streams, which will be presented in another section, the findAny operation may yield different results.

    Like the max and min operations presented earlier, findAny and findFirst return an empty Optional if Stream is empty.

  4. You can verify that Stream elements meet some validation by using the allMatch, anyMatch, and noneMatch final operations.

    src/org/j6toj8/streams/usingstreams/Streams_Match.java
    link:../../../src/org/j6toj8/streams/usingstreams/Streams_Match.java[role=include]
    console output
    anyMatch: true
    allMatch: false
    noneMatch: false

    Note that in the first operation it is checked that any element is greater than 5. In the second, if all elements are greater than 5. And in the third, if no element is greater than 5.

  5. You cannot call more than one final operation on the same Stream.

    src/org/j6toj8/streams/usingstreams/Streams_ReuseStream.java
    link:../../../src/org/j6toj8/streams/usingstreams/Streams_ReuseStream.java[role=include]
    console output
    7
    2
    1
    Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
    	at java.util.stream.AbstractPipeline.sourceStageSpliterator(AbstractPipeline.java:279)
    	at java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:580)
    	at org.j6toj8.streams.usingstreams.Streams_ReuseStream.main(Streams_ReuseStream.java:11)
Pipeline
  1. You can create a pipeline with multiple operations in a single Stream.

    src/org/j6toj8/streams/usingstreams/Streams_Pipeline.java
    link:../../../src/org/j6toj8/streams/usingstreams/Streams_Pipeline.java[role=include]
    console output
    8
    12

    To understand all operations performed in this pipeline, you must understand step by step:

    1. A Stream was created containing all numbers from 0 to 9.

    2. A filter was applied keeping only the even numbers: 0, 2, 4, 6 and 8.

    3. The first two numbers were ignored, keeping only: 4, 6 and 8.

    4. Processing was limited to the first two numbers: 4 and 6.

    5. Multiplication by 2 was applied to each element, resulting in 8 and 12.

    6. Both elements were printed on the console.

  2. The Stream will only actually be created after any operation has been performed on it.

    src/org/j6toj8/streams/usingstreams/Streams_ChangeBackingList.java
    link:../../../src/org/j6toj8/streams/usingstreams/Streams_ChangeBackingList.java[role=include]
    console output
    1
    2
    3
    4

    Note that even though Stream was apparently created before adding number 4 to the list, it prints this number on the console. This is because Stream was only created when some operation was done on it, i.e., when forEach was invoked.

  3. You can chain the final Stream operation using lambda expressions in the Optional class.

    src/org/j6toj8/streams/usingstreams/Streams_Optional.java
    link:../../../src/org/j6toj8/streams/usingstreams/Streams_Optional.java[role=include]

    Note that the ifPresent method is from the Optional class, even though in the second example, it might look like it is part of Stream. In other words, the final operation is max, and ifPresent is a called in Optional and no longer in Stream

Lazy Invocation

Intermediate operations of a Stream are only performed when necessary. That is, even if the operation is present in pipeline, it is not certain that it will be executed.

  1. Nothing will be done if Stream does not contain a final operation.

    src/org/j6toj8/streams/usingstreams/Streams_LazyNoFinal.java
    link:../../../src/org/j6toj8/streams/usingstreams/Streams_LazyNoFinal.java[role=include]

    In this case nothing is printed on the console because no final operations were applied to Stream. That is, if there is nothing consuming the result of this Stream, Java does not have to execute the created pipeline.

  2. Other intermediate operations are also not usually performed if not required.

    src/org/j6toj8/streams/usingstreams/Streams_LazyMap.java
    link:../../../src/org/j6toj8/streams/usingstreams/Streams_LazyMap.java[role=include]
    console output
    Peek: 0
    ForEach: 0
    Peek: 1
    ForEach: 1
    Peek: 2
    ForEach: 2

    Note that even if the peek operation is before the limit operation, it is not performed for all Stream elements, only for those that will actually be used.

Primitive Streams

There are specific streams for some primitive types like double, int and long. They have the advantage of avoiding Boxing and Unboxing by providing some more specialized methods as shown below.

  1. You can create Streams of primitive types with the classes: DoubleStream, IntStream and LongStream.

    src/org/j6toj8/streams/usingstreams/primitives/Streams_Primitives.java
    link:../../../src/org/j6toj8/streams/usingstreams/primitives/Streams_Primitives.java[role=include]
    console output
     DoubleStream
    1.12.23.3
     IntStream
    123
    123
     LongStream
    123
    123
  2. You can transform a common Stream into a Stream of primitives using mapTo* operations.

    src/org/j6toj8/streams/usingstreams/primitives/Streams_MapTo.java
    link:../../../src/org/j6toj8/streams/usingstreams/primitives/Streams_MapTo.java[role=include]
    console output
     Stream to IntStream
    1234
     Stream to LongStream
    1234
     Stream to DoubleStream
    1.02.03.04.0
  3. You can generate infinite Streams with the generate method.

    src/org/j6toj8/streams/usingstreams/primitives/Streams_Generate.java
    link:../../../src/org/j6toj8/streams/usingstreams/primitives/Streams_Generate.java[role=include]
    console output
     infinity IntStream of random numbers
    2111846625
    -1692075394
    122693397
    
     infinity DoubleStream of random numbers
    0.913037010633669
    0.23669861350384735
    0.32655918031847697

    In this case the Streams are really infinite. Only 3 numbers of each were displayed as there is the limit operation, otherwise program execution would also be endless.

  4. You can use the rangeClosed operation instead of range, making the code more readable.

    src/org/j6toj8/streams/usingstreams/primitives/Streams_RangeClosed.java
    link:../../../src/org/j6toj8/streams/usingstreams/primitives/Streams_RangeClosed.java[role=include]
    console output
    123
    1234

    Note that in the call using range, the last number is unique (not part of Stream). In rangeClosed, both the first and last numbers are inclusive (they are part of Stream).

  5. You can generate various Streams statistics using the summaryStatistics operation.

    src/org/j6toj8/streams/usingstreams/primitives/Streams_Statistics.java
    link:../../../src/org/j6toj8/streams/usingstreams/primitives/Streams_Statistics.java[role=include]
    console output
    Count: 10
    Max: 9
    Min: 0
    Sum: 45
    Average: 4.5

Reduce and Collectors

Reduce

Reduce is one of the main final operations that can be done on a Stream. It is an operation that transforms multiple Stream values into a single value. Several operations previously presented are a type of Reduce, such as: max, min and summaryStatistics. However, these operations are not always sufficient, so there are reduce methods. They allow the implementation of Reduce custom operations.

  1. You can create a custom Reduce operation with the reduce() method that takes 1 argument.

    src/org/j6toj8/streams/usingstreams/primitives/Streams_Reduce.java
    link:../../../src/org/j6toj8/streams/usingstreams/reduce/Streams_Reduce.java[role=include]
    console output
    336

    In this case a Reduce is being made where the result of the previous operation is passed to the next execution. That is, first we multiply 7 * 2, which is 14. Then the function is called again passing as argument the previous result (14) and the next Stream number (3). The result is 42. Then the function is called one last time passing the previous result (42) and the next Stream number (8), which gives the result of 336.

  2. You can create a Reduce operation by entering the identity value.

    src/org/j6toj8/streams/usingstreams/primitives/Streams_ReduceIdentity.java
    link:../../../src/org/j6toj8/streams/usingstreams/reduce/Streams_ReduceIdentity.java[role=include]
    console output
    336

    In this case you can enter the identity value of the role. The concept of value or identity function is a bit more complex, but for certification just understand that it represents a neutral value. That is, for the multiplication operation, the identity value is 1, because any value multiplied by 1 results in itself. If it were a sum operation, the identity value would be 0 because any value added to 0 results in itself.

    Also, if Stream is empty, the identity value is returned. Therefore, unlike the previous example, it is not necessary to return an Optional.

  3. You can create a Reduce operation that can be performed on multiple Threads and then combined into a single value.

    src/org/j6toj8/streams/usingstreams/primitives/Streams_ReduceCombiner.java
    link:../../../src/org/j6toj8/streams/usingstreams/reduce/Streams_ReduceCombiner.java[role=include]
    console output
    336

    In this case, an additional argument is passed. It is the combination function. This function is used when Stream is parallel, in other words, it uses more than one thread. It takes the value returned by 2 or more threads and combines them into a single value. In a multiplication operation, the combination is also a multiplication. That is, if the first thread multiplies 7 and 2, resulting in 14, and the second thread multiplies 3 and 8, resulting in 24, the combination function only needs to multiply 14 by 24 to reach the value of 336. Thus, the function combination only makes sense in a parallel Stream, which will be presented in the next chapter.

Collect

The collect final operation is also a type of Reduce but is used for mutable objects. That is, instead of using the reduce operation with String, it would probably be more efficient to use the collect operation with the StringBuilder class to avoid creating multiple String objects. Because Java uses many mutable objects, including lists and maps, generally the collect operation will be more efficient than reduce.

Because they are very common, there are several Collectors already implemented in Java, available in the Collectors class.

  1. It is possible to use a Collector that joins several Strings.

    src/org/j6toj8/streams/usingstreams/primitives/Streams_CollectorJoining.java
    link:../../../src/org/j6toj8/streams/usingstreams/collect/Streams_CollectorJoining.java[role=include]
    console output
    ABC
  2. You can use a Collector to represent each element as a number and average them.

    src/org/j6toj8/streams/usingstreams/primitives/Streams_CollectorAveragingInt.java
    link:../../../src/org/j6toj8/streams/usingstreams/collect/Streams_CollectorAveragingInt.java[role=include]
    console output
    6.2
  3. You can use a Collector to store the elements of a Stream in a new collection.

    src/org/j6toj8/streams/usingstreams/primitives/Streams_CollectorToCollect.java
    link:../../../src/org/j6toj8/streams/usingstreams/collect/Streams_CollectorToCollect.java[role=include]
    console output
    ArrayList: [1, 2, 3, 4]
    HashSet: [1, 2, 3, 4]
    LinkedList: [1, 2, 3, 4]
    TreeSet: [1, 2, 3, 4]
  4. You can use a Collector to store the elements of a Stream in a map.

    src/org/j6toj8/streams/usingstreams/primitives/Streams_CollectorToMap.java
    link:../../../src/org/j6toj8/streams/usingstreams/collect/Streams_CollectorToMap.java[role=include]
    console output
    {Roseany=7, Amélia=6, Rodrigo=7, Rinaldo=7, Luiz=4}
  5. It is also possible to store in a map in cases where the key will be repeated. The third argument of the toMap method defines the rule to merge values to equal keys.

    src/org/j6toj8/streams/usingstreams/primitives/Streams_CollectorToMapDuplicateKey.java
    link:../../../src/org/j6toj8/streams/usingstreams/collect/Streams_CollectorToMapDuplicateKey.java[role=include]
    console output
    {4=Luiz, 6=Amélia, 7=Rinaldo,Rodrigo,Roseany}
  6. You can use a Collector that creates a map by grouping values that have the same key in a list.

    src/org/j6toj8/streams/usingstreams/primitives/Streams_CollectorGroupingBy.java
    link:../../../src/org/j6toj8/streams/usingstreams/collect/Streams_CollectorGroupingBy.java[role=include]
    console output
    {4=[Luiz], 6=[Amélia], 7=[Rinaldo, Rodrigo, Roseany]}
  7. You can also customize the way that values with equal keys will be combined.

    src/org/j6toj8/streams/usingstreams/primitives/Streams_CollectorGroupingByDownstream.java
    link:../../../src/org/j6toj8/streams/usingstreams/collect/Streams_CollectorGroupingByDownstream.java[role=include]
    console output
    {4=Luiz, 6=Amélia, 7=Rinaldo,Rodrigo,Roseany}

    Note that in this case the values were combined using another Collector, which grouped the names separating with commas.

  8. You can also define which type of map will be used for grouping.

    src/org/j6toj8/streams/usingstreams/primitives/Streams_CollectorGroupingByMapFactory.java
    link:../../../src/org/j6toj8/streams/usingstreams/collect/Streams_CollectorGroupingByMapFactory.java[role=include]
    console output
    {4=Luiz, 6=Amélia, 7=Rinaldo,Rodrigo,Roseany}

    Note that the result of this example is identical to the previous one, but one more argument was passed, which is the map constructor that should be used.

  9. You can use a Collector that partitions values in True or False from a function of type Predicate.

    src/org/j6toj8/streams/usingstreams/primitives/Streams_CollectorPartitioningBy.java
    link:../../../src/org/j6toj8/streams/usingstreams/collect/Streams_CollectorPartitioningBy.java[role=include]
    console output
    {false=[Luiz, Amélia], true=[Rinaldo, Rodrigo, Roseany]}

    Note that in this case the partitioning rule is the names beginning with R.

  10. You can also customize how the combination of partitioned values will be done.

    src/org/j6toj8/streams/usingstreams/primitives/Streams_CollectorPartitioningByDownstream.java
    link:../../../src/org/j6toj8/streams/usingstreams/collect/Streams_CollectorPartitioningByDownstream.java[role=include]
    console output
    {false=Luiz,Amélia, true=Rinaldo,Rodrigo,Roseany}

    Note that in this case the values were combined using another Collector, which merged the values of that same key into a single String separated by commas.

  11. You can add one more layer of transformation using a Collector using the mapping method.

    src/org/j6toj8/streams/usingstreams/primitives/Streams_CollectorMapping.java
    link:../../../src/org/j6toj8/streams/usingstreams/collect/Streams_CollectorMapping.java[role=include]
    console output
    {4=LUIZ, 6=AMÉLIA, 7=RINALDO,RODRIGO,ROSEANY}

    This type of code, while complex, may appear on the certification exam. It is recommended to practice these examples with an IDE to really understand their behaviors. Access the sample codes in this book to facilitate your study.

References