The Streams API is a real gem in Java 8, and I keep finding more or less unexpected uses for them. I recently wrote about using them as ForkJoinPool facade. Here’s another interesting example: Walking recursive data structures.
Without much ado, have a look at the code:
class Tree { private int value; private List<Tree> children = new LinkedList<>(); public Tree(int value, List<Tree> children) { super(); this.value = value; this.children.addAll(children); } public Tree(int value, Tree... children) { this(value, asList(children)); } public int getValue() { return value; } public List<Tree> getChildren() { return Collections.unmodifiableList(children); } public Stream<Tree> flattened() { return Stream.concat( Stream.of(this), children.stream().flatMap(Tree::flattened)); } }
It’s pretty boring, except for the few highlighted lines.
Let’s say we want to be able to find elements matching some criteria in the tree or find particular element. One typical way to do it is a recursive function – but that has some complexity and is likely to need a mutable argument (e.g. a set where you can append matching elements). Another approach is iteration with a stack or a queue. They work fine, but take a few lines of code and aren’t so easy to generalize.
Here’s what we can do with this flattened
function:
// Get all values in the tree: t.flattened().map(Tree::getValue).collect(toList()); // Get even values: t.flattened().map(Tree::getValue).filter(v -> v % 2 == 0).collect(toList()); // Sum of even values: t.flattened().map(Tree::getValue).filter(v -> v % 2 == 0).reduce((a, b) -> a + b); // Does it contain 13? t.flattened().anyMatch(n -> n.getValue() == 13);
I think this solution is pretty slick and versatile. One line of code (here split to 3 for readability on blog) is enough to flatten the tree to a straightforward stream that can be searched, filtered and whatnot.
It’s not perfect though: It is not lazy and flattened
is called for each and every node in the tree every time. It probably could be improved using a Supplier
. Anyway, it doesn’t matter for typical, reasonably small trees, especially in a business application on a very tall stack of libraries. But for very large trees, very frequent execution and tight time constraints the overhead might cause some trouble.
That is beautiful :)
I think this is a great example of how a verbose algorithm problem can be solved with Java 8 Streams. The only problem is, most interviewers who ask these types of questions aren’t familiar with Java 8 Streams.
Perfect! Exactly what I needed.
Hi do have any gitHub So I can download and try this ?
Can’t make this work.
Thanks.
J.Martins – how about this gist: https://gist.github.com/konrad-garus/bb87b6a01b2b6bdafca5566376dd0411
It’s literally a copy/paste, works just fine for me.
If something doesn’t work, please be more specific about what’s broken.
That’s very nice! As it happens, there are some incorrect explanations (DZone, e.g.) of streaming DFS.
Perfect! Exactly what I needed.
Can it be a tail call recursion in some way?
Good question. Offhand (and after a quick look at the code), it seems that Streams.concat holds on to both references to close them together, so it is “leaky”. It seems doable though, if we had (or created) an implementation of “concat” that would return all elements from the first stream, then close and discard that stream, then include elements from the second stream.