Saturday, 7 June 2014

2 Examples of Streams with Collections in Java 8

Finally Java 8 is here, after more than 2 years of JDK 7, we have a much expected Java 8 with lots of interesting feature. Though Lambda expression is the most talked item of coming Java 8 release, it wouldn't have been this much popular, if Collections were not improved and Stream API were not introduced. Java 8 is bringing on new Streams API java.util.stream package, which allow you to process elements of Java Collections in parallel. Java is inheritably sequential and there is no direct mean to introduce parallel processing at library level, stream API is going to fill that gap. By using this, you can filter elements of collection on given criterion e.g. if you have a List of orders, you can filter buy orders with sell orders, filter orders based upon there quantity and price and so on. You can also perform two of most popular functional programming functions e.g. 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 few of 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 List for these example, but you can use any Collection e.g. Set, LinkedList etc. By the way, use of Stream is not limited to Collections only, you can even use an array, a generator function, or an I/O channel as source. In most cases, a Stream pipeline in Java 8  consists a source, followed by zero or more intermediate stream operations e.g. filter() or map(); and a terminal operation such as forEach() or reduce().


How to use Streams with Collections in Java 8

Stream API examples from Java 8
As I said, we can use Stream with Collection as source in 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 e.g. 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 first example, we are using filter() method to filter all sell orders. In second example, we are using Stream APIs mapToDouble() method to calculate total for both price and quantity, which would have mean iterating over Collection and adding each elements price in total. Because of Java 8 lambda expression and stream API our code is reduced into pretty much one liner.


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 Java 8 Stream API

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();

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 think to note is that Stream do not modify the original source. For every operation, a new Stream is created and 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 name suggests, stateless operations doesn't retain any state from previously processed element, filter() and map() are two examples of stateless intermediate operation. On the other hand distinct() and sorted() are example of stateful operations, which can have state 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 going to be hugely popular among Java developers, given it's supports of functional programming idioms such as 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 boiler plate code, which was generated due to usage of anonymous inner class. If you still not downloaded Java 8 release, you can download from here.

No comments:

Post a Comment