Monday, September 27, 2021

How to use Stream with List and Collection in Java 8? filter + map Example Tutorial

Finally, Java 8 is here, after more than 2 years of JDK 7, we have a much expected Java 8 with lots of interesting features. Though Lambda expression is the most talked-about item of the coming Java 8 release, it wouldn't have been this much popular, if Collections were not improved and Stream API was not introduced. Java 8 is bringing on new Streams API java.util.stream package, which allows you to process elements of Java Collections in parallel. Java is inheritably sequential and there are no direct means to introduce parallel processing at the library level, stream API is going to fill that gap. By using Stream API in Java, you can filter elements of collection on a given criterion. For example,  if you have a list of orders, you can filter buy orders with sell orders, filter orders based upon their quantity and price, and so on.

You can also perform two of the most popular functional programming functions like map and reduce. java.util.stream class provides function such mapToInt(), mapToLong(), and map function to apply an operation on all elements of Collections.

By the way, these are just a few of the gems of Stream API, I am sure you will find several more when you start exploring lambda expression and java.util.stream package. In this tutorial, we will see 2 examples of using Java 8 Stream with Collections classes.

I have chosen a List for these examples, but you can use any Collection like a Set, or  LinkedList, etc.

By the way, the use of Stream is not limited to Collections only, you can even use an array, a generator function, or an I/O channel as the source. In most cases, a Stream pipeline in Java 8  consists of a source, followed by zero or more intermediate stream operations e.g. filter() or map(); and a terminal operation such as forEach() or reduce().

And,  if you are not familiar with Lambda Expression and Stream in Java then  I suggest you check to Learn Java Functional Programming with Lambdas & Streams by Rang Rao Karnam on Udemy, which explains Stream fundamentals in good detail.





How to use Streams with Collections in Java 8? Example

As I said, we can use Stream with Collection as a source in the previous paragraph, now is time to see some code in action. For this example, I have a list of Orders, where each order contains bare minimum details like Side (buy or sell), price, quantity, and security.

Once we initialized our list with some orders, we can perform interesting operations exposed by stream API. In the first example, we are using a filter() method to filter all sell orders.

In the second example, we are using the Stream APIs mapToDouble() method to calculate the total for both price and quantity, which would have to mean iterating over Collection and adding each element price in total. Because of Java 8 lambda expression and stream API, our code is reduced into pretty much one-liner.

Stream API examples from Java 8


And, here is our complete Java program to demonstrate how to use Stream API in Java:

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;

public class StreamDemo{

    public static void main(String args[]) {

        // Initialization of Collection
        List<Order> orderBook = new ArrayList<>();

        Order buyGoogle = new Order("GOOG.NS", 300, 900.30, Order.Side.BUY);
        Order sellGoogle = new Order("GOOG.NS", 600, 890.30, Order.Side.SELL);
        Order buyApple = new Order("APPL.NS", 400, 552, Order.Side.BUY);
        Order sellApple = new Order("APPL.NS", 200, 550, Order.Side.SELL);
        Order buyGS = new Order("GS.NS", 300, 130, Order.Side.BUY);

        orderBook.add(buyGoogle);
        orderBook.add(sellGoogle);
        orderBook.add(buyApple);
        orderBook.add(sellApple);
        orderBook.add(buyGS);

        // Java 8 Streams Example 1 : Filtering Collection elements
        // Filtering buy and sell order using filter() method of java.util.Stream class
        Stream<Order> stream = orderBook.stream();
        Stream buyOrders = stream
                           .filter((Order o) -> o.side().equals(Order.Side.BUY));
        System.out.println("No of Buy Order Placed :" 
                               + buyOrders.count());

        Stream<Order> sellOrders 
                           = orderBook
                                .stream()
                                .filter((Order o) -> o.side() == Order.Side.SELL);
        System.out.println("No of Sell Order Placed : " + sellOrders.count());

        // Java 8 Streams Example 2 : Reduce or Fold operation
        // Calculating total value of all orders
        double value = orderBook.stream()
                                .mapToDouble((Order o) -> o.price())
                                .sum();
        System.out.println("Total value of all orders : " + value);

        long quantity = orderBook.stream()
                                 .mapToLong((Order o) -> o.quantity())
                                 .sum();
        System.out.println("Total quantity of all orders : " + quantity);

    }

}

class Order {

    enum Side {
        BUY, SELL;
    }
    private final String symbol;
    private final int quantity;
    private double price;
    private final Side side;

    public Order(String symbol, int quantity, double price, Side side) {
        this.symbol = symbol;
        this.quantity = quantity;
        this.price = price;
        this.side = side;
    }

    public double price() {
        return price;
    }

    public void price(double price) {
        this.price = price;
    }

    public String symbol() {
        return symbol;
    }

    public int quantity() {
        return quantity;
    }

    public Side side() {
        return side;
    }
}

Output:
No of Buy Order Placed :3
No of Sell Order Placed : 2
Total value of all orders : 3022.6
Total quantity of all orders : 1800


Important points about Stream API in Java

Following are some important points about Stream API in Java

1. Stream API allows you to process Collection both sequentially and parallel. This is also useful for bulk data operation. You can create a sequential and parallel stream as follows :

List<Order> orders =  getListOfOrders();

// sequential version
Stream<Order> stream = orders.stream();

//parallel version
Stream<Order> parallelStream = orders.parallelStream();

The collection interface is enhanced to provide stream support. It now has a stream() method which returns a sequential Stream with this collection as its source. Once you get the reference of stream, you can perform bulk data operations with this collection.

2. One of the important things to note is that Stream does not modify the original source. For every operation, a new Stream is created and the original collection remains unmodified. Similarly, you cannot reuse Stream either. Reusing a closed stream will throw IllegalStateException as shown below :

Exception in thread "main" java.lang.IllegalStateException:
    stream has already been operated upon or closed
    at java.util.stream.AbstractPipeline.(AbstractPipeline.java:203)
    at java.util.stream.ReferencePipeline.(ReferencePipeline.java:94)
    at java.util.stream.ReferencePipeline$StatelessOp.(ReferencePipeline.java:618)
    at java.util.stream.ReferencePipeline$2.(ReferencePipeline.java:163)
    at java.util.stream.ReferencePipeline.filter(ReferencePipeline.java:162)
    at Test.main(Test.java:33)

3. Stream operations are mainly divided into two categories: intermediate and terminal operations. Intermediate operations such as filter() or map() returns a new Stream, while terminal operations such as Stream.forEach() produce a result or side effect. After the terminal operation, the stream pipeline is considered consumed, and can no longer be used.

4. Intermediate operations are also of two types stateless and stateful. As the name suggests, stateless operations don't retain any state from the previously processed element, filter() and map() are two examples of stateless intermediate operations

On the other hand distinct() and sorted() are examples of stateful operations, which can have states from previously processed elements while processing new elements.

That's all about how to use Streams with Collections in Java 8. In my opinion, Stream will be going to be hugely popular among Java developers, given its supports of functional programming idioms such as a map and reduce along with parallel processing capability, support for filtering elements from Collection. 

Lambda expression and Stream API will also help in removing lots of boilerplate code, which was generated due to usage of the anonymous inner class. If you still have not downloaded Java 8 release, you can download it from here.



Related Java 8 Tutorials
If you are interested in learning more about the new features of Java 8, here are my earlier articles covering some of the important concepts of Java 8:
  • The Java Developer RoadMap (guide)
  • 20 Examples of Date and Time in Java 8 (tutorial)
  • How to use Stream class in Java 8 (tutorial)
  • How to use filter() method in Java 8 (tutorial)
  • How to use forEach() method in Java 8 (example)
  • How to join String in Java 8 (example)
  • How to convert List to Map in Java 8 (solution)
  • How to use peek() method in Java 8 (example)
  • 5 Books to Learn Java 8 from Scratch (books)
  • How to convert the stream to array in Java 8 (tutorial)
  • Java 8 Certification FAQ (guide)
  • Java 8 Mock Exams and Practice Test (test)
  • 50+ Java Lambda Expression Interview Questions (lambda questions)
  • 15 Java Stream and Functional Programming questions (stream questions)

Thanks for reading this article so far. If you like this article then please share it with your friends and colleagues. If you have any questions, doubts, or feedback then please drop a comment and I'll try to answer your question.

P.S.: If you want to learn more about new features in Java 8 then please see the tutorial What's New in Java 8. It explains all important features of Java 8 like lambda expressions, streams, functional interface, Optionals, new date, and time API, and other miscellaneous changes.

5 comments:

  1. Great tutorial! I have been following your blog posts for around 9 months now.. and I feel I have greatly enhanced my knowledge and my enthusiasm for Java due to your blogs.

    I don't have any specific inputs on this specific blog 'cus I am learning about Java 8 only by reading your posts.

    ReplyDelete
  2. Hi. I can't understand, why I can't run your code. On lines
    Stream buyOrders = stream.filter((Order o) -> o.side().equals(Order.Side.BUY));
    Stream sellOrders = orderBook.stream().filter((Order o) -> o.side() == Order.Side.SELL);
    double value = orderBook.stream().mapToDouble((Order o) -> o.price()).sum();
    long quantity = orderBook.stream().mapToLong((Order o) -> o.quantity()).sum();

    I got exceptions

    com/mycompany/java8_test2/StreamDemo.java:[35,41] error: incompatible types: incompatible parameter types in lambda expression
    com/mycompany/java8_test2/StreamDemo.java:[38,54] error: incompatible types: incompatible parameter types in lambda expression
    com/mycompany/java8_test2/StreamDemo.java:[43,54] error: incompatible types: incompatible parameter types in lambda expression
    com/mycompany/java8_test2/StreamDemo.java:[46,53] error: incompatible types: incompatible parameter types in lambda expression

    How to fix this? I use Netbeans with latest version and Oracle Java 1.8.0.

    ReplyDelete
  3. The code you supplied does not compile - you missed a generic type in orderBook declaration. Cheers!

    ReplyDelete
  4. @Peenuts, thanks, glad that you find these Java 8 streams and collections example useful.

    ReplyDelete