Статьи

Ходьба рекурсивных структур данных с использованием потоков Java 8

Streams API — это настоящая жемчужина в Java 8, и я продолжаю находить более или менее неожиданные способы их использования. Недавно я писал об их использовании в качестве фасада ForkJoinPool . Вот еще один интересный пример: ходячие рекурсивные структуры данных.

Без лишних слов взгляните на код:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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));
    }
}

Это довольно скучно, за исключением нескольких выделенных строк.

Допустим, мы хотим иметь возможность найти элементы, соответствующие некоторым критериям в дереве, или найти определенный элемент. Один из типичных способов сделать это — рекурсивная функция, но она имеет некоторую сложность и, вероятно, потребует изменяемого аргумента (например, набора, в который вы можете добавлять соответствующие элементы). Другой подход — итерация со стеком или очередью. Они отлично работают, но занимают несколько строк кода и не так легко обобщить.

Вот что мы можем сделать с этой flattened функцией:

01
02
03
04
05
06
07
08
09
10
11
// 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(t -> t.getValue() == 13);

Я думаю, что это решение довольно гладкое и универсальное. Одной строки кода (здесь разделенной на 3 для удобства чтения в блоге) достаточно, чтобы сгладить дерево в простой поток, который можно искать, фильтровать и еще много чего.

Хотя он не идеален: он не ленив и flattened , каждый раз вызывается для каждого узла в дереве. Возможно, это можно улучшить с помощью Supplier . В любом случае, это не имеет значения для типичных, достаточно небольших деревьев, особенно в бизнес-приложениях на очень высоком стеке библиотек. Но для очень больших деревьев, очень частого выполнения и жестких временных ограничений могут возникнуть некоторые проблемы.