Я давно и прочно ничего хорошего от 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:
Вторая чуть похитрее, но не шибко:
Здесь стоит отметить следующее:
Чувствуете этот запах? Вот-вот ...
Не, ну можно конечно, 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), но все равно иногда хочется, а то код выглядит несколько странно:
Но хорош ныть :). Мы перешли на Java 8 еще в начале этого года, но по
А тут мне понадобилось странного. Настолько странного, что делать это нужно именно в Java и считать много всякого и не особо приятного (речь идет о группировках, суммах, средних значениях и прочих агрегатах, которые нужно считать по набору значений). Все это желательно делать быстро (в несколько потоков и пр.). Делать быстро я умел, а вот считать агрегаты на Java это скажем так ... "не фонтан". Причем для меня это насколько "не фонтан", что потом хочется пойти руки вымыть. О чем я? Ну, представьте, что у вас есть List<Map<String, Object>> где в Object валяется что попало от Integer до BigDecimal, но может и null придти и вот по ключу это все нужно как-то считать.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
BigDecimal total = BigDecimal.ZERO; | |
for (Map<String, Object> row : pList){ | |
BigDecimal value = (BigDecimal) row.get(COLUMN_AMOUNT); | |
if (value != null){ | |
total = total.add(value); | |
} | |
} |
Ну, да мы это все можем обернуть в какие-то методы (я про cast'ы и проверки на null) , но как в том анекдоте - "осадочек" остается.
Мне понадобилось считать не просто суммы, а с разбивкой по каким-то категориям. В примере она одна, но у меня их было обычно 2-4, что еще добавляет "вкуса" в этот код:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Map<String, BigDecimal> total = HashMap<>(); | |
for (Map<String,Object> row : pList){ | |
String key = (String) row.get(COLUMN_KEY); | |
BigDecimal value = (BigDecimal) row.get(COLUMN_AMOUNT); | |
if (value != null){ | |
if (total.get(key) == null){ | |
total.put(key, BigDecimal.ZERO); | |
} | |
total.put(key, total.get(key).add(value)); | |
} | |
} |
И тут нам на помощь приходят Stream'ы из Java 8. Я имел небольшой опыт решения подобных задач на Scala и мне показалось, что тот же подход можно использовать и здесь - разбивку по группам с обработкой каждой группы индивидуально (для тех кто знаком с SQL - по сути Collectors.groupingBy() - это и есть Ваши любимые SQL'ные GROUP BY).
Что из этого получилось?
Первая задача - oneliner:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
BigDecimal total = pList.stream() | |
.map(x -> (BigDecimal)x.get(COLUMN_AMOUNT)) | |
.reduce(BigDecimal.ZERO, (x, y) -> x.add(y)); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Map<String, BigDecimal> total = new HashMap<>(); | |
Collection<List<Map<String, Object>>> groups = pList.stream().collect( | |
Collectors.groupingBy((Map<String, Object> pData) | |
-> Lists.newArrayList(pData.get(COLUMN_KEY)))).values(); | |
for(List<Map<String, Object>> item : groups){ | |
total.put(item.get(COLUMN_KEY), item.stream() | |
.map(x -> (BigDecimal)x.get(COLUMN_AMOUNT)) | |
.reduce(BigDecimal.ZERO, (x, y) -> x.add(y))); | |
} |
- Collectors.groupingBy() принимает на вход List<T>. В моем случае ArrayList<Object> - его, конечно, можно создавать "руками", но лучше всего это делает Guava (точнее я не знаю как "красиво" это сделать на Java 8 API). Так, что не обессудьте Lists.newArrayList() - это именно Guava. Коллеги, меня конечно "заплюют" за этот код и скажут, что
кошерноправильно только ImmutableList.of() но в данном случае это не принципиально, а первый вызов мне кажется более очевидным. - Почему-то в Collectors.groupingBy() не работает type inference. По-этому пришлось написать явный cast (я про (Map<String, Object> pData)).
- Collectors.groupingBy() возвращает Map<ArrayList<Object>, List<Map<String, Object>>>(естественно, это не generic, а мой конкретный случай). Обратите внимание на ключ ArrayList<Object>. Это не проблема, так как List реализует и equals(), и hashCode().
- В данной задаче меня совсем не интересуют ключи по которым делается разбивка. Мне их гораздо проще потом будет брать из данных, поэтому я сразу получаю Collection<List<Map<String, Object>>>.
- Императивная обработка полученных в предыдущем пункте данных не противоречит моим религиозным убеждениям, а наоборот кажется очевидной и "нативной" что ли (я про for(List<Map<String, Object>> item : groups) если кто-то еще не понял).
- Обратите внимание, что в этом месте задача свелась к предыдущей :)
Еще 2 момента, которые я использовал из Java 8 Stream API (или около). Я выше писал об итераторах. У меня периодически возникает задача, когда нужно пройтись по List'у и по каким-то условиям удалить элементы (причем важно именно не вернуть новый List, а удалить значения в старом). До Java 8, насколько я знаю, проще всего это делалось так:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public void postProcess(List<Map<String, Object>> pList) { | |
Iterator<Map<String, Object>> iter = pList.iterator(); | |
while (iter.hasNext()){ | |
Map<String, Object> val = iter.next(); | |
if ((Boolean)val.get(COLUMN_KEY)) iter.remove(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public void postProcess(List<Map<String, Object>> pList) { | |
pList.removeIf(x -> (Boolean)x.get(COLUMN_KEY)); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
private Predicate<Map<String, Object>> getPredicate(String pId) { | |
return x-> (Boolean)x.get(pId); | |
} | |
item.stream().filter(getPredicate(COLUMN_END) | |
.and(getPredicate(COLUMN_START).negate())); |
И напоследок - настолько сильных чувств как при работе с Java 8 Stream'ами (и не только) я не испытывал, пожалуй, с средины 90х когда портировал какую-то систему на dBase'e на тот диалект SQL'a с двухуровневого подхода (это когда вы "ручками" в цикле проходите по всем записям и что-то считаете и куда-то складываете). Очень похожие впечатления.
С уверенностью могу сказать, что Java 8 -
P.S. Ложка дегтя по Stream'ам пока единственный "косяк", который увидел - нельзя получить последний элемент stream'a. Я понимаю, что Stream может быть бесконечным (я про нечто сродни thunk), но все равно иногда хочется, а то код выглядит несколько странно:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
IntStream.range(1 , 10).reduce(0, (x,y) -> y); // Вернет 9 :) |
Комментариев нет:
Отправить комментарий