Pull to refresh

Comments 50

Отличная статья. А что есть из новшеств стримов в java 9?
После scala стримы на джаве кажутся жутко неудобными (или неочевидными). Так же пришлось какие-то привычные вещи портировать в джаву. Было бы здорово избавиться от ненужного явного преобразования в стрим и добавить синтаксический сахар для «большей наглядности кода». Тогда джава заблистает опять новыми красками (я не говорю что сейчас плохо, но хочется лучшего)

У джавы своя философия. В частности, в джаве ценится ясность кода. Наличие методов типа map прямо у коллекций не даёт явного понимания, метод ленивый или нет, будет ли промежуточная коллекция, если сделать два раза map или нет. А со стримами всё понятно. Вы можете сказать, мол, давайте скажем, что у коллекции все методы неленивы. Но если сама коллекция — это вьюшка над другой коллекцией? Скала тут как раз засорила все абстрактные интерфейсы кучей всего. Пришёл вам Traversable, вы вызываете у него map. Что вы получите, новую независимую копию или вьюшку над старой? Зависит от того, что вам на самом деле там передали, TraversableView или тупо List. По факту протекающая абстракция. Кому-то нравится такой подход, абстрагировать всё и вся, те пишут на Скале. Мне нравится, когда я лучше понимаю, что в моём коде происходит, поэтому я пишу на Джаве. Это замечательно, что есть разные языки с разной философией.

Guava Collections неплохи, и с лямбдами получается достаточно компактный код.

Примеры кажутся нереальными) неужели действительно все они из реальных проектов?)

Мне многие из них тоже казались нереальными, пока я их не увидел в реальном коде :-)

Сначала прочитал «пока я их не увидел в *своем* реальном коде». Потом подумал и понял, что часто так оно и бывает :)
бесполезная штука в реальной жизни, потому как разобраться откуда эксепшен и что в данных не так практически не реально. проще сразу писать так, что бы при проблеме получить вменяемый эксепшен
Не далее как вчера наткнулся вот на такое: List sortedParams = params.stream().sorted(...).collect(Collectors.toList()); Мало того, что Collections.sort(..) по-прежнему работает, в Java8 же есть ещё default-метод sort(...) на List-е!

В примере 2.3 наверное имелось в виду b -> true, а не b->b?

Нет-нет, именно b->b.


Ваш пример интересен. sort() меняет текущий список. По сути дела замена — это два выражения, List<T> sortedParams = new ArrayList(params);sortedParams.sort(...);. Это заметно оптимальнее, но можно спорить, красивее ли. Теперь, к примеру, sortedParams не заинлайнишь, к примеру.

params.stream().sorted(...).collect(Collectors.toList())

А если нужно сохранить исходный params в неприкосновенности? Коллектор toList ведь порождает новую коллекцию, не трогая ту, из которой приготовлен стрим.
Ниже уже разобрались. Конкретно в том месте не нужно было, если бы и нужно было, я бы всё равно предпочёл написать List sortedParams = new ArrayList(params); sortedParams.sort(...);, хотя тут уже действительно, возможно, дело вкуса.
Чорт, перечитал пример 2.3 внимательнее! Я просто видел такое: stream.filter(condition).anyMatch(x -> true) И ещё очень часто встречается first (find/match), там где достаточно any.

Насчёт примера с sorted: если бы копия списка там была нужна! Но копия списка там была не нужна.
Когда не посмотрел, кто автор статьи. Но замыкание зачетное, да :)

А для Optional и CompletionStage такие статьи ожидаются?

Про Optional может быть. С CompletionStage не наберётся материала, его используют сильно реже.

Меня удручает, что оба Stream и Optional имеют метод #map(map(Function<? super T, ? extends U> mapper), который выглядит одинаково, но имеет несколько разный смысл:


  • в Optional mapper вызовется только для ненулевого элемента
  • в Stream mapper вызовется для любых элементов стрима (нужно делать filter(), если хотим исключить нулевые)

Сейчас уже привык и веду себя осторожнее, но вижу иногда, как новички наступают на те же грабли.

"Нулевой элемент" в Optional обозначает отсутствие элемента. Как в принципе можно вызвать mapper для отсутствующего элемента?


Или с другой стороны: Optional делали чтобы избавиться от null. Как можно использовать Optional и при этом ожидать где-то null?

У Optional действительно есть особенность, нарушающая монадический закон: map(map(opt, F), G) ≠ map(opt, G○F) или в Java-терминах opt.map(F).map(G) != opt.map(G.compose(F)). Если F возвращает null для какого-либо значения, то в композиции G выполнится с аргументом null, а в Optional G не выполнится вообще. Пуристы в этом месте начинают плеваться. Ну и, например, соответствующий рефакторинг для Optional, который предлагает IDEA, по факту меняет семантику кода (а для Stream не меняет).

На английском не планируется статьи? Хотел коллегам показать.

Не поверишь, но около половины нашлось в коде IDEA Ultimate / Android Studio :-)

Сначала пиши код, потом читай спеку — наш подход 8)
Видел несколько раз следующий код с ненужным промежуточным 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());

Да, я такое тоже встречал. К сожалению, автоматически предлагать склеить это в одну цепочку опасно. Может человек полагается на сайд-эффекты и ему важно, чтобы все первые операции выполнились до вторых. Классический пример:


List<CompletableFuture<T>> futures = callables.stream()
   .map(CompletableFuture::supplyAsync).collect(toList());
List<T> results = futures.stream()
   .map(CompletableFuture::join).collect(toList());

Здесь если склеить в один стрим, то все фоновые задачи будут выполняться последовательно (причём этого можно не заметить, скорее всего тесты не упадут).

Такой код лучше как-нибудь отрефакторить.
Встречал вот такое:
List<String> list = Arrays.asList("A", "B", "C");
List<String> copy = list.stream().map(String::new).collect(Collectors.toList());
6.1. stream.sorted(comparator).findFirst()

Тут на самом деле есть некоторая ментальная ловушка.
Когда мы говорим о методе min или max, то невольно в голове ссылаемся на числовое представление.
А когда объекты в стриме мы не представляем как последовательность чисел, то некоторые и не думают, что можно использование данные операции.
То есть если бы методы назывались last(Comparator) и first(Comparator), то скорее всего эти методы были бы использованы.
То есть разработчик здесь говорит упорядочи каким-то способом набор объектов и возьми самый первый(Он не думает про объект в терминах минимальный/максимальный).

То есть если бы методы назывались last(Comparator) и first(Comparator), то скорее всего эти методы были бы использованы.

До чтения javadoc/просмотра реализации, ориентируясь только по названиям, я бы решил, что это методы, возвращающие первое и последнее по порядку следования вхождения элементов в stream, на которые сделал стойку компаратор. А вот с min / max всё однозначно.


То есть разработчик здесь… не думает про объект в терминах минимальный/максимальный.

Это неправильный разработчик и он даёт неправильный мёд.

Была необходимость определить что либо хоть один элемент в стриме удовлетворяет предикату, либо стрим пуст. Пришлось использовать peek
        final AtomicBoolean streamIsEmpty = new AtomicBoolean(true);
        final boolean anyMatch = someStream(...)
                .map(...)
                .peek(obj -> streamIsEmpty.set(false))
                .anyMatch(condition);
        return anyMatch || streamIsEmpty.get();
Можно придумать вариант лучше?
Вот только 30 минут назад в очередной раз столкнулся с этой проблемой. К счастью, у меня доступна исходная коллекция, и я ее могу проверить на пустоту. Как элегантно решить проблему в общем случае я не знаю.

Лучше, наверное, в таких случаях свой Collector писать.

Однако он не будет короткозамкнутым, потому что в стандартном Stream API не бывает короткозамкнутых коллекторов (у меня в StreamEx они есть)

Собственно да. С помощью стандартного 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));
Немного вырожденный пример (скорее всего такой код надо рефактрить), но давайте для примера попробуем reduce() c троичным Boolean (null, true, false):
        Boolean anyMatch = someStream()
                .map(condition)
                .reduce(null, (a, b) -> a == null ? b : a || b);
        return anyMatch == null || anyMatch;

Потребует некоторого времени со стороны разработчика, который будет пытаться понять, зачем такой сложный редьюс (можно снабдить комментарием), но выглядит немного изящнее, чем добавление AtomicBoolean.

Можно и попроще вроде someStream().map(condition).reduce(Boolean::logicalOr).orElse(true). Только это всё без короткого замыкания, что печально.

Насчёт IDE не заменяет голову, кстати, пример. IDEA предупредит, если вы напишете Arrays.asList(array).stream(). Но если у вас сложный внешний стрим и в нём встретится .map(Arrays::asList).flatMap(List::stream) вместо простого .flatMap(Arrays::stream), то тут уже не предупредит. Или про filter().findFirst().isPresent() скажет, но если вы промежуточный Optional присвоите в переменную и используете её только для isPresent, то уже увы. Может когда-нибудь и эти случаи покроем, но не стоит надеяться, что IDE вам подскажет всегда.

1.1. collection.stream().forEach()

Напишите просто collection.forEach().

Если для метода не передается Consumer из вне, то лучше использовать старый-добрый форыч.
for (T it : collection) {
...
}


Более того, обычно метод forEach() используют в связке с замыканием, чтобы туда сложить значения. Что еще более печально выглядит.

ps: Я до сих пор не понимаю, почему разработчики Java решили добавить Stream API непосредственно в интерфейс коллекция?
Ведь практически всегда можно: Stream.of(… ) и погнали. Зачем было уродовать интерфейс коллекций? Притом с подходом Stream.of – коллекции бы не зависели от Стримов, а так получается довольно жесткая связанность между Стримом и Коллекцией :(

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


Я до сих пор не понимаю, почему разработчики Java решили добавить Stream API непосредственно в интерфейс коллекция?

Ну вот любят люди всё такое fluent-fluent. Все вон трещат, чтобы им прямо в коллекцию добавили map или filter. Написать свой утилитный класс и вызывать статические методы религия не позволяет. Stream.from(veryLongCallProducingACollection).many().stream().operations() тоже смотрится не очень.


Вообще с точки зрения разработчиков Java, стримы более базовая вещь, чем коллекции. От коллекций по факту зависит только класс Collectors, который утилитный. Остальные стримы от коллекций не зависят.

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

А по производительности скорее всего будет одинаково (в приделах погрешности).

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

list.stream().(transforming).collect(toList()).stream(). ...


хотя, я наверно еще не сталкивался с реально сложной работой с данными, где действительно пригодился бы .stream() как интерфейсный метод.
Очень часто неопытные программиста при работе с dao/repository делают repo.findAll().stream().sorted(..).findFirst(). То есть выгребают с БД всё, сортируют, и берут первый элемент… Что есть очень плохо, так как: во первых это можно сделать запросом к БД, а во вторых это может привести к ООМ. Советую всем проверять такие места :)
Я не сразу поняла, что ООМ — это OutOfMemoryError…
>Говорят, мол, forEach не гарантирует порядок. Как раз в стриме по спецификации не гарантирует (по факту он есть)

docs.oracle.com/javase/tutorial/collections/streams/parallelism.html#ordering
здесь сказано, что порядок всё же гарантируется. А где Вы нашли инфу, что не гарантируется?
Sign up to leave a comment.

Articles