Вот и состоялся релиз Java 8. Кто-то по-настоящему ждал её и тестировал предрелизную версию, считая недели до марта, для кого-то смена цифры в версии JDK была лишь поводом пару раз поиграть с обновленным языком в домашней IDE без отрыва от работы (ввод языка в production всегда занимает некоторое время), кто-то просто не нуждается в новых фичах, им и возможностей «семерки» хватает с лихвой. Тем не менее, восьмую Java ждать стоило — и не просто ждать, но и внимательно присмотреться к некоторым ее нововведениям, ведь в этой версии их действительно немало, и если ознакомиться с ними поближе, то не исключено, что хорошо знакомый язык предстанет перед вами в совершенно новом свете, порадовав возможностью писать еще более красивый и лаконичный код. И если уж говорить про новые возможности Java 8, было бы странно не начать с лямбда-выражений.
Так уж получилось, что в последние годы Oracle было сложно «обвинить» в быстром или революционном развитии языка — пока конкуренты по цеху обрастали новыми фичами чуть ли не ежегодно, в Java неспешно фиксили баги и выкатывали essentials, иногда — с некоторым опозданием. Так вышло и с лямбда-выражениями. Слухи о них ходили еще до седьмой версии, но не срослось, были написаны не одни «костыли», отчасти решавшие эту проблему, потом многие обрели для себя «Джаву с функциональщиной» в виде Scala и более-менее успокоились, потом Java 8 пару раз отложили — и вот, наконец, все желающие дождались официальной поддержки лямбд.
Тем не менее, несмотря на определенный интерес к теме среди разработчиков, многие до сих пор не совсем понимают, зачем оно нужно, как это использовать, и что это вообще за функциональное программирование, которое так и норовит наступить на пятки прочим парадигмам. Поскольку каждый из этих вопросов достоин как минимум солидной статьи (скорее даже книги), в этом посте будут рассмотрены лишь некоторые аспекты применения лямбда-выражений на практике, на максимально простых и понятных примерах. Стоит сразу предупредить дорогих читателей, что в первую очередь этот пост написан для того, чтобы заинтересовать Java-разработчиков темой лямбд в новой версии языка (если они по какой-то причине про них еще не знали или не оценили их по достоинству), не раздувая его за счет подробностей и не пугая их на этом этапе заумными терминами функциональщины. Поэтому если вы уже имеете опыт работы с функциональной парадигмой, то скорее всего, в этой статье вы не найдете почти ничего нового. Всем остальным, а особенно Java-разработчикам, учившим Java еще пару версий назад в ее первозданном варианте (без функциональных «костылей» вроде lambdaj), возможно будет интересно.
Отучаемся писать много
Проще, наверное, и быть не может — создадим список чисел и выведем его на экран через простейший цикл:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
for (int number : numbers) {
System.out.println(number);
}
Каждый человек, использовавший Java, наверняка писал такие циклы — казалось бы, невозможно придумать что-то более простое и удобное. Или всё-таки возможно? Что происходит в цикле? Все мы знаем ответ — числа выводятся на экран одна за другой, пока не достигнут конец списка. Звучит вполне логично и правильно, не так ли? Но давайте посмотрим на проблему с другой стороны. Тут на ум приходит сравнение с человеком, который складывает детали от конструктора «Лего», разбросанные по полу, в одну коробку — его цикл заключается в том, что он берет одну детальку, кладет в коробку, смотрит, не осталось ли на полу других деталей (их там не одна сотня), кладет в коробку следующую деталь, снова проверяет, не остались ли еще детали, снова кладет, снова проверяет. Но что мешает взять в охапку столько деталей, сколько сможешь ухватить, и разом закинуть их в коробку?
Попробуем отойти от старых привычек и таки наконец воспользоваться лямбдами. Вот как может выглядеть тот же код в Java 8:
numbers.forEach((Integer value) -> System.out.println(value));
Как мы можем видеть, структура лямбда-выражения может быть разделена на две части: часть слева от «стрелки» (->) содержит параметры выражения, а часть справа — его «тело». Компилятор уже знает, как ему работать с этим выражением, более того — в большинстве случаев, типы в лямбда-выражениях можно не указывать в коде явным образом, делая выражение еще более лаконичным:
numbers.forEach(value -> System.out.println(value));
Но и это не предел — можно использовать оператор :: и получить еще более красивое:
numbers.forEach(System.out::println);
Не знаю, как вам, но мне после этого писать на работе циклы «по-старинке» уже совсем не хочется.
Копипаст или абстракции? Выбирать вам!
Но, конечно, вокруг функциональной парадигмы не было бы столько шума, если бы она была нужна только для вывода на экран нескольких чисел одной строчкой кода. Если хотя бы ненадолго задуматься об их применении, то окажется, что дело еще и в повышении уровня абстракции и гибкости ваших проектов. Давайте разберем еще один пример:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
Уже знакомый нам список, но теперь представим, что для проекта (своего или рабочего) требуется написать метод, который находит сумму всех элементов списка (Эх, если бы задачи действительно были такими простыми!)
public int sumAll(List<Integer> numbers) {
int total = 0;
for (int number : numbers) {
total += number;
}
return total;
}
Просто? Безусловно. Хорошо, представим, прошло некоторое время, и оказалось, что нужно написать еще один метод — пусть он, например, складывает только четные числа. Тоже задача уровня 10-го класса, не так ли?
public int sumAllEven(List<Integer> numbers) {
int total = 0;
for (int number : numbers) {
if (number % 2 == 0) {
total += number;
}
}
return total;
}
Всё прекрасно, но количество задача растет, и в один день нам понадобился еще один метод, который находит сумму всех чисел больше 3-х. Уверен, что первое, что придет в голову большинству разработчиков — это выделить предыдущий метод, воспользоваться старым добрым копипастом и поменять условие. Прекрасно, код работает, но… Самый ли это логичный подход? Представим, что новые методы придется дописывать постоянно, и завтра нам понадобится метод, считающий суммы нечетных чисел, чисел больше 2, меньше 5, и так далее. В итоге даже задачки школьного уровня вырастут в целую «простыню» кода. Неужели в 2014-м году нет более простого подхода?
Было бы странно, если бы его не было. Воспользуемся еще одной фичей Java 8 — функциональным интерфейсом Predicate, который определяет, как мы будем сортировать наши числа до того, как суммировать их.
public int sumAll(List<Integer> numbers, Predicate<Integer> p) {
int total = 0;
for (int number : numbers) {
if (p.test(number)) {
total += number;
}
}
return total;
}
В таком случае, саму реализацию всех возможных вариантов мы можем уместить всего в 3 строчки:
sumAll(numbers, n -> true);
sumAll(numbers, n -> n % 2 == 0);
sumAll(numbers, n -> n > 3);
Красота же! И это видно всего на двух простейших примерах — не углубляясь в дебри, не затрагивая lazy evaluation и прочие важные аспекты. Впрочем, целью этой статьи как раз и было заинтересовать Java-разработчиков темой лямбд в новой версии языка (если они по какой-то причине до этого момента не обращали на них внимания), не перегружая читателя на этом этапе матчастью и всей глубиной идей ФП.
Если кого-то эти практические примеры вдохновят на то, чтобы найти книги и блоги по теме, начав путь по постижению дзена функционального программирования (путь, конечно, не самый короткий, но ведь красота она такая — требует жертв), то это уже будет очень хорошо — как для вас, так и для меня, ведь получается, что не зря писал. К тому же, уверен, на этом пути вы со временем найдете и более удачные примеры, которые выглядят намного любопытнее тех, что представлены в этом посте. Спасибо за внимание!
Автор: complete_unknown