null

Работа со списками в Java 8

C появлением в Java 8 лямбда-выражений и ряда других подходов, присущих функциональным языкам, выразительная сила, без сомнения, увеличилась, однако появились и далеко не очевидные моменты. Примером может служить работа со списками с применением операций map, filter, reduce.

Эти декларативные функции работы с коллекциями, появились в теории множеств. В качестве аргументов они принимают некоторое множество и функцию преобразования. Функция Map выполняет отображение одного множества на другое путем применения заданной функции к каждому элементу исходного множества. Filter возвращает множество элементов исходного множества, удовлетворяющих заданному условию. Reduce собирает все элементы множества в один по заданному правилу.

Один из плюсов подобных декларативных операций над списками - возможность не только скрыть реализацию обходов списков, но и локализовать ее в одном месте и легко ее модифицировать. Примером может служить распараллеливание операции map.

ExecutorService executorService = Executors.newFixedThreadPool(10);
list.map(e -> executorService.submit(() -> {
    Thread.sleep(100);
    return e;
 })).map(e -> {
    Integer ret = null;
    try {
        ret = e.get();
    }
    catch (Exception ignore) {}
    return ret;
}).filter(e -> e != null).forEach(System.out::println);

Если не обращать на непривычные для Java программиста лямбды все выглядит понятно. Для каждого элемента мы создаем задачу, передаем ее в executorService и получаем объект Future из которого получаем результат с помощью метода get(). Исключаем null элементы и выводим в stdout. 

Удивительно здесь то, что все выполняется последовательно. Задачи действительно передается в executorService и даже выполняется в отдельных потоках, но последовательно.

Причина заключается в том что Java 8 переняла от функциональных языков не только лямбда-выражения, но и такую концепцию как ленивые вычисления. Функция map не применяет передаваемую функцию ко всем элементам списка сразу, она лишь возвращает объект Iterable, а отображение происходит по вызову next() этого объекта. В приведенном примере до момента вывода строится цепочка отложенных вычислений, в результате чего они и выполняются последовательно.

Альтернативный пример без ленивых вичислений работает как и предполагалось, выполняя задачи параллельно, однако это требует переопределения функции map на классический последовательный обход списка.

List<Integer> list = new LinkedList<Integer>() {
    @Override
    public <U> Iterable<U> map(Mapper<? super Integer, ? extends U> mapper) {
        List<U> list = new LinkedList<>();
        for (Integer e : this) list.add(mapper.map(e));
        return list;
    }
};