Stream API — замечательная вещь быстро завоевавшая популярность у джава программистов. Лаконичные однострочники обрабатывающие коллекции данных посредством цепочек простых операций map, filter, forEach, collect оказались очень удобны. Операции над парами ключ-значение, конечно, тоже не помешали бы, но увы.

В целом примерно понятно как это всё устроено, но все же зачастую ответ на вопрос «А как бы это написал я?» здорово помогает понять внутренние механизмы той или иной технологии. Так получилось, что внезапно для себя я ответил на этот вопрос применительно к Stream API, историей изобретения этого велосипеда и спешу с вами поделиться.

Пока я спокойно себе писал на свинге компоненты IDE, мир менялся — javascript захватывал сферу разработки UI. И захватил. Как ни крути, качественный рантайм на абсолютно каждой машине — сильный аргумент. Ничего не поделаешь, пришлось разбираться. В джава скрипте пользовательский код выполняется в одном потоке, поэтому все сколько нибудь длительные операции асинхронны. И если наша бизнес логика предполагает последовательность таких операций, то первой из них надо передать колбэк, который запустит вторую, которой передать колбэк, который выполнит третью и так далее. В общем, читается ужасно, поддерживается мучительно, js разработчики как-то пытаются с этим жить и придумывают обходные подходы, один из встретившихся мне головоломных вариантов — использование генераторов. Так я про них и узнал.

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

И так, если бы джава разработчик захотел сделать некое подобие генератора, что бы у него получилось? У меня получилось так:

    public interface Generator<T> {
        void generate(GeneratorContext<T> context);
    }
    
    public interface GeneratorContext<T> {
        void emit(T value);
    }

Идея понятна, генерацией занимается метод generate(...), ему передаётся параметром некий контекст и последовательно вызывая его метод emit(...) можно возвращать множественные значения.

Определенно, данные генерируемые данным генератором образуют сущность, назовём её Dataset:

public class Dataset<T> {
    
    private final Generator<T> generator;
    
    private Dataset(Generator<T> generator) { 
          this.generator = generator; 
    }
}

И если в наличии есть набор данных, то неплохо бы иметь возможность что-нибудь сделать с каждым их элементом. Напечатать там или ещё что. Добавляем в класс Dataset метод forEach:

    public void forEach(Consumer<T> consumer) {
        generator.generate(value -> consumer.accept(value));
    }

Мы сформировали такой контекст генератора, что на каждый вызов метода еmit, он передаёт эмитированное значение в consumer, и запустили генерацию.

Осталось откуда-нибудь добыть инстанс датасета и можно испытывать. Добавляем фабричный метод, который создаёт генератор из коллекции и оборачивает его в датасет:

    public static <T> Dataset<T> of(Collection<T> collection) {
        return new Dataset<>(generatorContext -> 
               collection.forEach(item -> generatorContext.emit(item))
        );
    }

То же самое с помощью старого доброго цикла:

    public static <T> Dataset<T> of(Collection<T> collection) {
        return new Dataset<>(generatorContext -> {
               for (T item : collection) {
	           generatorContext.emit(item);
               }
        });
    }

Попросту пробежали по коллекции и эмитировали каждый элемент. Уже можно запускать:

        Dataset.of(Arrays.asList("foo", "bar")).forEach(System.out::println);

Вывод:
foo
bar

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

    public Dataset<T> union(Collection<T> collection) {
        return new Dataset<>(generatorContext -> {
            generator.generate(generatorContext);
            collection.forEach(item -> generatorContext.emit(item));
        });
    }

Создали новый датасет с таким генератором, который сначала эмитирует все значения текущего датасета, а потом все значения присоединяемой коллекции.

Фильтруем:

    public Dataset<T> filter(Predicate<T> predicate) {
        return new Dataset<>(generatorContext -> generator.generate(value -> {
            if (predicate.test(value)) {
                generatorContext.emit(value);
            }
        }));
    }

Здесь мы создали новый датасет с таким генератором, который получает значения от текущего генератора, но эмитирует дальше только те из них, что прошли проверку.

И, наконец, преобразуем каждый элемент множества данных:

    public <R> Dataset<R> map(Function<T, R> function) {
        return new Dataset<>(generatorContext -> generator.generate(
                value -> generatorContext.emit(function.apply(value))
        ));
    }

Создали новый датасет, каждый элемент которого будет сгенерирован посредством преобразования элементов данного датасета. Но элементы данного датасета как таковые не существует, они ещё должны быть сгенерированы. Да, это ленивые вычисления.

Теперь запускаем всё вместе.

         Dataset.of(Arrays.asList("шла", "саша", "по", "шоссе"))
                .union(Arrays.asList("и", "сосала", "сушку"))
                .filter(s -> s.length() > 4)
                .map(s -> s + ", length=" + s.length())
                .forEach(System.out::println);

Вывод:
шоссе, length=5
сосала, length=6
сушку, length=5

Пора рефакторить. Первое, что бросается в глаза: интерфейс GeneratorContext можно заменить стандартным Consumer-ом. Заменяем. Местами и код сократится, так как ранее нам приходилось оборачивать Consumer-ы в GeneratorContext.

Тут стоит остановиться и обратить внимание на определенную схожесть нашего Dataset и java.util.stream.Stream, что наводит на мысли и о родстве нашего Generator-а с загадочными Spliterator-ами из платформы джава.

Велосипед готов. Надеюсь, вам тоже стали немного понятнее внутренние механизмы Stream API, роль Spliterator-а и способ организации ленивых вычислений.

PS. Git repo здесь.
Поделиться с друзьями
-->

Комментарии (12)


  1. NaHCO3
    17.03.2017 17:50

    Могу предложить следующую цель — дуальную структуру к генератору. Это когда не возвращается значение в середине функции, а ожидается. Получается читатель. Если объединить с генератором — то map. Если добавить мультредовости и футур — то async/await.

    И всё это, конечно, на кодогенерации через аннотации, потому что из коробки джава подобные структуры не поддерживает. Кто знает, может ваша библиотека даже станет модной среди хипсторов.


    1. xGromMx
      17.03.2017 18:35
      +1

      Дуальная структура к генератору это Observable и это уже есть Rx{NET, Java, Kotlin, Scala, JS}


  1. federalkosmos
    19.03.2017 11:14

    Пока я спокойно себе писал на свинге компоненты IDE, мир менялся — javascript захватывал сферу разработки UI. И захватил. Как ни крути, качественный рантайм на абсолютно каждой машине — сильный аргумент. Ничего не поделаешь, пришлось разбираться. В джава скрипте

    В общем, читается ужасно, поддерживается мучительно, js разработчики


    Так тут про Java или JavaScript?


  1. drseergio
    20.03.2017 14:44

    У вас вроде закралась небольшая опечатка: вместо Array.asList должно быть Arrays.asList.


  1. drseergio
    20.03.2017 15:25

    И в методе union вы имели ввиду не iterable.forEach() а collection.forEach().


    1. yannmar
      21.03.2017 16:02

      Да, спасибо.


  1. drseergio
    20.03.2017 15:39

    Спасибо за статью. Мне понадобилось минут 40, чтобы полностью понять что происходит. Очень помогло переименование Generator в GeneratorFunction и GeneratorContext (generatorContext) в GeneratorContextFunction.


  1. sergueik
    20.03.2017 22:55

    а можете код положить на гитхаб? спасибо!



  1. vasja0441
    21.03.2017 23:52
    +1

    Супер-познавателная статья! А есть готовый код? GITHUB или что то типа?
    А то кусочки не компиляются полностью…
    … а хотелось бы оценить сей шедевр полностью!


    1. yannmar
      21.03.2017 23:54

      Я оценил ваш сарказм, вот вам гитхаб


  1. tyamur
    22.03.2017 12:59

    Спасибо за статью.