Functional-Style Operations in Java with the Stream API

I discussed Java Lambda Expressions quite a time ago in another article. Those were not the only new thing that got implemented in Java 8. Besides Lambdas, Type-Annotations, and Default methods in interfaces, version 8 of the Java API added support for functional-style operations. Because I think that this is quite a rarely discussed feature, I decided to write this short introduction to the Java Stream API and how it can be used for basic functional-style programming in Java.

But before I do that, let’s take a quick look at iterators.

External and internal Iterators

Every Java developer knows what Iterators are and that implementing one can be a bit of an overkill, especially if the data structure is very simple.

The classic external iterator lets the client control what happens to the elements of the structure. The client is also responsible for checking the bounds and requesting the next element. For that purpose, an Iterator usually exposes a public next() and hasNext() method:

Figure 1: The classic external iterator

Internal iterators, however, work internally (who would’ve thought). That means that the client doesn’t have to define a loop to get the individual elements of the collection. Instead, all that logic is packed into the Iterator itself.

The client just has to tell the iterator what it should do with every element of the collection instead of defining how things should be done.

External iterator example

Here’s a very simple implementation of that pattern for a custom linked list. You can also download the complete IntelliJ project at the end of this article.

public class MyCollection<T> implements Collection<T>
{
    private class Node
    { /* ... */ }

    private class MyIterator implements Iterator<T>
    {
        private MyCollection<T> data;
        private Node current;
        private boolean initialized = false;

        MyIterator(MyCollection<T> data)
        {
            this.data = data;
        }

        @Override
        public boolean hasNext()
        {
            if(!initialized)
                return data.head != null;
            else
                return (current != null && current.next != null);
        }

        @Override
        public T next()
        {
            if(current == null && !initialized)
            {
                current = data.head;
                initialized = true;
            }
            else
                this.current = current.next;

            if(current != null)
                return current.elem;
            else
                return null;
        }
    }

    private Node head;
    private int length = 0;

    private Node getLast()
    { /* ... */ }

    @Override
    public void add(T elem)
    { /* ... */ }

    public int length()
    { /* ... */ }

    @Override
    public Iterator<T> iterator()
    {
        return new MyIterator(this);
    }
}

As you can see, the concrete Iterator MyIterator is implemented with a private inner class. That’s absolutely fine because it implements the default Iterator interface that’s provided by Java. A client doesn’t even have to know what concrete Iterator a class uses as long as it provides the next() and hasNext() functions.

The client will, however, have to call those two functions in an external loop:

private static void testExternalIterator()
{
    MyCollection<Integer> c = new MyCollection<>();

    for(int i = 1; i <= 10; i++)
            c.add(i);

    Iterator<Integer> iterator = c.iterator();

    System.out.println("Length of the collection: " + c.length());
    System.out.println("The collection contains the following elements:");

    while(iterator.hasNext())
        System.out.print(iterator.next() + "  ");
}

Internal iterator example

A simple internal iterator can be implemented like this:

public class MyFunctionalCollection<T> implements FunctionalCollection<T>
{
    List<T> data = new ArrayList<>();

    @Override
    public void iterate(Consumer<T> action)
    {
        for (T x : data)
            action.accept(x);

        // Lambda representation:
        // data.stream().forEach(x -> action.accept(x));
    }

    @Override
    public void add(T elem)
    {
        data.add(elem);
    }
}

Note that the iterator is now fully contained within the collection. To make it even clearer, I explicitly defined the for loop that iterates over the elements. Underneath it is the shorter lambda representation that uses the Stream API, which I’ll discuss in a second.

Anyway, the client can now iterate over the elements of the collection using a single line of code:

private static void testInternalIterator()
{
    FunctionalCollection<String> fc = new MyFunctionalCollection<>();

    fc.add("Hello");
    fc.add(" ");
    fc.add("World");
    fc.add("!");

    fc.iterate((x -> System.out.print(x)));
}

Internal iterators and Lambdas

As you could see in the internal iterator example, Lambdas are used to represent the action that should be applied to every element of the concrete aggregate. In this example, the client chose to print every element in the collection.

You can take a look at this article to learn more about lambda expressions in Java.

Functional-style mapping and filtering in Java

The internal iterator above didn’t make use of the Java API so far. However, we can easily change that and add two functions that are typically found in functional programming languages: One that maps and one that filters.

The new FunctionalCollection interface looks like this:

public interface FunctionalCollection<T>
{
    void iterate(Consumer<T> action);
    <R extends T> Stream<R> map(Function<? super T, R> function);
    Stream<T> filter(Predicate<? super T> f);
    void add(T elem);
}

What’s the difference between the map and iterate functions?

Iterate is our custom implementation of an internal iterator, and it will simply consume one element of the collection at a time and apply a function to it. That function may or may not return a value. That doesn’t matter because the result is ignored if the function itself doesn’t have any side-effects (which it shouldn’t have in a functional-style program).

The map function, however, is part of the Java Stream API and it more or The map function, however, is part of the Java Stream API, and it more or less does the same thing as iterate, but it expects the applied function to return a value that it’ll then return. In this case, the result must be of Type R, which has to be a subtype of T.

This might sound and look complicated, but I’ll provide a simple example in a second. For now, let’s take a look at the updated implementation of a concrete FunctionalCollection:

public class MyFunctionalCollection<T> implements FunctionalCollection<T>
{
    List<T> data;

    @Override
    public void iterate(Consumer<T> action)
    {
        data.stream().forEach(x -> action.accept(x));
    }

    @Override
    public <R extends T> Stream<R> map(Function<? super T, R> function)
    {
        return data.stream().map(function);
    }

    @Override
    public Stream<T> filter(Predicate<? super T> f)
    {
        return data.stream().filter(f);
    }

    /* Other functions and constructors */
}

As you can see, the implemented functions call the appropriate methods of the Java Stream API with the same parameters and return a new Stream.

Therefore, it’s easy to concatenate stream calls and build more complex calls from small building-blocks:

private static void testMapAndFilter()
    {
        FunctionalCollection<Integer> fc = new MyFunctionalCollection<>();
        fc.add(10); fc.add(20); fc.add(30); fc.add(40); fc.add(50);

        System.out.print("Contents: ");
        fc.iterate((x -> System.out.print(x + "  ")));
        System.out.println("");

        System.out.println("Multiply each element with 2 and then divide by 3:");

        Stream<Integer> r = fc.map(x -> (x*2)).map(z -> (z/3));
        FunctionalCollection<Integer> result = new MyFunctionalCollection<>(r);

        System.out.print("Result: ");
        result.iterate((x -> System.out.print(x + "  ")));
        System.out.println("");

        System.out.println("Filter out odd numbers:");

        // Note: We have to define a new Stream every time we use it
        //       because they have to be stateless and an exception
        //       will be thrown if you reuse streams.
        r = fc.map(x -> (x*2)).map(z -> (z/3)).filter((b -> (b % 2) == 0));
        FunctionalCollection<Integer> even = new MyFunctionalCollection<>(r);

        System.out.print("Result: ");
        even.iterate((x -> System.out.print(x + "  ")));
    }

The highlighted lines show the concatenated calls to map and filter. Their results are processed and stored in a new stream, which then can be converted back to a FunctionalCollection object.

Note that you must not re-use streams. It’s discouraged when streams have a state and when they aren’t side-effect free.

Example Project

Click here to download the code examples.

Summary

This was a short intro to Java Streams. Take a look at the official API for a more detailed explanation and more tips and examples.

Java Streams are an interesting way of using functional-style programming in Java. However, they are limited compared to a real functional programming language like Haskell.

Streams natively support the parallel processing of large data-sets, which means that you don’t have to worry about synchronization yourself, which can be a huge benefit.

However, using them can be quite intimidating for beginners or when you are unfamiliar with lambda expressions and generics. A similar effect can be accomplished with internal iterators. However, then you’ll have to take care of all the details yourself, which can be a time consuming.

Leave your two cents, comment here!

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.