Pull to refresh

Comments 16

Вроде в целом всё и верно написано. Только вот стримы и прочая функциональщина в Java ещё c восьмёрки. Её и здесь и где угодно обсосали по 10 раз, причём подробнее и систематичней.

В чём смысл данного текста? Прорекламировать свой telegram-канал?

Благодарю за критику! Вы правы — тема стримов действительно не нова, но, как говорится, «нет ничего нового под солнцем». Статья — это попытка «очеловечить» сухую теорию через личную историю и иронию, а не заменить учебники.

Что касается упоминания Telegram-канала — хотелось добавить в технический разбор немного неформальности, но, вижу, это не приветствуется. Принял к сведению, спасибо за обратную связь!

Функциональное программирование.

Stream появился в Java на 6 лет позже чем linq в c#. Но находятся ещё 🦥 которые открыли Java stream через 10 лет после его появления.

Я начинаю опасаться за адептов языка для кофемашин

Если б я начал изучать java недавно, то статья б мне зашла - понятно написана и без лишнего усложнения.. однако тут такое не любят и задизлайкают за рекламу телеги и за то, что миллиард таких статей.. все ждут нового чего-то. Поставлю лайк, чтобы не загубили твоё желание писать. Глядишь и родится шедевр, который устроит процентов 90 публики Хабра.

Stream API — это про декларативный стиль программирования. То есть вы описываете, что хотите сделать с данными (отфильтровать, преобразовать, агрегировать), а не как это делать шаг за шагом.

Это вопрос договорённостей, что считать декларативным стилем. Но, насколько я себе представляю, фильтрация, маппинг и свёртка не являются декларативным стилем. Это просто приведение произвольного цикла к определенным стандартным формам, но цикл при этом остаётся циклом. Иначе можно считать, что for(i = 1; i < 10; i++){...} это тоже декларативныё стиль. Ведь мы сказали компьютеру сделай цикл, но не описали как его делать - как разместить счётчик в регистре, где поставить метку для условного jump-а, какие операции выполнить для правильного заполнения флага переноса перед каждым условным jump-ом.

Некстати:

Но "select * from xxx where a=1" декларативный или процедурный? Я привык считать, что декларативный.

Далее, анaлогом "collection.filter(x -> x == 1)" на Smalltalk будет "collection select: [:x| x = 1]", в принципе, то же самое, за исключением нюансов (обработка исключений, из-за чего Smalltalk-код даже более "процедурный", чем Java-код).

Но в Smalltalk есть финт ("doesNotUnderstand:"), благодаря которому блок кода ([:x| x = 1]) можно расшифровать во время выполнения. Благодаря этому, в том числе, если в переменной collection будет реально содержаться query-объект (ORM-фреймворка), можно во время выполнения реконструировать "collection select: [:x| x = 1]" в "select * from xxx where a=1" и передать запрос базе данных. Или, если в collection коллекция какой-то особой структуры, то как-то оптимизировать выполнение для определённого типа блоков кодов, а не тупо гонять цикл (как оптимизатор в SQL-сервере при наличии определённых выражений во WHERE может воспользоваться статистикой).

Т.е., какую-то декларативность можно высосать.

Select в SQL несомненно декларативный. У него внутри под капотом скрыто и обращение к БД, а значит и сетевой стек, это уже немало, также и выполнение на машине с БД ряда инструкций, зачастую достаточно сложных и хитро оптимизированных, и отправка результатов выполнения запроса обратно. И о том, как именно сделать все эти действия, запрос Select ничего не говорит. Реальный код, будет выполнен разный при разных ситуациях - СУБД сама решает как лучше сделать при разной организации таблиц, полей и индексов в БД, при разной конфигурации сети и её текущем состоянии, ещё имеет значение находится ли БД на удалённом компьютере или на локальном.

А когда мы применяем filter например к связанному списку, по сути мы говорим: У меня есть список и процедура, которая принимает на вход два аргумента 1) этот самый список 2) функцию-предикат. Примени эту функцию предикат ко всем элементам списка и оставь в нём только те элементы, для которых предикат истинный. И тут даже не стоит вопрос о том, как это будет сделано. Очевидным образом - начиная с первого элемента списка и далее по одному элементу. Где же тут декларативный подход? Мы же даём однозначные инструкции, которые могут быть выполнены одним единственным образом.

Я вам в комменте, на который вы отвечаете, показал пару примеров, что не "по сути мы говорим". Правда, примеры привёл на Smalltalk'е, где мне понятно, как расшифровывать блок кода, а на Java я про рантайм-расшифровку лямбды не знаю, и если это никак не возможно, то сильно ограничивает в возможностях оптимизации. Но всё-таки Java-коллекции (объекты с "коллекционными" интерфейсами) не обязаны быть стандартными (в смысле - взятыми из JDK), да и JDK даёт нам пример - параллельную обработку, так что "начиная с первого элемента списка и далее по одному элементу" не обязательно. Коллекцию, похожую на стандартную, но принимающую "самостоятельное" решение, параллелить обработку или нет, совсем не сложно вообразить.

Вот не знаю, "настоялись" ли уже стримы к последней версии java, но вот когда они появились в восьмой версии и все стали дружно их использовать, на одном высоконагруженном сервисе, профилирование которого я делал, для меня стало открытием, что stream api весьма тормозное. Разница производительности операций в секунду, в идентичных условиях, логика на стримах против классического цикла проигрывала в 3-5 раз. Точнее уже не скажу, т.к. 10 лет точно прошло. Осадок неприятный остался. Стримы красивы и лаконичны, но это не про производительный код

Достаточно просто поставить точку на итерации, и полюбоваться на стек.

 для меня стало открытием, что stream api весьма тормозное

Перебор массива с доступом к элементам по индексу всегда быстрее, чем перебор через итераторы. Для разработчиков java это открытие?

Каким образом доступ по индексу и Stream API связаны? Вы что то путаете. Стримы - это про синтаксический сахар к итераторам, а не про доступ к элементам по индексу.

Ещё важный момент: обработка исключений в Java для for и стримов очень разная. Что делать, если внутри лямбды произошла какая-то неприятность и хочется прекратить обработку остатка потока и выбросить исключение?

Коль уж вы про стримы, то может стоит использовать IntStream.rangeClosed(1, 6).boxed() вместо Arrays.asList(1, 2, 3, 4, 5, 6).stream() ? А то вроде про деклеративный, но тут же используете императивный архаизм.

Если вы с восьмерки (это сколько лет уже прошло?) насилуете себя и пытаетесь полюбить стримы и использовать их везде, где можно использовать for, то может, вы что-то делаете не так, и стримы не так уж хороши, лаконичны, читаемы и вообще хороши? Может, стоит не только научиться их использовать, но и научиться их НЕ использовать?

Девяносто процентов кода с коллекциями - пройтись по коллекции без ее изменения и получения новой коллекции, просто выполнить ОДНУ какую-то операцию. Может, в этом случае просто использовать for?

Лучшая статья по стримам, что я находил. Сравнения filter, map и reduce с охранником, художником и барменом - божественно.

Sign up to leave a comment.

Articles