A Beginner Tutorial on Java Streams

The screenshot above explains the dictionary meaning of stream. And stream in java is no different. The dictionary meaning says:

stream – a continuous flow of liquid, air or gas

A quick google search

Stream in Java is also a sequence of values. As you can make a dam to aggregate the water in case of liquid stream; Stream in Java allows you to do many aggregate operation to manipulate the values of the stream easily and clearly. It’s perfectly all right if the meaning of aggregate operation is not clear right now. We will be writing a few code to demonstrate the usage of stream.

But Why Stream?

The answer to this question is important because it will provide a motivation for the Java users to use streams and related library in their day to day life.

Parallel Execution

Servers are getting powerful everyday. Over the years, servers are reducing the clock cycle and increasing cores to become more faster.

The biggest change is in the hardware environment, where designers have turned their attention away from increasing the clock speed of individual cores and towards the goal of placing increasingly large numbers of cores on to the same chip.

Maurice Naftalin in his Lambda FAQ

Therefore, Java wants to encourage developers to write more and more parallel code; so that the code could utilise server more efficiently.

Java provides very powerful support for concurrency and parallelism. But traditionally writing parallel code in java means initialising a executor service, making class implement runnable/callable and so on. We can say that writing code parallel in java has to follow different set of libraries/methods than what we do in sequential. (Just for an example, You can see how Kafka Consumer class is implemented using runnable here.)

Java wants to encourage syntax that allows us to write parallel or sequential code without any significant change. Streams facilitates this approach as we will see in the example below:

package com.company;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class StreamsExample {
public static void main(String[] args) {
List<String> stringList =
Arrays.asList("11","2", "3", "2", "2", "6", "11", "3");
findUniqueUsingForLoopSequential(stringList);
System.out.println("\n");
stringList.stream().distinct().forEach(distinctString -> System.out.println("Sequentially using Stream is "+distinctString));
System.out.println("\n");
stringList.parallelStream().distinct().forEach(distinctString -> System.out.println("Parallel using Stream is "+distinctString));
}
private static void findUniqueUsingForLoopSequential(List<String> stringList) {
List<String> uniqueStringList = new ArrayList<>();
for (String str : stringList){
if(!uniqueStringList.contains(str)) {
uniqueStringList.add(str);
System.out.println("Sequentially using for loop : Unique String in the list is "+str);
}
}
}
}

Here the code tries to find unique strings in a given list of strings. The code has been written in three different ways. But if you notice the two code using streams to find distinct string.

stringList.stream().distinct().forEach(distinctString -> System.out.println("Sequentially using Stream is "+distinctString));
       
stringList.parallelStream().distinct().forEach(distinctString -> System.out.println("Parallel using Stream is "+distinctString));

Both of the code to find distinct strings are almost same except for the word stream and parallelStream. As you would have guessed, parallelStream makes the operation of finding distinct string parallel without changing the much syntax. This is the power of syntax that we are talking about.

Declarative Style

Stream promotes declarative style of coding. Being more expressive or declarative means the syntax focusses on what it should do, rather than how it should be done.

Consider the example shown below:

List<String> stringList =
                Arrays.asList("11","2", "3", "2", "2", "6", "11", "3");

        List<String> uniqueStringList = new ArrayList<>();
        for (String str : stringList){
            if(!uniqueStringList.contains(str)) {
                uniqueStringList.add(str);
                System.out.println("Sequentially using for loop : Unique String in the list is "+str);
            }
        }
        
        
        stringList.stream().distinct().forEach(distinctString -> System.out.println("Sequentially using Stream is "+distinctString));
        

Both the for loop as well as the stream does the same task of finding the unique strings in the list of strings.

When we use the traditional for loop iterative approach, we write code to:

  • list to store the unique string
  • iterate over the given list
  • check the string in the list of unique string
  • print if found unique

Second approach to the same problem statement involved just one line which says it to filter and print. It does not care how it iterates. It does not care how it filters. User wants to filter and print and we tell java just that. How is left for the platform code.

Love for Functional Programming

Streams encourages to write more stateless code. Stream supports many functions which can be applied to a stream to modify, filter or collect the result.

package com.company;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
List<String> numList =
Arrays.asList("11","2", "3", "four", "5", "6", "7", "8");
numList.stream()
.filter(str -> !str.contains("3"))
.flatMap(str -> Stream.of(str + " " + " right? "))
.distinct()
.map(str -> str.split(" ")[0])
.limit(3)
.skip(1)
.sorted()
.forEach(str -> System.out.println(str));
}
}
view raw Main.java hosted with ❤ by GitHub

The code shown above, shows a series of operation done on a stream. Details of the operation is beyond the scope of this post. (Series of operation is called pipeline)But, we can appreciate Stream with providing us opportunity to apply and define multiple functions with the help of lambdas.

This statelessness facilitates parallel execution of streams. Failure of ensuring statelessness can result in wrong result.

Non-Interference

Stream source might be a mutable. The stream operations are intended to be used when the source is held constant during the execution of pipeline. In other words, stream operations do not mutate the stream source.

Streams try to be lazy

package com.company;
import java.util.Arrays;
import java.util.List;
public class StreamsExample {
public static void main(String[] args) {
List<String> stringList =
Arrays.asList("2", "3", "3");
int sum = stringList.stream()
.filter(str -> !str.contains("3"))
.mapToInt(Integer::parseInt)
.sum();
System.out.println("Sum is "+sum);
}
}

The stream in the example above takes sources as a list of string, filters all the string “3” and then convert that into integer and then return the sum. Laziness means stream won’t do a round of filtering on it values and then convert each string to integer and finally adds all the values in a string. Rather, it will wait for the terminal operation of sum, which is eager by nature and while adding the values in a stream, it will check to filter and convert into integer. As in, filtering and conversion of integer will happen while doing sum operation.

Streams are compact

Streams make syntax pretty compact. Streams provide a terser expression for very elaborate behaviour.

Streams are unbounded too. Short-circuiting operations like limit or findFirst can allow computations on infinite streams to complete in finite time.

Why Not Streams?

  1. Overkill to use stream on small data sets. Streams are slow in comparison to the traditional iterators because of large overhead
  2. It takes some time to get used to the new declarative style as it abstracts more than what it shows
  3. A disadvantage of a Stream is that filters, mappings, etc., cannot throw checked exceptions
  4. Less support in terms of IDE
  5. Less familiarity in terms of Java developer world where people are more used to loops and iterative syntax.


References And Further Reading

https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html

http://cr.openjdk.java.net/~briangoetz/lambda/lambda-libraries-final.html

https://stackoverflow.com/questions/44180101/in-java-what-are-the-advantages-of-streams-over-loops

http://www.lambdafaq.org/what-is-a-stream/

If you liked this article and would like one such blog to land in your inbox every week, consider subscribing to our newsletter: https://skillcaptain.substack.com

Leave a Reply

Up ↑

%d bloggers like this: