Streams and Iterables

In this short article, we'll explore various StreamSupport methods that allow us to wrap an Iterable instance as a stream of elements.

Quoting Wikipedia, the iterator design pattern is used to traverse a container for accessing its elements. But with the introduction of streams in java8, we now can leverage parallel processing utilizing the underlying hardware to its maximum potential.

In this post, we’ll explore the StreamSupport class and its various methods that help us to wrap an iterable instance as a stream of elements.

Iterables and stream operations

The Iterable interface does not have any stream() method, so while the overall purpose at a high level remains the same, i.e., to traverse the elements, we cannot directly use an iterator to generate a stream for our custom use-cases.

Having said that, we can still wrap an iterator instance using one of the utility methods in StreamSupport class to generate a stream of underlying elements in the container:

// wrapping an Iterable instance as a Stream
StreamSupport.stream(iterable.spliterator(), false)

But there could be scenarios where we have a custom iteration logic that we want to wrap as a stream like accessing a ResultSet instance while it has more records or maybe reading a String via nextLine() from a BufferredReader instance while it has data to offer.

Let’s see a quick example for the same:

Imagine a custom LinkedList data structure implementation where the nodes are connected via the next reference. The following code can be used to create a stream of the underlying elements:

// using StreamSupport to wrap an Iterator instance in a Stream - openjdk@1.16.0
public Stream<E> stream(){
    return StreamSupport.stream(Spliterators.spliteratorUnknownSize(new Iterator<>(){
        Node temp = head;
        @Override
        public boolean hasNext(){
            return temp != null;
        }
        @Override
        public E next(){
            E data = temp.data;
            temp = temp.next;
            return data;
        }
    }, Spliterator.ORDERED), false);
}

The hasNext() method will check if the temp node is pointing to a null value or not, and accordingly, the presence of the next element is derived.

Similarly, we return the data at the current node every time the next() method is called. The temp reference is then shifted one node ahead to traverse the next node.

Using a Spliterator

Similarly, we can use a Spliterator to generate the stream. Using the same example:

// using StreamSupport to wrap a Spliterator instance in a Stream - openjdk@1.16.0
public Stream<E> stream(){
    return StreamSupport.stream(new Spliterator<E>(){
        Node temp = head;
        @Override
        public boolean tryAdvance(Consumer<? super E> action){
            if(temp != null){
                action.accept(temp.data);
                temp = temp.next;
                return true;
            }
            return false;
        }
        @Override
        public Spliterator<E> trySplit(){
            return null;
        }
        @Override
        public long estimateSize(){
            return size;
        }
        @Override
        public int characteristics(){
            return Spliterator.ORDERED;
        }
    }, false);
}

In the above example, we can see a similar implementation done using a Spliteraor. The tryAdvance() method will feed the current data to the Consumer instance action , moving the temp reference one node ahead afterward. In case the temp already points to a null value, false is returned.

You can check the complete example in the Github repository.

That is all for this post. If you want to share any feedback, please drop me an email, or contact me on any social platforms. I’ll try to respond at the earliest. Also, please consider subscribing for regular updates.

Be notified of new posts. Subscribe to the RSS feed.