Friday, January 17, 2020

Java Stream API Tutorial

If we have to point out the most important inclusion in Java 8 apart from lambda expressions then that has to be Stream API in Java. Stream API usually works in conjunction with lambda expression and provide an easy yet efficient way to perform data manipulation operations like sort, filter, map, reduce etc.

This Javs Stream API tutorial gives an overview of what exactly is a stream in Stream API and what all types of Stream operations are there.


What is Stream in Java Stream API

A stream can be visualized as a pipeline. A stream pipeline consists of a source (which might be an array, a collection, a generator function, an I/O channel, etc), zero or more intermediate operations (which transform a stream into another stream, such as filter(Predicate)), and a terminal operation (which produces a result or side-effect, such as count() or forEach(Consumer)).

Stream API in Java
Stream data flow

As shown in the figure beginning of the stream has a data source like an array, collection, file. From there data moves through the stream where the stream operation like sorting, filtering etc. is performed on the data. Here note one important point that stream operations do not modify the data source in any way. Any stream operation results in a creation of a new stream.
As example if you are filtering a stream using some condition that will result in a creation of new stream that produces the filtered results.

How to obtain stream in Java

Streams can be obtained in a number of ways in Java Stream API. Some examples include:

  • From a Collection via the stream() and parallelStream() methods;
    List<Integer> myList = Arrays.asList(7000, 5000, 4000, 24000, 17000, 6000);
    Stream<Integer> stream = myList.stream();
    

    Refer Parallel Stream in Java Stream API to know about parallel stream in stream API.

  • From an array via Arrays.stream(Object[]);
    String[] strArr = {"x", "y", "z"};
    Stream<String> str = Arrays.stream(strArr);
    
  • From static factory methods on the stream classes, such as Stream.of(Object[]), IntStream.range(int, int) or Stream.iterate(Object, UnaryOperator);
    IntStream intStream = IntStream.range(1,10);
    //prints integers 1-9
    intStream.forEach(System.out::println);
    
  • The lines of a file can be obtained from BufferedReader.lines();
    Path path = Paths.get("F:\\NETJS\\Fromat code.txt");
    Stream<String> lines = Files.newBufferedReader(path).lines();
    
  • Streams of file paths can be obtained from methods in Files;
  • Streams of random numbers can be obtained from Random.ints();
  • Numerous other stream-bearing methods in the JDK, including BitSet.stream(), Pattern.splitAsStream(java.lang.CharSequence), and JarFile.stream().

Java Stream Example

At this point let’s see an example to actually see Java Stream API in action–

In this example objective is to take a list as an input and sort it taking only those elements of list which are greater than 5 and finally print it.

List<Integer> numList = Arrays.asList(34, 6, 3, 12, 65, 1, 8);
numList.stream().filter((n) -> n > 5).sorted().forEach(System.out::println);

Output

6
8
12
34
65

In the Java stream example it can be seen that the list is the data source for the stream and there are two intermediate operations – filter and sorted.

Filter condition here is; take only those elements of the list which are greater than 5. In next stream operation of the stream pipeline,filtered output of the last stream is sorted using sorted method of the Stream API.

Terminal operation here is forEach statement (provided in Java 8) which iterates the sorted result and displays them.

Types of Stream operations in Java

Stream operations are divided into intermediate and terminal operations, and are combined to form stream pipelines.

Intermediate operations in Java Stream return a new stream. They are always lazy; executing an intermediate operation such as filter() does not actually perform any filtering, but instead creates a new stream that, when traversed, contains the elements of the initial stream that match the given predicate. Traversal of the pipeline source does not begin until the terminal operation of the pipeline is executed.

Examples of intermediate operations are map, filter, flatMap, sorted, distinct.

  • filter- filter method returns a stream that consists filtered elements matching the passed Predicate.
    For example- To filter those names from a List that doesn't start with "A".
    List<String> nameList = Arrays.asList("Ram", "Amit", "Ashok", "Manish", "Rajat");
    nameList.stream().filter(n -> !n.startsWith("A")).collect(Collectors.toList()).forEach(System.out::println);
    
  • sorted- sorted method returns a stream consisting of the elements of this stream, sorted according to natural order or there is another variant where custom comparator can be provided.
    List<Integer> myList = Arrays.asList(7000, 5000, 4000, 24000, 17000, 6000);
    myList.stream().sorted().forEach(System.out::println);
    
  • Refer Map Operation in Java Stream API to see examples of Map operation.
  • Refer FlatMap in Java to see examples of FlatMap.

Terminal operations in Java Stream such as Stream.forEach or IntStream.sum, may traverse the stream to produce a result or a side-effect. After the terminal operation is performed, the stream pipeline is considered consumed, and can no longer be used; if you need to traverse the same data source again, you must return to the data source to get a new stream.

Examples of terminal operations are forEach, reduce, collect, min, max, count.

Features of Java Stream

Some of the features of the Java Stream API are-

  • No storage- A stream is not a data structure that stores elements; instead, it conveys elements from a source such as a data structure, an array, a generator function, or an I/O channel, through a pipeline of computational operations.
  • Functional in nature- An operation on a stream produces a result, but does not modify the data source. As example filtering a Stream obtained from a collection produces a new Stream without the filtered elements, rather than removing elements from the source collection.
  • Lazy behavior- Intermediate operations are always lazy. These operations do not start as soon as you reach that intermediate operation, it’s only when stream hits the terminal operation that it start executing operations.

    For example, if you execute the following code you won’t get any output as only filter operation is there which is an intermediate operation and it won’t execute unless until there is a terminal operation.

    List<Integer> numList = Arrays.asList(34, 6, 3, 12);  
    numList.stream().filter((n) -> {
        System.out.println("While filtering - " + n);
        return true;
    });
    

    If you add a terminal operation like forEach in this code then only both the operation will get executed.

    List<Integer> numList = Arrays.asList(34, 6, 3, 12);  
    List<Integer> numList = Arrays.asList(34, 6, 3, 12);  
    numList.stream().filter((n) -> {
        System.out.println("While filtering - " + n);
        return true;
    }).forEach(n -> System.out.println("forEach iteration - " + n));
    
  • Output

    While filtering - 34
    forEach iteration - 34
    While filtering - 6
    forEach iteration - 6
    While filtering - 3
    forEach iteration - 3
    While filtering - 12
    forEach iteration - 12
    

    Besides lazy execution also provides opportunities for optimization. For example, "find the first String with three consecutive vowels" need not examine all the input strings.

  • Possibly unbounded- While collections have a finite size, streams need not. Short-circuiting operations such as limit(n) or findFirst() can allow computations on infinite streams to complete in finite time.
  • Consumable- The elements of a stream are only visited once during the life of a stream. Once terminal operation is executed that stream is deemed consumed and it can’t be used again. A new stream must be generated to revisit the same elements of the source.

Stateless and Stateful operations in Java Stream

Intermediate operations in Java Stream API are further divided into stateless and stateful operations.

Stateless operations, such as filter and map, retain no state from previously seen element when processing a new element, each element can be processed independently of operations on other elements.

Stateful operations, such as distinct and sorted, may incorporate state from previously seen elements when processing new elements. Stateful operations may need to process the entire input before producing a result. For example, one cannot produce any results from sorting a stream until one has seen all elements of the stream.

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

That's all for this topic Stream API in Java 8. If you have any doubt or any suggestions to make please drop a comment. Thanks!

>>>Return to Java Advanced Tutorial Page


Related Topics

  1. Primitive Type Streams in Java Stream API
  2. Parallel Stream in Java Stream API
  3. Spliterator in Java
  4. Optional Class in Java 8
  5. Java Stream API Interview Questions

You may also like-

  1. Difference between Encapsulation and Abstraction in Java
  2. static method overloading or overriding in Java
  3. String in Java
  4. CyclicBarrier in Java concurrency
  5. ReentrantLock in Java concurrency
  6. How to fix the target type of this expression must be a functional interface error
  7. fail-fast Vs fail-safe iterator in Java
  8. Transaction in Java-JDBC