С появлением Java 8 Stream API позволило программистам писать существенно короче то, что раньше занимало много строк кода. Однако оказалось, что многие даже с использованием Stream API пишут длиннее, чем надо. Причём это не только делает код длиннее и усложняет его понимание, но иногда приводит к существенному провалу производительности. Не всегда понятно, почему люди так пишут. Возможно, они прочитали только небольшой кусок документации, а про другие возможности не слышали. Или вообще документацию не читали, просто видели где-то пример и решили сделать похоже. Иногда это напоминает анекдот про «задача сведена к предыдущей».
В этой статье я собрал те примеры, с которыми столкнулся в практике. Надеюсь, после такого ликбеза код программистов станет чуточку красивее и быстрее. Большинство этих штук хорошая IDE поможет вам исправить, но IDE всё-таки не всесильна и голову не заменяет.
1. Стрим из коллекции без промежуточных операций обычно не нужен
Если у вас промежуточных операций нет, часто можно и нужно обойтись без стрима.
1.1. collection.stream().forEach()
Хотите что-то сделать для всех элементов коллекции? Замечательно. Но зачем вам стрим? Напишите просто collection.forEach()
. В большинстве случаев это одно и то же, но короче и производит меньше мусора. Некоторые боятся, что различие в функциональности есть, но не могут толком объяснить, какое оно. Говорят, мол, forEach
не гарантирует порядок. Как раз в стриме по спецификации не гарантирует (по факту он есть), а без стрима для упорядоченных коллекций гарантирует. Если порядок вам не нужен, вам не станет хуже, если он появится. Единственное отличие из стандартной библиотеки, которое мне известно — это синхронизированные коллекции, созданные через Collections.synchronizedXyz()
. В этом случае collection.forEach()
синхронизирует всю операцию, тогда как collection.stream().forEach()
не синхронизирует ничего. Скорее всего, если вы уж используете синхронизированные коллекции, вам всё-таки синхронизация нужна, поэтому станет только лучше.
1.2. collection.stream().collect(Collectors.toList())
Собираетесь преобразовать произвольную коллекцию в список? Замечательно. Начиная с Java 1.2 у вас есть отличная возможность для этого: new ArrayList<>(collection)
(ну хорошо, до Java 5 дженериков не было). Это не только короче, но и быстрее и опять же создаст меньше мусора в куче. Может быть значительно меньше, так как в большинстве случаев у вас выделится один массив нужного размера, тогда как стрим будет добавлять элементы по одному, растягивая по мере необходимости. Аналогично вместо stream().collect(toSet())
создаём new HashSet<>()
, а вместо stream().collect(toCollection(TreeSet::new))
— new TreeSet<>()
.
1.3. collection.stream().toArray(String[]::new)
Новый способ преобразования в массив ничем не лучше старого доброго collection.toArray(new String[0])
. Опять же: так как на пути меньше абстракций, преобразование может оказаться более эффективно. Во всяком случае объект стрима вам не нужен.
1.4. collection.stream().max(Comparator.naturalOrder()).get()
Есть замечательный метод Collections.max
, который почему-то незаслуженно многими забыт. Вызов Collections.max(collection)
сделает то же самое и опять же с меньшим количеством мусора. Если у вас свой компаратор, используйте Collections.max(collection, comparator)
. Метод Collections.max()
подойдёт хуже, если вы хотите специально обработать пустую коллекцию, тогда стрим более оправдан. Цепочка collection.stream().max(comparator).orElse(null)
смотрится лучше, чем collection.isEmpty() ? null : Collections.max(collection, comparator)
.
1.5. collection.stream().count()
Это совсем ни в какие ворота не лезет: есть ведь collection.size()
! Если в Java 9 count()
отработает быстро, то в Java 8 этот вызов всегда пересчитывает все элементы, даже если размер очевиден. Не делайте так.
2. Поиск элемента
2.1. stream.filter(condition).findFirst().isPresent()
Такой код вижу на удивление часто. Суть его: проверить, выполняется ли условие для какого-то элемента стрима. Именно для этого есть специальный метод: stream.anyMatch(condition)
. Зачем вам Optional
?
2.2. !stream.anyMatch(condition)
Тут некоторые поспорят, но я считаю, что использовать специальный метод stream.noneMatch(condition)
более выразительно. А вот если и в условии отрицание: !stream.anyMatch(x -> !condition(x))
, то тут однозначно лучше написать stream.allMatch(x -> condition(x))
. Тот, кто будет читать код, скажет вам спасибо.
2.3. stream.map(condition).anyMatch(b -> b)
И такой странный код иногда пишут, чтобы запутать коллег. Если увидите такое, знайте, что это просто stream.anyMatch(condition)
. Здесь же вариации на тему вроде stream.map(condition).noneMatch(Boolean::booleanValue)
или stream.map(condition).allMatch(Boolean.TRUE::equals)
.
3. Создание стрима
3.1. Collections.emptyList().stream()
Нужен пустой стрим? Бывает, ничего страшного. И для этого есть специальный метод Stream.empty()
. Производительность одинаковая, но короче и понятнее. Метод emptySet
здесь не отличается от emptyList
.
3.2. Collections.singleton(x).stream()
И тут можно упростить жизнь: если вам потребовался стрим из одного элемента, пишите просто Stream.of(x)
. Опять же без разницы singleton
или singletonList
: когда в стриме один элемент, никого не волнует, упорядочен стрим или нет.
3.3. Arrays.asList(array).stream()
Развитие этой же темы. Люди почему-то так делают, хотя Arrays.stream(array)
или Stream.of(array)
отработает не хуже. Если вы указываете элементы явно (Arrays.asList(x, y, z).stream()
), то Stream.of(x, y, z)
тоже сработает. Аналогично с EnumSet.of(x, y, z).stream()
. Вам же стрим нужен, а не коллекция, так и создавайте сразу стрим.
3.4. Collections.nCopies(N, "ignored").stream().map(ignored -> new MyObject())
Нужен стрим из N одинаковых объектов? Тогда nCopies()
— ваш выбор. А вот если нужно сгенерировать стрим из N объектов, созданных одним и тем же способом, то тут красивее и оптимальнее воспользоваться Stream.generate(() -> new MyObject()).limit(N)
.
3.5. IntStream.range(from, to).mapToObj(idx -> array[idx])
Нужен стрим из куска массива? Есть специальный метод Arrays.stream(array, from, to)
. Опять же короче и меньше мусора, плюс так как массив больше не захвачен лямбдой, он не обязан быть effectively-final. Понятно, если from — это 0, а to — это array.length
, тогда вам просто нужен Arrays.stream(array)
, причём тут код станет приятнее, даже если в mapToObj
что-то более сложное. Например, IntStream.range(0, strings.length).mapToObj(idx -> strings[idx].trim())
легко превращается в Arrays.stream(strings).map(String::trim)
.
Более хитрая вариация на тему — IntStream.range(0, Math.min(array.length, max)).mapToObj(idx -> array[idx])
. Немножко подумав, понимаешь, что это Arrays.stream(array).limit(max)
.
4. Ненужные и сложные коллекторы
Иногда люди изучают коллекторы и всё пытаются делать через них. Однако не всегда они нужны.
4.1. stream.collect(Collectors.counting())
Многие коллекторы нужны только как вторичные в сложных каскадных операциях вроде groupingBy
. Коллектор counting()
как раз из них. Пишите stream.count()
и не мучайтесь. Опять же если в Java 9 count()
может иногда выполниться за константное время, то коллектор всегда будет пересчитывать элементы. А в Java 8 коллектор counting()
ещё и боксит зазря (я это исправил в Java 9). Из этой же оперы коллекторы maxBy()
, minBy()
(есть методы max()
и min()
), reducing()
(используйте reduce()
), mapping()
(просто добавьте шаг map()
, а затем воспользуйтесь вторичным коллектором напрямую). В Java 9 добавились filtering()
и flatMapping()
, которые также дублируют соответствующие промежуточные операции.
4.2. groupingBy(classifier, collectingAndThen(maxBy(comparator), Optional::get))
Частая задача: хочется сгруппировать элементы по классификатору, выбрав в каждой группе максимум. В SQL это выглядит просто SELECT classifier, MAX(...) FROM ... GROUP BY classifier
. Видимо, пытаясь перенести опыт SQL, люди пытаются использовать тот же самый groupingBy и в Stream API. Казалось бы должно сработать groupingBy(classifier, maxBy(comparator))
, но нет. Коллектор maxBy
возвращает Optional
. Но мы-то знаем, что вложенный Optional
всегда не пуст, так как в каждой группе по крайней мере один элемент есть. Поэтому приходится добавлять некрасивые шаги вроде collectingAndThen
, и всё начинает выглядеть совсем чудовищно.
Однако отступив на шаг назад, можно понять, что groupingBy
тут не нужен. Есть другой замечательный коллектор — toMap
, и это как раз то что надо. Мы просто хотим собрать элементы в Map
, где ключом будет классификатор, а значением сам элемент. В случае же дубликата выберем больший из них. Для этого, кстати, есть BinaryOperator.maxBy(comparator)
, который можно статически импортировать вместо одноимённого коллектора. В результате имеем: toMap(classifier, identity(), maxBy(comparator))
.
Если вы порываетесь использовать groupingBy
, а вторичным коллектором у вас maxBy
, minBy
или reducing
(возможно, с промежуточным mapping
), посмотрите в сторону коллектора toMap
— может полегчать.
5. Не считайте то, что не нужно считать
5.1. listOfLists.stream().flatMap(List::stream).count()
Это перекликается с пунктом 1.5. Мы хотим посчитать суммарное число элементов во вложенных коллекциях. Казалось бы всё логично: растянем эти коллекции в один стрим с помощью flatMap
и пересчитаем. Однако в большинстве случаев размеры вложенных списков уже посчитаны, хранятся у них в поле и легко доступны с помощью метода size()
. Небольшая модификация существенно увеличит скорость операции: listOfLists.stream().mapToInt(List::size).sum()
. Если боитесь, что int
переполнится, mapToLong
тоже сработает.
5.2. if(stream.filter(condition).count() > 0)
Опять же забавный способ записать stream.anyMatch(condition)
. Но в отличие от довольно безобидного 2.1 вы тут теряете короткое замыкание: будут перебраны все элементы, даже если условие сработало на самом первом. Аналогично если вы проверяете filter(condition).count() == 0
, лучше воспользоваться noneMatch(condition)
.
5.3. if(stream.count() > 2)
Этот случай более хитрый. Вам теперь важно знать, больше двух элементов в стриме или нет. Если вас волнует производительность, возможно, стоит вставить stream.limit(3).count()
. Вам ведь не важно, сколько их, если их больше двух.
6. Разное
6.1. stream.sorted(comparator).findFirst()
Что хотел сказать автор? Отсортируй стрим и возьми первый элемент. Это же всё равно что взять минимальный элемент: stream.min(comparator)
. Иногда видишь даже stream.sorted(comparator.reversed()).findFirst()
, что аналогично stream.max(comparator)
. Реализация Stream API не соптимизирует тут (хотя могла бы), а сделает всё как вы сказали: соберёт стрим в промежуточный массив, отсортирует его весь и выдаст вам первый элемент. Вы существенно потеряете в памяти и скорости на такой операции. Ну и, конечно, замена существенно понятнее.
6.2. stream.map(x -> {counter.addAndGet(x);return x;})
Некоторые люди пытаются выполнить какой-нибудь побочный эффект в стриме. Вообще это в принципе уже звоночек, что может стрим вам вовсе и не нужен. Но так или иначе для этих целей есть специальный метод peek
. Пишем stream.peek(counter::addAndGet)
.
На этом у меня всё. Если вы сталкивались со странными и неэффективными способами использования Stream API, напишите о них в комментариях.
Комментарии (45)
molecularmanvlz
07.09.2017 10:10+2После scala стримы на джаве кажутся жутко неудобными (или неочевидными). Так же пришлось какие-то привычные вещи портировать в джаву. Было бы здорово избавиться от ненужного явного преобразования в стрим и добавить синтаксический сахар для «большей наглядности кода». Тогда джава заблистает опять новыми красками (я не говорю что сейчас плохо, но хочется лучшего)
lany Автор
07.09.2017 10:22+14У джавы своя философия. В частности, в джаве ценится ясность кода. Наличие методов типа map прямо у коллекций не даёт явного понимания, метод ленивый или нет, будет ли промежуточная коллекция, если сделать два раза map или нет. А со стримами всё понятно. Вы можете сказать, мол, давайте скажем, что у коллекции все методы неленивы. Но если сама коллекция — это вьюшка над другой коллекцией? Скала тут как раз засорила все абстрактные интерфейсы кучей всего. Пришёл вам
Traversable
, вы вызываете у негоmap
. Что вы получите, новую независимую копию или вьюшку над старой? Зависит от того, что вам на самом деле там передали,TraversableView
или тупоList
. По факту протекающая абстракция. Кому-то нравится такой подход, абстрагировать всё и вся, те пишут на Скале. Мне нравится, когда я лучше понимаю, что в моём коде происходит, поэтому я пишу на Джаве. Это замечательно, что есть разные языки с разной философией.
deadyshk
07.09.2017 10:22Примеры кажутся нереальными) неужели действительно все они из реальных проектов?)
lany Автор
07.09.2017 10:22+7Мне многие из них тоже казались нереальными, пока я их не увидел в реальном коде :-)
AstarothAst
07.09.2017 14:25Сначала прочитал «пока я их не увидел в *своем* реальном коде». Потом подумал и понял, что часто так оно и бывает :)
Yo1
07.09.2017 10:26-4бесполезная штука в реальной жизни, потому как разобраться откуда эксепшен и что в данных не так практически не реально. проще сразу писать так, что бы при проблеме получить вменяемый эксепшен
IvanPonomarev
07.09.2017 11:03+3Не далее как вчера наткнулся вот на такое: List sortedParams = params.stream().sorted(...).collect(Collectors.toList()); Мало того, что Collections.sort(..) по-прежнему работает, в Java8 же есть ещё default-метод sort(...) на List-е!
В примере 2.3 наверное имелось в виду b -> true, а не b->b?lany Автор
07.09.2017 11:28+1Нет-нет, именно
b->b
.
Ваш пример интересен. sort() меняет текущий список. По сути дела замена — это два выражения,
List<T> sortedParams = new ArrayList(params);sortedParams.sort(...);
. Это заметно оптимальнее, но можно спорить, красивее ли. Теперь, к примеру,sortedParams
не заинлайнишь, к примеру.
AstarothAst
07.09.2017 14:29params.stream().sorted(...).collect(Collectors.toList())
А если нужно сохранить исходный params в неприкосновенности? Коллектор toList ведь порождает новую коллекцию, не трогая ту, из которой приготовлен стрим.IvanPonomarev
07.09.2017 14:49Ниже уже разобрались. Конкретно в том месте не нужно было, если бы и нужно было, я бы всё равно предпочёл написать List sortedParams = new ArrayList(params); sortedParams.sort(...);, хотя тут уже действительно, возможно, дело вкуса.
IvanPonomarev
07.09.2017 11:54+2Чорт, перечитал пример 2.3 внимательнее! Я просто видел такое: stream.filter(condition).anyMatch(x -> true) И ещё очень часто встречается first (find/match), там где достаточно any.
Насчёт примера с sorted: если бы копия списка там была нужна! Но копия списка там была не нужна.
radistao
07.09.2017 14:39Меня удручает, что оба
Stream
иOptional
имеют метод#map(map(Function<? super T, ? extends U> mapper)
, который выглядит одинаково, но имеет несколько разный смысл:
- в
Optional
mapper вызовется только для ненулевого элемента - в
Stream
mapper вызовется для любых элементов стрима (нужно делатьfilter()
, если хотим исключить нулевые)
Сейчас уже привык и веду себя осторожнее, но вижу иногда, как новички наступают на те же грабли.
mayorovp
08.09.2017 06:30"Нулевой элемент" в Optional обозначает отсутствие элемента. Как в принципе можно вызвать mapper для отсутствующего элемента?
Или с другой стороны: Optional делали чтобы избавиться от null. Как можно использовать Optional и при этом ожидать где-то null?
lany Автор
08.09.2017 06:53У
Optional
действительно есть особенность, нарушающая монадический закон:map(map(opt, F), G) ? map(opt, G0F)
или в Java-терминахopt.map(F).map(G) != opt.map(G.compose(F))
. Если F возвращает null для какого-либо значения, то в композиции G выполнится с аргументом null, а вOptional
G не выполнится вообще. Пуристы в этом месте начинают плеваться. Ну и, например, соответствующий рефакторинг для Optional, который предлагает IDEA, по факту меняет семантику кода (а для Stream не меняет).
- в
radistao
07.09.2017 14:47+2На английском не планируется статьи? Хотел коллегам показать.
zvorygin
08.09.2017 06:20Видел несколько раз следующий код с ненужным промежуточным collect. Часто получается в процессе рефакторинга.
List<T> temp = collection.stream().filter(...).map(...).collect(Collectors.toList()); ... some unrelated to "temp" stuff... List<T> result = temp.stream().filter(...).map(...).collect(Collectors.toList());
Вместо
List<T> result = collection.stream().filter(...).map(...).filter(...).map(...).collect(Collectors.toList());
lany Автор
08.09.2017 06:26Да, я такое тоже встречал. К сожалению, автоматически предлагать склеить это в одну цепочку опасно. Может человек полагается на сайд-эффекты и ему важно, чтобы все первые операции выполнились до вторых. Классический пример:
List<CompletableFuture<T>> futures = callables.stream() .map(CompletableFuture::supplyAsync).collect(toList()); List<T> results = futures.stream() .map(CompletableFuture::join).collect(toList());
Здесь если склеить в один стрим, то все фоновые задачи будут выполняться последовательно (причём этого можно не заметить, скорее всего тесты не упадут).
Evlikat
08.09.2017 06:52+1Встречал вот такое:
List<String> list = Arrays.asList("A", "B", "C"); List<String> copy = list.stream().map(String::new).collect(Collectors.toList());
DmitriyLuckyman
08.09.2017 16:39+16.1. stream.sorted(comparator).findFirst()
Тут на самом деле есть некоторая ментальная ловушка.
Когда мы говорим о методе min или max, то невольно в голове ссылаемся на числовое представление.
А когда объекты в стриме мы не представляем как последовательность чисел, то некоторые и не думают, что можно использование данные операции.
То есть если бы методы назывались last(Comparator) и first(Comparator), то скорее всего эти методы были бы использованы.
То есть разработчик здесь говорит упорядочи каким-то способом набор объектов и возьми самый первый(Он не думает про объект в терминах минимальный/максимальный).
Maccimo
08.09.2017 18:30+1То есть если бы методы назывались last(Comparator) и first(Comparator), то скорее всего эти методы были бы использованы.
До чтения javadoc/просмотра реализации, ориентируясь только по названиям, я бы решил, что это методы, возвращающие первое и последнее по порядку следования вхождения элементов в stream, на которые сделал стойку компаратор. А вот с
min
/max
всё однозначно.
То есть разработчик здесь… не думает про объект в терминах минимальный/максимальный.
Это неправильный разработчик и он даёт неправильный мёд.
Beholder
08.09.2017 18:00+2Была необходимость определить что либо хоть один элемент в стриме удовлетворяет предикату, либо стрим пуст. Пришлось использовать
peek
Можно придумать вариант лучше?final AtomicBoolean streamIsEmpty = new AtomicBoolean(true); final boolean anyMatch = someStream(...) .map(...) .peek(obj -> streamIsEmpty.set(false)) .anyMatch(condition); return anyMatch || streamIsEmpty.get();
artspb
08.09.2017 18:06+1Вот только 30 минут назад в очередной раз столкнулся с этой проблемой. К счастью, у меня доступна исходная коллекция, и я ее могу проверить на пустоту. Как элегантно решить проблему в общем случае я не знаю.
lany Автор
09.09.2017 06:26Собственно да. С помощью стандартного Stream API вроде красиво не решить. Моя библиотека StreamEx позволяет легко сделать такой коллектор:
static <T> Collector<T, ?, Boolean> anyMatchOrEmpty(Predicate<T> predicate) { return MoreCollectors.pairing( MoreCollectors.filtering(predicate, MoreCollectors.first()), MoreCollectors.first(), (filtered, nonFiltered) -> filtered.isPresent() || !nonFiltered.isPresent()); }
С обычным стримом он не будет короткозамкнут, но если источник обернуть в StreamEx (или если источник сразу возвращает StreamEx), то будет:
return StreamEx.of(someStream(...)).collect(anyMatchOrEmpty(condition));
lany Автор
09.09.2017 09:51+1Насчёт IDE не заменяет голову, кстати, пример. IDEA предупредит, если вы напишете
Arrays.asList(array).stream()
. Но если у вас сложный внешний стрим и в нём встретится.map(Arrays::asList).flatMap(List::stream)
вместо простого.flatMap(Arrays::stream)
, то тут уже не предупредит. Или проfilter().findFirst().isPresent()
скажет, но если вы промежуточныйOptional
присвоите в переменную и используете её только дляisPresent
, то уже увы. Может когда-нибудь и эти случаи покроем, но не стоит надеяться, что IDE вам подскажет всегда.
AndreyRubankov
09.09.2017 11:11+11.1. collection.stream().forEach()
…
Напишите просто collection.forEach().
Если для метода не передается Consumer из вне, то лучше использовать старый-добрый форыч.
for (T it : collection) { ... }
Более того, обычно метод forEach() используют в связке с замыканием, чтобы туда сложить значения. Что еще более печально выглядит.
ps: Я до сих пор не понимаю, почему разработчики Java решили добавить Stream API непосредственно в интерфейс коллекция?
Ведь практически всегда можно: Stream.of(… ) и погнали. Зачем было уродовать интерфейс коллекций? Притом с подходом Stream.of – коллекции бы не зависели от Стримов, а так получается довольно жесткая связанность между Стримом и Коллекцией :(lany Автор
09.09.2017 12:15forEach
изящнее выглядит со ссылкой на метод. Кроме того может быть несколько быстрее, потому что не создаётся итератор.
Я до сих пор не понимаю, почему разработчики Java решили добавить Stream API непосредственно в интерфейс коллекция?
Ну вот любят люди всё такое fluent-fluent. Все вон трещат, чтобы им прямо в коллекцию добавили
map
илиfilter
. Написать свой утилитный класс и вызывать статические методы религия не позволяет.Stream.from(veryLongCallProducingACollection).many().stream().operations()
тоже смотрится не очень.
Вообще с точки зрения разработчиков Java, стримы более базовая вещь, чем коллекции. От коллекций по факту зависит только класс
Collectors
, который утилитный. Остальные стримы от коллекций не зависят.AndreyRubankov
09.09.2017 13:37+1forEach изящнее выглядит со ссылкой на метод. Кроме того может быть несколько быстрее, потому что не создаётся итератор.
Изящнее это только с ссылкой на метод, согласен. Но если пишут лямбду или того хуже – лямбду-многострочник, это смотрится хуже, особенно с захватом переменной.
А по производительности скорее всего будет одинаково (в приделах погрешности).
стримы более базовая вещь, чем коллекции.
Стримы – это фреймворк для работы с данными, который свободно может стоять особняком.
Да, конечно в ситуации, когда стрим порождает коллекцию из которой берется новый стрим этот код смотрелся бы отвратно, но ведь если так делается, то это уже признак, что что-то делается не так, как следовало бы.
list.stream().(transforming).collect(toList()).stream(). ...
хотя, я наверно еще не сталкивался с реально сложной работой с данными, где действительно пригодился бы .stream() как интерфейсный метод.
vlanko
Отличная статья. А что есть из новшеств стримов в java 9?
lany Автор
Там не очень много. Почти всё перечислено, например, здесь