Java Stream Tree Walker

Introduction

This article discusses walking a tree or graph of nodes with a Java 8 Stream implementation. The implementation is codified in a Spliterator. The strategy described herein can be a useful alternative to implementing a class-hierarchy specific visitor because only a single per-type method need be defined (often as a Java lambda).

Please refer to this article for an in-depth discussion of creating Spliterators.

API Definition

The implementaion provides the following API:

public class Walker<T> ... {
    ...
    public static <T> Stream<T> walk(T root, Function<? super T,Stream<? extends T>> childrenOf)
    ...
}

The Function provides the subordinate (“children”) nodes of the argument node. For example, for Java Classes to obtain inner (declared) Classes:

    t -> Stream.of(t.getDeclaredClasses()

or for Files:

    t -> t.isDirectory() ? Stream.of(t.listFiles()) : Stream.empty()

Implementation

The API provides a static method to create a Stream from the implemented Spliterator:

    public static <T> Stream<T> walk(T root, Function<? super T,Stream<? extends T>> childrenOf) {
        return StreamSupport.stream(new Walker<>(root, childrenOf), false);
    }

The complete Spliterator implementation is:

public class Walker<T> extends Spliterators.AbstractSpliterator<T> {
    private final Stream<Supplier<Spliterator<T>>> stream;
    private Iterator<Supplier<Spliterator<T>>> iterator = null;
    private Spliterator<? extends T> spliterator = null;

    private Walker(T node, Function<? super T,Stream<? extends T>> childrenOf) {
        super(Long.MAX_VALUE, IMMUTABLE | NONNULL);

        stream =
            Stream.of(node)
            .filter(Objects::nonNull)
            .flatMap(childrenOf)
            .filter(Objects::nonNull)
            .map(t -> (() -> new Walker<T>(t, childrenOf)));
        spliterator = Stream.of(node).spliterator();
    }

    @Override
    public Spliterator<T> trySplit() {
        if (iterator == null) {
            iterator = stream.iterator();
        }

        return iterator.hasNext() ? iterator.next().get() : null;
    }

    @Override
    public boolean tryAdvance(Consumer<? super T> consumer) {
        boolean accepted = false;

        while (! accepted) {
            if (spliterator == null) {
                spliterator = trySplit();
            }

            if (spliterator != null) {
                accepted = spliterator.tryAdvance(consumer);

                if (! accepted) {
                    spliterator = null;
                }
            } else {
                break;
            }
        }

        return accepted;
    }
    ...
}

A Stream of Spliterator Suppliers is created at instantiation (and the Supplier will supply another Walker). The first time trySplit() is called the Stream is converted to an Iterator and the next Spliterator is generated. The tryAdvance(Consumer) will exhaust the last created Spliterator before calling tryAdvance() to obtain another. The first Spliterator consists solely of the root node.

Examples

To print the inner defined classes of Collections:

        Walker.<Class<?>>walk(java.util.Collections.class,
                              t -> Stream.of(t.getDeclaredClasses()))
            .limit(10)
            .forEach(System.out::println);

Yields:

class java.util.Collections
class java.util.Collections$UnmodifiableCollection
class java.util.Collections$UnmodifiableSet
class java.util.Collections$UnmodifiableSortedSet
class java.util.Collections$UnmodifiableNavigableSet
class java.util.Collections$UnmodifiableNavigableSet$EmptyNavigableSet
class java.util.Collections$UnmodifiableRandomAccessList
class java.util.Collections$UnmodifiableList
class java.util.Collections$UnmodifiableMap
class java.util.Collections$UnmodifiableMap$UnmodifiableEntrySet

And to print the directory structure (sorted):

        Walker.walk(new File("."),
                    t -> t.isDirectory() ? Stream.of(t.listFiles()) : Stream.empty())
            .filter(File::isDirectory)
            .sorted()
            .forEach(System.out::println);

Yields:

.
./src
./src/main
./src/main/resources
./target

This presents a flexible solution and can be extended with disparate objects. For example, a method to walk XML Nodes might be:

    public static Stream<Node> childrenOf(Node node) {
        NodeList list = node.getChildNodes();

        return IntStream.range(0, list.getLength()).mapToObj(list::item);
    }

Alternate Entry Point

It is straightforward to offer an alternate entry point where multiple root nodes are supplied by defining a corresponding constructor:

public class Walker<T> extends Spliterators.AbstractSpliterator<T> {
    ...
    private Walker(Stream<T> nodes, Function<? super T,Stream<? extends T>> childrenOf) {
        super(Long.MAX_VALUE, IMMUTABLE | NONNULL);

        stream = nodes.map(t -> (() -> new Walker<T>(t, childrenOf)));
    }
    ...
    public static <T> Stream<T> walk(Stream<T> roots, Function<? super T,Stream<? extends T>> childrenOf) {
        return StreamSupport.stream(new Walker<>(roots, childrenOf), false);
    }
    ...
}

Summary

Streams created from the Spliterator implementation described herein provide a versatile means for walking any tree with a minimum per-type implementation while offering all the filtering and processing power of the Stream API.