Blogg

Här finns tekniska artiklar, presentationer och nyheter om arkitektur och systemutveckling. Håll dig uppdaterad, följ oss på LinkedIn

Callista medarbetare Magnus Larsson

Trying out functional programming in Java 8

// Magnus Larsson

I guess you have noted that functional programming is gaining popularity? With Java 8 even Java SE has received support for functional programming. In this blog I have tried to use Lambda Expressions and the Streaming API to address a typical every day problem, building a query API for the well-known object structure of Orders, Order Lines and Products.

After defining the query API I first implemented the API using a traditional imperative programming style and then I tried to re-implement it using a more functional oriented programming style. Let’s see how that worked out…

Object model and query API

Let’s start with a minimalistic (close to oversimplified) model over the well-known structure Order, Order Line and Product:

I decided to write a query API that could be used to answer the following questions:

  1. What products from category X have been ordered in the date interval M to N? I want them ordered by their weight!

  2. What products with weight from X to Y have been sold in orders with an order value from M to N? I want them ordered by their product ID!

First I defined a Java interface:

public interface QueryApi {

    public List<Product> getProductsByDateAndCategoryOrderByWeight(
        LocalDate minDate, 
        LocalDate maxDate, 
        String category);


    public List<Product> getProductsByOrderValueAndWeightOrderByProductId(
        int minOrderValue, 
        int maxOrderValue, 
        int minProductWeight, 
        int maxProductWeight);
}

Yes, I know, I like long descriptive method names ☺

A traditional imperative implementation

Next, I implemented the interface in the good old way using imperative programming. The first query-method getProductsByDateAndCategoryOrderByWeight() looks like:

public List<Product> getProductsByDateAndCategoryOrderByWeight(
    LocalDate minDate, LocalDate maxDate, String category) {

    List<Order> orders = getOrders();

    List<Product> products = new ArrayList<>();
    for (Order order : orders) {

        // Filter on order date
        LocalDate date = order.getOrderDate();
        if (date.isAfter(minDate) && date.isBefore(maxDate)) {

            List<OrderLine> orderLines = order.getOrderLines();
            for (OrderLine orderLine : orderLines) {

                // Filter on product category
                Product product = orderLine.getProduct();
                if (product.getCategory().equals(category)) {
                    products.add(product);
                }
            }

        }
    }

    // Remove any duplicates from the list of selected products
    products = new ArrayList<>(new HashSet<>(products));

    // Sort on product weight
    Collections.sort(products, new Comparator<Product>() {
        @Override
        public int compare(Product p1, Product p2) {
            return (p1.getWeight() < p2.getWeight()) ? -1 : 
                   ((p1.getWeight() == p2.getWeight()) ? 0 : 1);
        }
    });

    return products;
}

As you can see I assumed that we can acquire all orders using the method getOrders() and I don’t really care how it is implemented for now. The implementation of the query-method is based on traditional for-loops and if-statements to filter out the products that meet the specific requirements. The implementation wraps up with removing duplicates and sorting the result as required.

The second query-method getProductsByDateAndCategoryOrderByWeight() looks very similar in its structure with the only differences being the specific implementation of its filtering and sorting rules:

public List<Product> getProductsByOrderValueAndWeightOrderByProductId(
    int minOrderValue, int maxOrderValue, int minProductWeight, int maxProductWeight) {

    List<Order> orders = getOrders();

    List<Product> products = new ArrayList<>();
    for (Order order : orders) {

        // Filter on order value
        int orderValue = order.getOrderValue();
        if (minOrderValue <= orderValue && orderValue <= maxOrderValue) {

            List<OrderLine> orderLines = order.getOrderLines();
            for (OrderLine orderLine : orderLines) {

                // Filter on product weight
                Product product = orderLine.getProduct();
                int productWeight = product.getWeight();
                if (minProductWeight <= productWeight && productWeight <= maxProductWeight) {
                    products.add(product);
                }
            }
        }
    }

    // Remove any duplicates from the list of selected products
    products = new ArrayList<>(new HashSet<>(products));

    // Sort on product Id
    Collections.sort(products, new Comparator<Product>() {
        @Override
        public int compare(Product p1, Product p2) {
            return (p1.getId() < p2.getId()) ? -1 : 
                   ((p1.getId() == p2.getId()) ? 0 : 1);
        }
    });

    return products;
}

When looking at the result I see source code in common with the two query-methods and in general excessive code that is required to navigate through the object structure. Not good. Can we rewrite the code to be more concise and unambiguous?

First attempt using functional programming

Let’s see if we can improve using the Java 8 Stream API and support for Lambda Expressions!

If the Java 8 Stream API and support for lambda expressions are new to you it might be worth spending a few minutes reading: • Lambda Expressions, Part 1 and Part 2 • Java SE 8 Streams, Part 1 and Part 2

My first attempt looks like this:

public List<Product> getProductsByDateAndCategoryOrderByWeight(
    LocalDate minDate, LocalDate maxDate, String category) {

    return getOrders().stream()
        .filter  (o  -> o.getOrderDate().isAfter(minDate) && o.getOrderDate().isBefore(maxDate))
        .flatMap (o  -> o.getOrderLines().stream())
        .map     (ol -> ol.getProduct())
        .filter  (p  -> p.getCategory().equals(category))
        .distinct()
        .sorted  ((p1, p2) -> (p1.getWeight() < p2.getWeight()) ? -1 : ((p1.getWeight() == p2.getWeight()) ? 0 : 1))
        .collect (Collectors.toList());
}

Okay, the code is much more concise but what is actually going on?

First we convert the list of order-objects that getOrders() return into a stream of order-objects (so that we can start to use the Stream API):

getOrders().stream()

Next we apply a filter() function to the order-objects, in order to keep the ones that are within the specified date interval:

.filter(o -> o.getOrderDate().isAfter(minDate) && o.getOrderDate().isBefore(maxDate))

Note: I use type inference (explained in the lambda article above) to make the lambda expressions shorter like o -> o.getOrderDate()... instead of the longer (Order o) -> o.getOrderDate()....

Type inference should be used wisely, or else the code can be hard to read for others then the writer, but I hope you find it proper to use in this case. The type of the parameter is given by the result of the previous method call. In this case the call to getOrders().stream() returns a stream of Order objects and therefore the parameter o must be of type Order.

After that we need to attain the products in the selected orders. We find the products by traversing the order lines of each order. To transform the stream of orders to a stream of order lines we use the flatmap() function:

.flatMap(o -> o.getOrderLines().stream())

Note: Using the normal map() function to transform a stream of orders to a stream of order lines as observed in the following example:

.map(o -> o.getOrderLines())

… is not working since the map() function should return a stream of lists of order lines instead of a stream of order lines. The flatmap() function on the other hand is capable of flattening out multiple streams into one stream, providing us with one single stream of order lines. Note that since the flatmap() function works on streams of streams, we have to transform the list of order lines that the method o.getOrderLines() returns to a stream of order lines with a call to the stream() method on the list.

Given a stream of order lines we can easily transform it to a stream of products as:

.map(ol -> ol.getProduct())

…and filter out the products from the selected category:

.filter(p -> p.getCategory().equals(category))

We wrap up the processing of the stream with removing any duplicates (i.e. if the same product is part of different orders) with the distinct() function and sort the result as required with the sorted() function:

.distinct()

.sorted((p1, p2) -> (p1.getWeight() < p2.getWeight()) ? -1 : 1)

…and return the stream as a list of products using the collect() function:

.collect(Collectors.toList());

Doesn’t that look promising?

Let’s also quickly look at the other query-method:

public List<Product> getProductsByOrderValueAndWeightOrderByProductId(
    int minOrderValue, int maxOrderValue, int minProductWeight, int maxProductWeight) {

    return getOrders().stream()
        .filter  (o  -> minOrderValue <= o.getOrderValue() && o.getOrderValue() <= maxOrderValue)
        .flatMap (o  -> o.getOrderLines().stream())
        .map     (ol -> ol.getProduct())
        .filter  (p  -> minProductWeight <= p.getWeight() && p.getWeight() <= maxProductWeight)
        .distinct()
        .sorted  ((p1, p2) -> (p1.getId() < p2.getId()) ? -1 : ((p1.getId() == p2.getId()) ? 0 : 1))
        .collect (Collectors.toList());
}

This code looks very similar to the implementation of the first query-method. Much more concise than before, but both methods still share source code of how to traverse the order –> order lines -> product structure. Can we avoid repeating this?

Second attempt using functional programming

Yes, we can!

Let’s define a third method that takes three functions as parameters, two predicate functions to perform the filtering on orders and products respectively and one comparator function to perform the final sorting:

private List<Product> getProducts(
    Predicate<Order> orderFilter, 
    Predicate<Product> productFilter, 
    Comparator<Product> productComparator) {

    return getOrders().stream()
        .filter  (orderFilter)
        .flatMap (o -> o.getOrderLines().stream())
        .map     (ol -> ol.getProduct())
        .filter  (productFilter)
        .distinct()
        .sorted  (productComparator)
        .collect (Collectors.toList());
}

Now we have encapsulated the knowledge on how to traverse the object structure in one single method and the implementation of the query-methods is only about expressing the specific logic for their filters and ordering:

public List<Product> getProductsByDateAndCategoryOrderByWeight(
    LocalDate minDate, LocalDate maxDate, String category) {

    return getProducts(
        o -> o.getOrderDate().isAfter(minDate) && o.getOrderDate().isBefore(maxDate),
        p -> p.getCategory().equals(category),
        (p1, p2) -> ((p1.getWeight() < p2.getWeight()) ? -1 : ((p1.getWeight() == p2.getWeight()) ? 0 : 1)));
}
public List<Product> getProductsByOrderValueAndWeightOrderByProductId(
    int minOrderValue, int maxOrderValue, int minProductWeight, int maxProductWeight) {

    return getProducts(
        o -> minOrderValue <= o.getOrderValue() && o.getOrderValue() <= maxOrderValue,
        p -> minProductWeight <= p.getWeight() && p.getWeight() <= maxProductWeight,
        (p1, p2) -> ((p1.getId() < p2.getId()) ? -1 : ((p1.getId() == p2.getId()) ? 0 : 1)));
}

We now have source code that is concise and contains no code duplication, i.e. we are following the DRY principle!

Great, isn’t it?

Try it out!

The source code of this blog can be found at GitHub and given that you have Java 8 as your default Java environment you can try it with the following commands:

$ git clone git@github.com:callistaenterprise/blog-java-8.git
$ cd blog-java-8
$ ./gradlew run

It should produce output similar to:

:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:run
2014-12-27 21:46:09:844 INFO  main s.c.b.j.Application:116 - Test with 1000 orders and 915 products, product categories: C1 - C8, product weights: 50 - 499
2014-12-27 21:46:09:850 INFO  main s.c.b.j.Application:42 - Testing ImperativeImpl
2014-12-27 21:46:09:855 INFO  main s.c.b.j.Application:48 - Count #1: 65, categories: C1 - C1
2014-12-27 21:46:09:858 INFO  main s.c.b.j.Application:54 - Count #2: 102, weights: 100 - 200
2014-12-27 21:46:09:859 INFO  main s.c.b.j.Application:42 - Testing FunctionalImpl1
2014-12-27 21:46:09:865 INFO  main s.c.b.j.Application:48 - Count #1: 65, categories: C1 - C1
2014-12-27 21:46:09:870 INFO  main s.c.b.j.Application:54 - Count #2: 102, weights: 100 - 200
2014-12-27 21:46:09:871 INFO  main s.c.b.j.Application:42 - Testing FunctionalImpl2
2014-12-27 21:46:09:876 INFO  main s.c.b.j.Application:48 - Count #1: 65, categories: C1 - C1
2014-12-27 21:46:09:878 INFO  main s.c.b.j.Application:54 - Count #2: 102, weights: 100 - 200

BUILD SUCCESSFUL

Total time: 3.379 secs

Given that you are a little accustomed to the Java 8 Streaming API and the support for Lambda Expressions, I’m sure you will enjoy functional programming in Java. You will start to find more and more areas where you can use it to make your code more concise and unambiguous, i.e. easier to write, read and maintain!

Tack för att du läser Callistas blogg.
Hjälp oss att nå ut med information genom att dela nyheter och artiklar i ditt nätverk.

Kommentarer