Лямбда-выражения в Java 8 — Простые примеры того, как они могут облегчить вашу жизнь



Вот и состоялся релиз 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-разработчиков темой лямбд в новой версии языка (если они по какой-то причине до этого момента не обращали на них внимания), не перегружая читателя на этом этапе матчастью и всей глубиной идей ФП.
Если кого-то эти практические примеры вдохновят на то, чтобы найти книги и блоги по теме, начав путь по постижению дзена функционального программирования (путь, конечно, не самый короткий, но ведь красота она такая — требует жертв), то это уже будет очень хорошо — как для вас, так и для меня, ведь получается, что не зря писал. К тому же, уверен, на этом пути вы со временем найдете и более удачные примеры, которые выглядят намного любопытнее тех, что представлены в этом посте. Спасибо за внимание!

Upd: в комментариях посоветовали несколько интересных ссылок по теме.
winterbe.com/posts/2014/03/16/java-8-tutorial — Более подробный туториал по нововведениям Java 8.
java.dzone.com/articles/why-we-need-lambda-expressions — Примеры применения лямбд в Java в нескольких частях.
docs.oracle.com/javase/tutorial/collections/streams/reduction.html — Применение reduce (как альтернативы для последнего примера из поста).
Поделиться публикацией

Комментарии 33

    +11
    Как написал Erik Meijer у себя в твиттере:

    Congrats to all my friends at #oracle. Functional programming is now officially mainstream,and my mission complete.


      +7
      Не прошло и 50 лет с момента появления лиспа… :)
      • НЛО прилетело и опубликовало эту надпись здесь
          0
          Подождем еще 50 лет пока они запилят там лисповые макросы?
      +2
      Вот еще немного добра winterbe.com/posts/2014/03/16/java-8-tutorial/
        –9
        Очередной синтаксический сахар и новая парадигма написания одного и того же в языке. В очередной раз получится, что лямбдами к месту пользуется дай-бог 5% кодеров. Еще 5% будут сувать их во все места. Для остальных 90% код станет менее читабельным.
        Такое впечатление, что ораклу надо выпускать новые версии джавы и надо что-то новое впихнуть. Получится как с javascript, который позволяет писать в куче разных методик, а на деле получается, что читать чужой код практически невозможно, так как нет стандарта.
          +8
          Лямбды открывают безмерные глубины для всякого рода GUI и прочих систем построенных на событиях. В Java катастрофически не хватает указателя на функцию или его замены. Как реализовывать бынальные events? Через три вагона интерфейсов и фабрик? Посмотрите во что превратились Swing/Fx? Даже C++ нервно курит в сторонке с его нагромождениями шаблонов. Теперь легко и непринуждённо это упросить и привести в человеческий вид. (А если ещё и поддержку динамических вызовов учесть, то вообще праздник жизни получается) Правда, сдаётся мне, что на практике ещё долго java будет развиваться по инерции.
            –4
            Зачем делать еще один убер-пупер универсальный язык со всеми возможными конструкциями? Хотите писать функционально и исполнять на JVM? Пишите на Scala или еще каком-нибудь языке компилирующимся под JVM. Весь такой синтаксический сахар, который «упрощает» код одновременно сильно поднимает издержки на изучение и поддержку. Хорошо, скажете, это типа хорошо, что порог вхождения в джава программирование будет выше. С одной стороны, да, а с другой стороны таким образом джава будет терять популярность, именно из-за сложности поддержки разнообразных парадигм написания кода.
              –1
              Потому что не все могут легко поменять язык.
              Для среднего масштаба конторы на пару сотен разработчиков это довольно массивная инвестиция с довольно сомнительными преимуществами.
              0
              Лямбды открывают безмерные глубины для всякого рода GUI и прочих систем построенных на событиях.
              Но все же, это только мне лямбды кажутся все еще незавершенными? Ну посудите сами, если мы ориентируемся использовать их для систем построенных на событиях то давайте же взглянем на такие системы, ну к примеру на GWT. Там все кишит прям callback'ами, а что такое callback — это комбинация onSuccess + onFail. Итого имеем 2 метода, а если я не ошибаюсь, то таких лямбд java еще не умеет.

              Кратенькую информацию все же удалось получить от нашего соратника — TheShade. Но на сколько я понял данная концепция дальше обсуждения на Java 8 EG не зашла.
              • НЛО прилетело и опубликовало эту надпись здесь
                  –1
                  Но согласитесь что от модели Горыныча мы уже отказаться не сможем и поэтому поддержка таких конструкций была бы только полюсом для java.
                  • НЛО прилетело и опубликовало эту надпись здесь
                      –1
                      А зачем пример? Не знаю как в других языках, но в питоне новые конструкции языка появляются постоянно и происходит это потому что Гвидо активно прислушывается к комьюнити — захотели дектораторы -> договорились о синтаксисе -> получили дектораторы.

                      Вот MS другая ситуация, C# тоже стреляет такими же новинками, но это пальцем в небо, кому-то нужно, а кому-то нет, ведь выдумано это внутри компании. А тут сообщество есть, собственно те кто будет использовать конечный продукт. Ну и я считаю прислушиваться нужно.

                      К чему я веду — «даешь JSR к рассмотрению от народа!»

                      P.S. За опечатку сори.
                      • НЛО прилетело и опубликовало эту надпись здесь
                          –1
                          Решить — не факт, предложить — почему нет. Но если мое предложение к усовершенствованию лямбд даже на Java 8 EG обсуждалось то видимо есть уже какие-то попытки формализировать. Поделитесь?
              +1
              синтаксический сахар и новая парадигма написания одного и того же в языке

              Вы уж определитесь.

              Я вам гарантирую — лямбдами будут пользоваться очень активно. Это не fork/join, который нужен очень не многим.
                +2
                Лямбды существуют во многих языках уже давно. И в том же C# ими пользуются все программисты, кого я видел. Это невероятно удобно.
                  +1
                  Поддерживаю. И в пхп тоже. Поначалу — да, смущает. Но потом привыкаешь и используешь только лямды. Потому, что это просто — удобно и в большинстве случаев читается легче.
                +5
                Перевёрнутый зуб-алкаш смотрит на карту, на которой указан путь до Half-Life?
                  +1
                  Скажите, а в Java добавили var-подобный синтаксис?

                  var o = new Object();
                  
                    0
                    Нет
                    0
                    Idea, кстати, давно уже анонимные классы сворачивает в lambda-выражения — с одной стороны hint как это должно выглядеть, с другой, как я понял, готовый инструмент преобразования по всему проекту.
                      0
                      Я надеюсь, Вы не думаете, что lambda = анонимные классы с одним методом?
                        0
                        Я раньше об этом не думал, но сейчас я уже знаю, что это не так :)
                      0
                      По поводу суммы чисел — а аналога «reduce» или «Aggregate» в Java не появилось? В том-же шарпе сумму чисел, помимо существующих extension method-ов, можно например так (например мы считаем сумму чисел больших чем 3):

                      var sum = container.Where( a => a>3).Aggregate(0, (a, b) => a + b);

                      Соответственно, если мы например хотим посчитать произведение тех-же чисел, то модификация кода минимальная:

                      var mult = container.Where( a => a>3).Aggregate(1, (a, b) => a * b);

                      как видно — довольно мощная конструкция, позволяющая унифицировать подобные обхождения списков / контейнеров / массивов.
                      0
                      Примеры, да и структура статьи один в один скопированы с dzone (http://java.dzone.com/articles/why-we-need-lambda-expressions).
                      Могли бы что-нибудь свое придумать.
                        0
                        >Могли бы что-нибудь свое придумать.
                        Собственно этим я сейчас и занят. Но дело в чём — когда фичи языка используются в реальных задачах, там и придумывать ничего не надо — достаточно описать задачи и их реализацию. А вот с простыми примерами сложнее, потому что они должны быть в определенной степени абстрактными, но при этом показывать весь потенциал языка. Поэтому примеры с дизоуна показались неплохим вариантом в плане соотношения порога вхождения к информативности. И таки да, лучше с точки зрения этого соотношения придумать не получилось. Да и нужно ли?
                        Но за ссылку спасибо — добавлю в пост. Там, кстати, есть и довольно интересное продолжение.
                        +1
                        Мне лично не хватает синтаксического сахара для лямбды без аргументов. Простой пример — ловля checked exceptions:

                            interface XBlock<T> {
                                T run() throws Exception;
                            }
                            
                            static <T> T unchecked(XBlock<T> block) {
                                try {
                                    return block.run();
                                } catch (Exception e) {
                                    throw new RuntimeException(e);
                                }
                            }
                        


                        эту конструкцию приходится вызывать так: unchecked(() -> {...}), это очень некрасиво. Куда логичней было бы unchecked({...}). По-моему это ничего не сломало бы в плане парсинга и в некоторых местах выглядело бы эстетичней.

                        Ещё хочется, чтобы java.lang.Void был больше похож на void. Например если функция возвращает java.lang.Void, то в ней не нужно было бы писать return null;

                        А так, конечно, жава 8 это прорыв. Таких масштабных изменений по-моему в Java вообще не было никогда. Даже generics меркнут.
                          0
                          Добавлю и от себя ссылку про лямбры/дефендеры/стримы zeroturnaround.com/rebellabs/java-8-revealed-lambdas-default-methods-and-bulk-data-operations/

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                          Самое читаемое