26 августа 2015 г.

Java 8 - это просто праздник какой-то!

Я давно и прочно ничего хорошего от Sun Microsystems Oracle не жду. Я помню странный выход Java 6 (когда ничего особого в язык не добавили), зарезанную Java 7 (когда столько всего наобещали и практически ничего не сделали). Здесь нужно сделать отступление - Java она (или он, как кому нравится) едина в двух лицах. Это платформа (AKA JVM) которая развивается семимильными шагами и в которой столько всего нового, вкусного и классного появилось, что и говорить об этом не стоит (по крайней мере в рамках этой статьи). Но Java это и язык программирования. А вот он, скажем так, со времен Java 5 особо не менялся. И это за 8 (ВОСЕМЬ) лет, если считать с выхода Java 6 (а если с Java 5 то и за все 11).
Но хорош ныть :). Мы перешли на Java 8 еще в начале этого года, но по религиозным принципам некоторым причинам я ее особо руками не трогал. Ну и в принципе зачем!? Хотите функциональщины - есть Scala, хотите скобочек Lisp'a - есть Clojure (да-да мы все это используем).
А тут мне понадобилось странного. Настолько странного, что делать это нужно именно в Java и считать много всякого и не особо приятного (речь идет о группировках, суммах, средних значениях и прочих агрегатах, которые нужно считать по набору значений). Все это желательно делать быстро (в несколько потоков и пр.). Делать быстро я умел, а вот считать агрегаты на Java это скажем так ... "не фонтан". Причем для меня это насколько "не фонтан", что потом хочется пойти руки вымыть. О чем я? Ну, представьте, что у вас есть List<Map<String, Object>> где в Object валяется что попало от Integer до BigDecimal, но может и null придти и вот по ключу это все нужно как-то считать. Узнаете?
Ну, да мы это все можем обернуть в какие-то методы (я про cast'ы и проверки на null) , но как в том анекдоте - "осадочек" остается.
Мне понадобилось считать не просто суммы, а с разбивкой по каким-то категориям. В примере она одна, но у меня их было обычно 2-4, что еще добавляет "вкуса" в этот код: А теперь представьте, что мне нужно примерно 30 таких итоговых значений (к счастью только сумм и количеств) по пяти наборам категорий разбивки, у каждого набора значений своя фильтрация и пр.
И тут нам на помощь приходят Stream'ы из Java 8. Я имел небольшой опыт решения подобных задач на Scala и мне показалось, что тот же подход можно использовать и здесь - разбивку по группам с обработкой каждой группы индивидуально (для тех кто знаком с SQL - по сути Collectors.groupingBy() - это и есть Ваши любимые SQL'ные GROUP BY).
Что из этого получилось?
Первая задача - oneliner: Вторая чуть похитрее, но не шибко: Здесь стоит отметить следующее:
  1. Collectors.groupingBy() принимает на вход List<T>. В моем случае ArrayList<Object> - его, конечно, можно создавать "руками", но лучше всего это делает Guava (точнее я не знаю как "красиво" это сделать на Java 8 API). Так, что не обессудьте Lists.newArrayList() - это именно Guava. Коллеги, меня конечно "заплюют" за этот код и скажут, что кошерно правильно только ImmutableList.of() но в данном случае это не принципиально, а первый вызов мне кажется более очевидным.
  2. Почему-то в Collectors.groupingBy() не работает type inference. По-этому пришлось написать явный cast (я про (Map<String, Object> pData)).
  3. Collectors.groupingBy() возвращает Map<ArrayList<Object>, List<Map<String, Object>>>(естественно, это не generic, а мой конкретный случай). Обратите внимание на ключ ArrayList<Object>. Это не проблема, так как List реализует и equals(), и hashCode().
  4. В данной задаче меня совсем не интересуют ключи по которым делается разбивка. Мне их гораздо проще потом будет брать из данных, поэтому я сразу получаю Collection<List<Map<String, Object>>>.
  5. Императивная обработка полученных в предыдущем пункте данных не противоречит моим религиозным убеждениям, а наоборот кажется очевидной и "нативной" что ли (я про for(List<Map<String, Object>> item : groups) если кто-то еще не понял).
  6. Обратите внимание, что в этом месте задача свелась к предыдущей :)
Итого вся моя обработка заняла менее 300 строк кода (из них собственно на расчет итогов - менее 100).  Код не вызывает "рвотных рефлексов" и очень даже maintainable :)

Еще 2 момента, которые я использовал из Java 8 Stream API (или около). Я выше писал об итераторах. У меня периодически возникает задача, когда нужно пройтись по List'у и по каким-то условиям удалить элементы (причем важно именно не вернуть новый List, а удалить значения в старом). До Java 8, насколько я знаю, проще всего это делалось так: Чувствуете этот запах? Вот-вот ... Не, ну можно конечно, for'м пройтись, отложить значения в другой List, а потом вызвать removeAll() но это еще хуже IMO. И вот спустя ... я не знаю когда итераторы в Java появились. Подозреваю, что были всегда, но может там в 1.1 они появились. Пусть меня знатоки поправят. В общем наконец-то это делается oneliner'ом: Причем, removeIf() на вход получает Predicate<T>. Предикат, это особая разновидность функционально интерфейса, которая получает на вход T, а возвращает boolean test(T t). Но кроме этого, предикаты поддерживают и операции декомпозиции (я про and() и or()) и операцию отрицания - negate(). Мне это понадобилось, например, для фильтрации данных за период, когда на начало период, чего-то еще не было, а на конец оно же уже есть: Этот подход может понадобиться, когда сам по себе предикат не тривиален или не хочется копировать его логику несколько раз. Также хотел бы обратить Ваше внимание на type inference в getPredicate() - крррррасота!
И напоследок - настолько сильных чувств как при работе с Java 8 Stream'ами (и не только) я не испытывал, пожалуй, с средины 90х когда портировал какую-то систему на dBase'e на тот диалект SQL'a с двухуровневого подхода (это когда вы "ручками" в цикле проходите по всем записям и что-то считаете и куда-то складываете). Очень похожие впечатления.
С уверенностью могу сказать, что Java 8 - it's a really big deal пожалуй наибольшие изменения в языке которые я вижу. Команда Oracle - вы молодцы! (никогда не думал что это напишу :)
P.S. Ложка дегтя по Stream'ам пока единственный "косяк", который увидел - нельзя получить последний элемент stream'a. Я понимаю, что Stream может быть бесконечным (я про нечто сродни thunk), но все равно иногда хочется, а то код выглядит несколько странно:



Комментариев нет:

Отправить комментарий