В Data Geekery мы любим Java. И так как мы действительно входим в свободный API jOOQ и запросы DSL , мы абсолютно взволнованы тем, что Java 8 принесет в нашу экосистему.
Ява 8 Пятница
Каждую пятницу мы показываем вам пару замечательных новых функций Java 8 в виде учебника, в которых используются лямбда-выражения, методы расширения и другие замечательные вещи. Вы найдете исходный код на GitHub .
10 тонких ошибок при использовании API Streams
Мы сделали все списки ошибок SQL:
- 10 распространенных ошибок разработчиков Java при написании SQL
- Еще 10 распространенных ошибок, которые делают Java-разработчики при написании SQL
- Еще 10 распространенных ошибок, которые делают Java-разработчики при написании SQL (вы не поверите последнему)
Но мы еще не попали в список 10 ошибок с Java 8! По сегодняшнему случаю ( сегодня пятница, 13-е ) мы узнаем, что пойдет не так в ВАШЕМ приложении, когда вы работаете с Java 8 (с нами этого не случится, поскольку мы застряли с Java 6 для другого). пока).
1. Случайное повторное использование потоков
Хочу поспорить, что это случится со всеми по крайней мере один раз. Как и существующие «потоки» (например, InputStream
), вы можете использовать потоки только один раз. Следующий код не будет работать:
1
2
3
4
5
|
IntStream stream = IntStream.of( 1 , 2 ); stream.forEach(System.out::println); // That was fun! Let's do it again! stream.forEach(System.out::println); |
Вы получите:
1
2
|
java.lang.IllegalStateException: stream has already been operated upon or closed |
Так что будьте осторожны при использовании вашего потока. Это можно сделать только один раз.
2. Случайное создание «бесконечных» потоков
Вы можете создавать бесконечные потоки довольно легко, не замечая этого. Возьмите следующий пример:
1
2
3
|
// Will run indefinitely IntStream.iterate( 0 , i -> i + 1 ) .forEach(System.out::println); |
Весь смысл потоков заключается в том, что они могут быть бесконечными, если вы их спроектируете. Единственная проблема в том, что вы, возможно, не хотели этого. Поэтому обязательно всегда ставьте правильные ограничения:
1
2
3
4
|
// That's better IntStream.iterate( 0 , i -> i + 1 ) .limit( 10 ) .forEach(System.out::println); |
3. Случайно создавая «тонкие» бесконечные потоки
Мы не можем сказать этого достаточно. Вы в конечном итоге создадите бесконечный поток, случайно. Возьмите следующий поток, например:
1
2
3
4
|
IntStream.iterate( 0 , i -> ( i + 1 ) % 2 ) .distinct() .limit( 10 ) .forEach(System.out::println); |
Так…
- мы генерируем чередующиеся 0 и 1
- тогда мы сохраняем только разные значения, то есть один 0 и один 1
- тогда мы ограничим поток размером 10
- тогда мы потребляем это
Ну что ж… Операция distinct()
не знает, что функция, предоставленная методу iterate()
выдаст только два разных значения. Можно ожидать большего. Таким образом, он будет всегда потреблять новые значения из потока, и limit(10)
никогда не будет достигнут. Не повезло, ваше приложение глохнет.
4. Случайно создавая «тонкие» параллельные бесконечные потоки
Мы действительно должны настаивать на том, что вы можете случайно попытаться использовать бесконечный поток. Предположим, вы считаете, что операция distinct()
должна выполняться параллельно. Вы могли бы написать это:
1
2
3
4
5
|
IntStream.iterate( 0 , i -> ( i + 1 ) % 2 ) .parallel() .distinct() .limit( 10 ) .forEach(System.out::println); |
Теперь мы уже видели, что это будет навсегда. Но ранее, по крайней мере, вы использовали только один процессор на вашем компьютере. Теперь вы, вероятно, потребите четыре из них, потенциально занимая почти всю вашу систему со случайным бесконечным потреблением потока. Это довольно плохо. После этого вы, вероятно, сможете перезагрузить свой сервер / компьютер для разработки. Посмотрите, как выглядел мой ноутбук до взрыва:
5. Перепутать порядок операций
Итак, почему мы настаивали на том, чтобы вы случайно создали бесконечные потоки? Это просто. Потому что вы можете просто случайно сделать это. Вышеуказанный поток может быть полностью использован, если вы переключаете порядок limit()
и distinct()
:
1
2
3
4
|
IntStream.iterate( 0 , i -> ( i + 1 ) % 2 ) .limit( 10 ) .distinct() .forEach(System.out::println); |
Это теперь дает:
1
2
|
0 1 |
Почему? Поскольку мы сначала ограничиваем бесконечный поток до 10 значений (0 1 0 1 0 1 0 1 0 1), прежде чем мы уменьшим ограниченный поток до различных значений, содержащихся в нем (0 1).
Конечно, это может больше не быть семантически правильным, потому что вы действительно хотели получить первые 10 различных значений из набора данных (вы просто «забыли», что данные бесконечны). Никто на самом деле не хочет 10 случайных значений, и только потом уменьшать их, чтобы они были различимы.
Если вы пришли из SQL-фона, вы можете не ожидать таких различий. Возьмите SQL Server 2012, например. Следующие два оператора SQL одинаковы:
01
02
03
04
05
06
07
08
09
10
11
|
-- Using TOP SELECT DISTINCT TOP 10 * FROM i ORDER BY .. -- Using FETCH SELECT * FROM i ORDER BY .. OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY |
Так что, как человек SQL, вы можете не осознавать важность порядка операций с потоками.
6. Перепутать порядок операций (снова)
Говоря о SQL, если вы являетесь пользователем MySQL или PostgreSQL, вы можете использовать предложение LIMIT .. OFFSET
. SQL полон тонких причуд, и это одна из них. Предложение OFFSET
применяется вначале , как это предлагается в синтаксисе SQL Server 2012 (т. Е. В стандарте SQL: 2008 ).
Если вы переведете диалект MySQL / PostgreSQL непосредственно в потоки, вы, вероятно, ошибетесь:
1
2
3
4
|
IntStream.iterate( 0 , i -> i + 1 ) .limit( 10 ) // LIMIT .skip( 5 ) // OFFSET .forEach(System.out::println); |
Вышеуказанные урожаи
1
2
3
4
5
|
5 6 7 8 9 |
Да. Это не продолжается после 9
, потому что limit()
теперь применяется первым , производя (0 1 2 3 4 5 6 7 8 9). skip()
применяется после уменьшения потока до (5 6 7 8 9). Не то, что вы, возможно, намеревались.
ОСТОРОЖНО ОГРАНИЧЕНИЯ LIMIT .. OFFSET
против ловушки "OFFSET .. LIMIT"
!
7. Хождение по файловой системе с фильтрами
Мы уже писали об этом раньше . Хорошей идеей является обход файловой системы с использованием фильтров:
1
2
3
|
Files.walk(Paths.get( "." )) .filter(p -> !p.toFile().getName().startsWith( "." )) .forEach(System.out::println); |
Вышеупомянутый поток, кажется, проходит только через не скрытые каталоги, то есть каталоги, которые не начинаются с точки. К сожалению, вы снова допустили ошибку № 5 и № 6. walk()
уже создала весь поток подкаталогов текущего каталога. Хотя и лениво, но логически содержит все подпути. Теперь фильтр правильно отфильтрует пути, имена которых начинаются с точки «.». Например, .git
или .idea
не будут частью результирующего потока. Но эти пути будут следующими:. .\.git\refs
или .\.idea\libraries
. Не то, что вы хотели.
Не исправляйте это, написав следующее:
1
2
3
|
Files.walk(Paths.get( "." )) .filter(p -> !p.toString().contains(File.separator + "." )) .forEach(System.out::println); |
Несмотря на то, что это даст правильный вывод, он все равно будет делать это, обходя полное поддерево каталога, возвращаясь во все подкаталоги «скрытых» каталогов.
Я думаю, вам придется снова прибегнуть к старому доброму JDK 1.0 File.list()
. Хорошей новостью является то, что FilenameFilter
и FileFilter
являются функциональными интерфейсами.
8. Изменение резервной коллекции потока
Пока вы выполняете итерацию List
, вы не должны изменять этот же список в теле итерации. Это было верно до Java 8, но это может стать более сложным с потоками Java 8. Рассмотрим следующий список из 0..9:
1
2
3
4
5
|
// Of course, we create this list using streams: List<Integer> list = IntStream.range( 0 , 10 ) .boxed() .collect(toCollection(ArrayList:: new )); |
Теперь давайте предположим, что мы хотим удалить каждый элемент при его использовании:
1
2
3
4
|
list.stream() // remove(Object), not remove(int)! .peek(list::remove) .forEach(System.out::println); |
Интересно, что это будет работать для некоторых элементов! Вывод, который вы можете получить:
01
02
03
04
05
06
07
08
09
10
11
|
0 2 4 6 8 null null null null null java.util.ConcurrentModificationException |
Если мы проанализируем список после того, как поймал это исключение, то получится забавная находка. Мы получим:
1
|
[ 1 , 3 , 5 , 7 , 9 ] |
Хех, это «сработало» для всех нечетных чисел. Это ошибка? Нет, это похоже на особенность. Если вы углубляетесь в код JDK, вы найдете этот комментарий в ArrayList.ArraListSpliterator
:
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
29
30
31
|
/* * If ArrayLists were immutable, or structurally immutable (no * adds, removes, etc), we could implement their spliterators * with Arrays.spliterator. Instead we detect as much * interference during traversal as practical without * sacrificing much performance. We rely primarily on * modCounts. These are not guaranteed to detect concurrency * violations, and are sometimes overly conservative about * within-thread interference, but detect enough problems to * be worthwhile in practice. To carry this out, we (1) lazily * initialize fence and expectedModCount until the latest * point that we need to commit to the state we are checking * against; thus improving precision. (This doesn't apply to * SubLists, that create spliterators with current non-lazy * values). (2) We perform only a single * ConcurrentModificationException check at the end of forEach * (the most performance-sensitive method). When using forEach * (as opposed to iterators), we can normally only detect * interference after actions, not before. Further * CME-triggering checks apply to all other possible * violations of assumptions for example null or too-small * elementData array given its size(), that could only have * occurred due to interference. This allows the inner loop * of forEach to run without any further checks, and * simplifies lambda-resolution. While this does entail a * number of checks, note that in the common case of * list.stream().forEach(a), no checks or other computation * occur anywhere other than inside forEach itself. The other * less-often-used methods cannot take advantage of most of * these streamlinings. */ |
Теперь посмотрим, что происходит, когда мы сообщаем потоку о результатах sorted()
:
1
2
3
4
|
list.stream() .sorted() .peek(list::remove) .forEach(System.out::println); |
Это теперь даст следующий, «ожидаемый» результат
01
02
03
04
05
06
07
08
09
10
|
0 1 2 3 4 5 6 7 8 9 |
А список после потребления потока? Пусто
1
|
|
Итак, все элементы израсходованы и удалены правильно. Операция sorted()
является «промежуточной операцией с отслеживанием состояния» , что означает, что последующие операции больше не работают с резервной коллекцией, но находятся во внутреннем состоянии. Теперь «безопасно» удалять элементы из списка!
Ну … мы можем на самом деле? Давайте продолжим с удалением parallel()
, sorted()
:
1
2
3
4
5
|
list.stream() .sorted() .parallel() .peek(list::remove) .forEach(System.out::println); |
Это теперь дает:
01
02
03
04
05
06
07
08
09
10
|
7 6 2 5 8 4 1 0 9 3 |
И список содержит
1
|
[ 8 ] |
Ик. Мы не удалили все элементы !? Бесплатное пиво ( и наклейки JOOQ ) отправляются всем, кто решает эту головоломку с потоками!
Все это выглядит довольно случайным и тонким, мы можем только предположить, что вы никогда не изменяете резервную копию при использовании потока. Это просто не работает.
9. Забыв на самом деле потреблять поток
Как вы думаете, что делает следующий поток?
1
2
3
4
5
6
|
IntStream.range( 1 , 5 ) .peek(System.out::println) .peek(i -> { if (i == 5 ) throw new RuntimeException( "bang" ); }); |
Когда вы читаете это, вы можете подумать, что он напечатает (1 2 3 4 5) и затем выдаст исключение. Но это не правильно. Это ничего не сделает. Поток просто сидит там, никогда не истощенный.
Как и в случае любого свободного API или DSL, вы можете забыть вызвать операцию «терминал». Это может быть особенно актуально, когда вы используете peek()
, так как peek()
ужасно похож на forEach()
.
Это может произойти с jOOQ точно так же, когда вы забудете вызвать execute()
или fetch()
:
1
2
3
4
5
|
DSL.using(configuration) .update(TABLE) .set(TABLE.COL1, 1 ) .set(TABLE.COL2, "abc" ) .where(TABLE.ID.eq( 3 )); |
К сожалению. Не execute()
Да, «лучший» способ — с 1-2 оговорками!
10. Параллельный поток тупик
Теперь это настоящее лакомство для конца!
Все параллельные системы могут застрять, если вы не синхронизируете их должным образом. Хотя нахождение реального примера не очевидно, нахождение принудительного примера есть. Следующий поток parallel()
гарантированно заходит в тупик:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
Object[] locks = { new Object(), new Object() }; IntStream .range( 1 , 5 ) .parallel() .peek(Unchecked.intConsumer(i -> { synchronized (locks[i % locks.length]) { Thread.sleep( 100 ); synchronized (locks[(i + 1 ) % locks.length]) { Thread.sleep( 50 ); } } })) .forEach(System.out::println); |
Обратите внимание на использование Unchecked.intConsumer()
, которое преобразует функциональный интерфейс IntConsumer
в org.jooq.lambda.fi.util.function.CheckedIntConsumer
, которому разрешено генерировать проверенные исключения.
Что ж. Не повезло вашей машине. Эти темы будут заблокированы навсегда!
Хорошей новостью является то, что никогда не было так легко привести пример тупика в учебнике на Java!
Для получения дополнительной информации см. Также ответ Брайана Гетца на этот вопрос о переполнении стека .
Вывод
С потоками и функциональным мышлением мы столкнемся с огромным количеством новых, тонких ошибок. Немногие из этих ошибок могут быть предотвращены, кроме как с помощью практики и концентрации внимания. Вы должны подумать о том, как заказать ваши операции. Вы должны подумать о том, могут ли ваши потоки быть бесконечными.
Потоки (и лямбды) — очень мощный инструмент. Но инструмент, который нам нужно освоить, в первую очередь.
Ссылка: | Java 8 Пятница: 10 незначительных ошибок при использовании API Streams от нашего партнера по JCG Лукаса Эдера в блоге JAVA, SQL и AND JOOQ . |