API-интерфейс Streams был представлен в Java 8 вместе с лямбда-выражениями всего несколько лет назад. Я, как дисциплинированный адепт Java, пытался использовать эту новую функцию в нескольких своих проектах, например, здесь и здесь . Мне это не очень понравилось, и я вернулся к старым добрым декораторам. Более того, я создал Cactoos , библиотеку декораторов, чтобы заменить Guava , что не так хорошо во многих местах.
Вот примитивный пример. Допустим, у нас есть набор измерений, поступающих из некоторого источника данных, все они представляют собой числа от нуля до единицы:
1
|
Iterable<Double> probes; |
Теперь нам нужно показать только первые 10 из них, игнорируя нули и единицы, и масштабировать их до (0..100)
. Звучит как легкая задача, верно? Есть три способа сделать это: процедурный, объектно-ориентированный и способ Java 8. Начнем с процедурного пути:
01
02
03
04
05
06
07
08
09
10
11
12
|
int pos = 0 ; for (Double probe : probes) { if (probe == 0 .0d || probe == 1 .0d) { continue ; } if (++pos > 10 ) { break ; } System.out.printf( "Probe #%d: %f" , pos, probe * 100 .0d ); } |
Почему это процедурный путь? Потому что это обязательно. Почему это обязательно? Потому что это процедурно. Нет, я шучу
Это необходимо, потому что мы даем компьютеру инструкции о том, какие данные помещать, где и как их перебирать. Мы не объявляем результат, но обязательно строим его. Это работает, но не очень масштабируемо. Мы не можем принять участие в этом алгоритме и применить его к другому варианту использования. Мы не можем легко изменить его, например, взять числа из двух источников вместо одного и т. Д. Это процедурно. Достаточно сказано. Не делай так.
Теперь Java 8 предоставляет нам Streams API , который должен предложить функциональный способ сделать то же самое. Давайте попробуем использовать это.
Во-первых, нам нужно создать экземпляр Stream
, который Iterable
не позволяет нам получить напрямую. Затем мы используем потоковый API, чтобы сделать работу:
1
2
3
4
5
6
7
8
|
StreamSupport.stream(probes.spliterator(), false ) .filter(p -> p == 0 .0d || p == 1 .0d) .limit(10L) .forEach( probe -> System.out.printf( "Probe #%d: %f" , 0 , probe * 100 .0d ) ); |
Это будет работать, но скажет Probe #0
для всех зондов, потому что forEach()
не работает с индексами. В интерфейсе Stream
нет такой вещи, как forEachWithIndex()
как в Java 8 (и в Java 9 тоже ). Вот обходной путь с атомным счетчиком:
01
02
03
04
05
06
07
08
09
10
11
|
AtomicInteger index = new AtomicInteger(); StreamSupport.stream(probes.spliterator(), false ) .filter(probe -> probe == 0 .0d || probe == 1 .0d) .limit(10L) .forEach( probe -> System.out.printf( "Probe #%d: %f" , index.getAndIncrement(), probe * 100 .0d ) ); |
«Что случилось с этим?» Вы можете спросить. Во-первых, посмотрите, как легко мы попали в беду, когда мы не нашли правильный метод в интерфейсе Stream
. Мы сразу же отказались от «потоковой» парадигмы и вернулись к старой доброй процедурной глобальной переменной (счетчику). Во-вторых, мы на самом деле не видим, что происходит внутри этих методов filter()
, limit()
и forEach()
. Как именно они работают? В документации говорится, что этот подход является «декларативным», и каждый метод в интерфейсе Stream
возвращает экземпляр некоторого класса. Какие они классы? Мы понятия не имеем, просто посмотрев на этот код.
Самая большая проблема с этим потоковым API — это сам интерфейс Stream, он огромен!
Эти две проблемы связаны. Самая большая проблема с этим потоковым API — это сам интерфейс Stream
— он огромен. На момент написания статьи существует 43 метода. Сорок три в одном интерфейсе! Это противоречит каждому принципу объектно-ориентированного программирования, начиная с SOLID, а затем до более серьезных.
Что такое объектно-ориентированный способ реализации того же алгоритма? Вот как я мог бы сделать это с Cactoos , который является просто коллекцией примитивный простые классы Java:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
new And( new Mapped<Double, Scalar<Boolean>>( new Limited<Double>( new Filtered<Double>( probes, probe -> probe == 0 .0d || probe == 1 .0d ), 10 ), probe -> () -> { System.out.printf( "Probe #%d: %f" , 0 , probe * 100 .0d ); return true ; } ), ).value(); |
Посмотрим, что здесь происходит. Во-первых, Filtered
украшает наши итерируемые probes
чтобы извлечь из него определенные элементы. Обратите внимание, что Filtered
реализует Iterable
. Затем Limited
, также являющаяся Iterable
, выводит только первые десять элементов. Затем Mapped
преобразует каждый зонд в экземпляр Scalar<Boolean>
, который выполняет печать строк.
Наконец, экземпляр And
проходит через список «скаляров» и просит каждого из них вернуть boolean
. Они печатают строку и возвращают true
. Так как это true
, And
делает следующую попытку со следующим скаляром. Наконец, его метод value()
возвращает true
.
Но подождите, индексов нет. Давайте добавим их. Для этого мы просто используем другой класс, называемый AndWithIndex
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
new AndWithIndex( new Mapped<Double, Func<Integer, Boolean>>( new Limited<Double>( new Filtered<Double>( probes, probe -> probe == 0 .0d || probe == 1 .0d ), 10 ), probe -> index -> { System.out.printf( "Probe #%d: %f" , index, probe * 100 .0d ); return true ; } ), ).value(); |
Вместо Scalar<Boolean>
теперь мы отображаем наши пробники на Func<Integer, Boolean>
чтобы они могли принять индекс.
Прелесть этого подхода в том, что все классы и интерфейсы маленькие, и поэтому они очень сочетаемы. Чтобы сделать итеративность зондов ограниченной, мы украшаем ее Limited
; чтобы сделать его отфильтрованным, мы украшаем его Filtered
; чтобы сделать что-то еще, мы создаем новый декоратор и используем его. Мы не привязаны к одному единственному интерфейсу, как Stream
.
Суть в том, что декораторы — это объектно-ориентированный инструмент для изменения поведения коллекций, в то время как потоки — это нечто другое, для чего я даже не могу найти название.
PS Кстати, вот как тот же алгоритм может быть реализован с помощью Iterables
:
01
02
03
04
05
06
07
08
09
10
11
12
13
|
Iterable<Double> ready = Iterables.limit( Iterables.filter( probes, probe -> probe == 0 .0d || probe == 1 .0d ), 10 ); int pos = 0 ; for (Double probe : probes) { System.out.printf( "Probe #%d: %f" , pos++, probe * 100 .0d ); } |
Это странная комбинация объектно-ориентированного и функционального стилей.
Опубликовано на Java Code Geeks с разрешения Егора Бугаенко, партнера нашей программы JCG . Смотрите оригинальную статью здесь: Streams vs. Decorators
Мнения, высказанные участниками Java Code Geeks, являются их собственными. |