Есть ли у лямбда-выражений какое-либо применение, кроме сохранения строк кода?

120

Есть ли у лямбда-выражений какое-либо применение, кроме сохранения строк кода?

Есть ли какие-либо специальные функции, предоставляемые лямбдами, которые решают проблемы, которые было нелегко решить? Типичное использование, которое я видел, заключается в том, что вместо написания этого:

Comparator<Developer> byName = new Comparator<Developer>() {
  @Override
  public int compare(Developer o1, Developer o2) {
    return o1.getName().compareTo(o2.getName());
  }
};

Мы можем использовать лямбда-выражение, чтобы сократить код:

Comparator<Developer> byName =
(Developer o1, Developer o2) -> o1.getName().compareTo(o2.getName());
Vikash
источник
27
Обратите внимание, что он может быть еще короче: (o1, o2) -> o1.getName().compareTo(o2.getName())
Thorbjørn Ravn Andersen
88
или еще короче:Comparator.comparing(Developer::getName)
Thomas Kläger
31
Я бы не стал недооценивать это преимущество. Когда все становится намного проще, вы с большей вероятностью будете поступать «правильно». Часто возникает некоторая напряженность между написанием кода, который более удобен для сопровождения в долгосрочной перспективе, и более легким для записи в краткосрочной перспективе. Лямбда-выражения уменьшают это напряжение и тем самым помогают писать более чистый код.
yshavit
5
Лямбда-выражения - это замыкания, что еще больше увеличивает экономию места, поскольку теперь вам не нужно добавлять параметризованный конструктор, поля, код назначения и т. Д. В свой класс замены.
CodesInChaos

Ответы:

116

Лямбда-выражения не меняют набор проблем, которые вы можете решить с помощью Java в целом, но определенно упрощают решение определенных проблем, по той же причине, по которой мы больше не программируем на языке ассемблера. Удаление избыточных задач из работы программиста облегчает жизнь и позволяет делать то, чего вы даже не трогали бы в противном случае, просто за тот объем кода, который вам пришлось бы создать (вручную).

Но лямбда-выражения - это не просто сохранение строк кода. Лямбда-выражения позволяют определять функции , для которых раньше вы могли использовать анонимные внутренние классы в качестве обходного пути, поэтому вы можете заменить анонимные внутренние классы в этих случаях, но не в целом.

В частности, лямбда-выражения определяются независимо от функционального интерфейса, в который они будут преобразованы, поэтому нет унаследованных членов, к которым они могли бы получить доступ, кроме того, они не могут получить доступ к экземпляру типа, реализующего функциональный интерфейс. В лямбда-выражении thisи superимеют то же значение, что и в окружающем контексте, см. Также этот ответ . Кроме того, вы не можете создавать новые локальные переменные, скрывающие локальные переменные окружающего контекста. Для предполагаемой задачи определения функции это удаляет множество источников ошибок, но также подразумевает, что для других случаев использования могут существовать анонимные внутренние классы, которые нельзя преобразовать в лямбда-выражение, даже если реализуется функциональный интерфейс.

Кроме того, конструкция new Type() { … }гарантирует создание нового отдельного экземпляра (как newвсегда). Экземпляры анонимного внутреннего класса всегда сохраняют ссылку на свой внешний экземпляр, если они созданы вне staticконтекста. Напротив, лямбда-выражения захватывают ссылку только thisтогда, когда это необходимо, то есть если они обращаются thisили не являются staticчленами. И они создают экземпляры намеренно неуказанной идентичности, что позволяет реализации решать во время выполнения, следует ли повторно использовать существующие экземпляры (см. Также « Создает ли лямбда-выражение объект в куче каждый раз при выполнении? »).

Эти различия относятся к вашему примеру. Конструкция вашего анонимного внутреннего класса всегда будет создавать новый экземпляр, а также может захватывать ссылку на внешний экземпляр, тогда как ваше (Developer o1, Developer o2) -> o1.getName().compareTo(o2.getName())лямбда-выражение без захвата, которое в типичных реализациях будет оцениваться как одноэлементное. Кроме того, он не производит.class файл на вашем жестком диске.

Учитывая различия, касающиеся как семантики, так и производительности, лямбда-выражения могут изменить способ решения программистами определенных проблем в будущем, конечно, также из-за новых API, охватывающих идеи функционального программирования с использованием новых функций языка. См. Также лямбда-выражение Java 8 и первоклассные значения .

Holger
источник
1
По сути, лямбда-выражения - это тип функционального программирования. Учитывая, что любую задачу можно выполнить с помощью функционального программирования или императивного программирования, нет никаких преимуществ, кроме удобочитаемости и ремонтопригодности , которые могут перевесить другие проблемы.
Draco18s больше не доверяет SE
1
@Trilarion: вы можете заполнить целые книги определением функции . Вам также нужно будет решить, сосредоточиться ли на цели или на аспектах, поддерживаемых лямбда-выражениями Java. В последней ссылке моего ответа ( этой ) уже обсуждаются некоторые из этих аспектов в контексте Java. Но чтобы понять мой ответ, уже достаточно понять, что функции - это другое понятие, чем классы. Для получения более подробной информации, есть уже существующие ресурсы и вопросы и ответы…
Holger
1
@Trilarion: обе функции не позволяют решать больше проблем, если вы не считаете объем кода / сложность или обходные пути, необходимые для других решений, неприемлемыми (как сказано в первом абзаце), и уже был способ определения функций как уже говорилось, внутренние классы могут использоваться как обходной путь из-за отсутствия лучшего способа определения функций (но внутренние классы не являются функциями, поскольку они могут служить гораздо большему количеству целей). Это то же самое, что и с ООП - оно не позволяет делать то, что вы не могли бы сделать со сборкой, но все же можете ли вы создать образ приложения, подобного Eclipse, написанного на сборке?
Holger
3
@EricDuminil: для меня полнота по Тьюрингу слишком теоретическая. Например, независимо от того, является ли Java завершенной по Тьюрингу или нет, вы не можете написать драйвер устройства Windows на Java. (Или в PostScript, который также является полным по Тьюрингу). Добавление в Java функций, позволяющих это сделать, изменило бы набор проблем, которые вы можете решить с его помощью, однако этого не произойдет. Так что я предпочитаю точку сборки; поскольку все, что вы можете делать, реализовано в исполняемых инструкциях ЦП, вы не можете ничего сделать в Ассемблере, который поддерживает каждую инструкцию ЦП (также легче понять, чем формализм машины Тьюринга).
Holger
2
@abelito является общим для всех программных конструкций более высокого уровня, чтобы обеспечить меньшую «видимость того, что на самом деле происходит», как это и есть, меньше деталей, больше внимания уделяется реальной проблеме программирования. Вы можете использовать его, чтобы сделать код лучше или хуже; это просто инструмент. Нравится функция голосования; это инструмент, который вы можете использовать по назначению, чтобы дать обоснованное суждение о фактическом качестве сообщения. Или вы используете его, чтобы проголосовать за подробный разумный ответ только потому, что вы ненавидите лямбды.
Хольгер
52

Языки программирования не предназначены для исполнения машинами.

Они предназначены для размышления программистами .

Языки - это диалог с компилятором, который превращает наши мысли в нечто, что может выполнить машина. Одна из главных жалоб на Java со стороны людей, которые пришли к ней с других языков (или оставили ее для других языков), заключалась в том, что она заставляет программисту определенную ментальную модель (то есть все является классом).

Я не собираюсь взвешивать, хорошо это или плохо: все дело в компромиссах. Но лямбды Java 8 позволяют программистам мыслить в терминах функций. , чего раньше в Java делать нельзя.

Это то же самое, что и процедурный программист, который учится думать категориями классов, когда приходит к Java: вы видите, как они постепенно переходят от классов, которые являются прославленными структурами и имеют `` вспомогательные '' классы с кучей статических методов, и переходят к чему-то, что больше напоминает рациональный объектно-ориентированный дизайн (mea culpa).

Если вы просто думаете о них как о более коротком способе выражения анонимных внутренних классов, то вы, вероятно, не найдете их очень впечатляющими в той же мере, в какой процедурный программист выше, вероятно, не считал классы большим улучшением.

Джаред Смит
источник
5
Однако будьте осторожны, я раньше думал, что ООП - это все, а затем я ужасно запутался с архитектурами сущность-компонент-система (что это значит, что у вас нет класса для каждого вида игрового объекта ?! и каждого игрового объекта. это не объект ООП ?!)
user253751
3
Другими словами, мышление в ООП вместо того, чтобы просто думать о классах как о другом инструменте, плохо ограничивало мое мышление.
user253751
2
@immibis: есть много разных способов «думать в ООП», поэтому, помимо распространенной ошибки, когда «мышление в ООП» путают с «мышлением в классах», существует множество способов думать непродуктивно. К сожалению, это происходит слишком часто с самого начала, поскольку даже книги и учебные пособия учат низших, демонстрируют антипаттерны на примерах и распространяют это мышление. Предполагаемая необходимость «иметь класс для каждого вида» всего лишь один пример, противоречащий концепции абстракции; похоже, что существует миф «ООП означает написание как можно большего количества шаблонов» и т. Д.
Хольгер,
@immibis, хотя в данном случае это вам не помогло, этот анекдот действительно подтверждает точку зрения: язык и парадигма обеспечивают основу для структурирования ваших мыслей и превращения их в дизайн. В вашем случае вы натолкнулись на края каркаса, но в другом контексте это могло бы помочь вам не попасть в большой шар грязи и создать уродливый дизайн. Следовательно, я полагаю, «все - компромиссы». Сохранение последнего преимущества при избегании первого вопроса - ключевой вопрос при принятии решения о том, насколько «большим» сделать язык.
Леушенко
@immibis, поэтому важно хорошо изучить несколько парадигм : каждый говорит вам о недостатках других.
Джаред Смит
36

Сохранение строк кода можно рассматривать как новую функцию, если она позволяет вам писать значительную часть логики более коротким и ясным образом, что требует меньше времени для прочтения и понимания другими.

Без лямбда-выражений (и / или ссылок на методы) Streamконвейеры были бы гораздо менее читабельны.

Подумайте, например, как Streamвыглядел бы следующий конвейер, если бы вы заменили каждое лямбда-выражение экземпляром анонимного класса.

List<String> names =
    people.stream()
          .filter(p -> p.getAge() > 21)
          .map(p -> p.getName())
          .sorted((n1,n2) -> n1.compareToIgnoreCase(n2))
          .collect(Collectors.toList());

Это было бы:

List<String> names =
    people.stream()
          .filter(new Predicate<Person>() {
              @Override
              public boolean test(Person p) {
                  return p.getAge() > 21;
              }
          })
          .map(new Function<Person,String>() {
              @Override
              public String apply(Person p) {
                  return p.getName();
              }
          })
          .sorted(new Comparator<String>() {
              @Override
              public int compare(String n1, String n2) {
                  return n1.compareToIgnoreCase(n2);
              }
          })
          .collect(Collectors.toList());

Это намного сложнее написать, чем версию с лямбда-выражениями, и она гораздо более подвержена ошибкам. Это также труднее понять.

И это относительно короткий трубопровод.

Чтобы сделать это читаемым без лямбда-выражений и ссылок на методы, вам пришлось бы определить переменные, которые содержат различные экземпляры функционального интерфейса, используемые здесь, что разделило бы логику конвейера, что затруднило бы понимание.

Эран
источник
17
Я думаю, что это ключевая идея. Можно утверждать, что что-то практически невозможно сделать на языке программирования, если синтаксические и когнитивные накладные расходы слишком велики, чтобы быть полезными, поэтому другие подходы будут лучше. Следовательно, за счет уменьшения синтаксических и когнитивных накладных расходов (как лямбды по сравнению с анонимными классами) становятся возможными конвейеры, подобные приведенным выше. Иронично: если написание кода заставит вас уволиться с работы, можно сказать, что это невозможно.
Себастьян Редл
Нитпик: Вам на самом деле не нужно Comparatorfor, sortedпоскольку оно все равно сравнивается в естественном порядке. Может быть, изменить его на сравнение длины строк или подобное, чтобы сделать пример еще лучше?
tobias_k
@tobias_k без проблем. Я изменил его на, compareToIgnoreCaseчтобы он вел себя иначе, чем sorted().
Eran
1
compareToIgnoreCaseпо-прежнему является плохим примером, поскольку вы можете просто использовать существующий String.CASE_INSENSITIVE_ORDERкомпаратор. Предложение @tobias_k о сравнении по длине (или любому другому свойству, не имеющему встроенного компаратора) подходит лучше. Судя по примерам кода SO Q&A, лямбда-выражения вызвали множество ненужных реализаций компаратора…
Holger
Неужели я единственный, кто считает, что второй пример легче понять?
Clark Battle
8

Внутренняя итерация

При повторении коллекций Java большинство разработчиков стремятся получить элемент, а затем обработать его. То есть выньте этот элемент и затем используйте его или повторно вставьте и т. Д. В версиях Java до 8 вы можете реализовать внутренний класс и сделать что-то вроде:

numbers.forEach(new Consumer<Integer>() {
    public void accept(Integer value) {
        System.out.println(value);
    }
});

Теперь с Java 8 вы можете работать лучше и менее многословно:

numbers.forEach((Integer value) -> System.out.println(value));

или лучше

numbers.forEach(System.out::println);

Поведение как аргумент

Угадайте следующий случай:

public int sumAllEven(List<Integer> numbers) {
    int total = 0;

    for (int number : numbers) {
        if (number % 2 == 0) {
            total += number;
        }
    } 
    return total;
}

С интерфейсом 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;
}

Называя это так:

sumAll(numbers, n -> n % 2 == 0);

Источник: DZone - Почему нам нужны лямбда-выражения в Java

alseether
источник
Вероятно, первый случай - это способ сэкономить несколько строк кода, но он упрощает чтение
кода
4
Как внутренняя итерация является лямбда-функцией? На самом деле, технически лямбды не позволяют делать то, что вы не могли делать до java-8. Это просто более лаконичный способ передачи кода.
Ousmane D.
@ Aominè В этом случае numbers.forEach((Integer value) -> System.out.println(value));лямбда-выражение состоит из двух частей: одна слева от символа стрелки (->), в которой перечислены его параметры, и правая, содержащая его тело . Компилятор автоматически определяет, что лямбда-выражение имеет ту же сигнатуру, что и единственный нереализованный метод интерфейса Consumer.
alseether
5
Я не спрашивал, что такое лямбда-выражение, я просто говорю, что внутренняя итерация не имеет ничего общего с лямбда-выражениями.
Ousmane D.
1
@ Aominè Я понимаю вашу точку зрения, в нем нет ничего с лямбдами как таковыми , но, как вы указываете, это больше похоже на более чистый способ написания. Я думаю, что с точки зрения эффективности такая же хорошая, как и у версий до 8
alseether
4

Существует много преимуществ использования лямбда-выражений вместо внутреннего класса, как показано ниже:

  • Сделайте код более компактным и выразительным, не вводя дополнительную семантику синтаксиса языка. вы уже привели пример в своем вопросе.

  • Используя лямбды, вы можете программировать с операциями функционального стиля над потоками элементов, такими как преобразования map-reduce в коллекциях. см. документацию пакетов java.util.function и java.util.stream .

  • Компилятор не создает файла физических классов для лямбда-выражений. Таким образом, ваши доставленные приложения становятся меньше. Как память назначается лямбде?

  • Компилятор оптимизирует создание лямбда, если лямбда не обращается к переменным вне своей области видимости, что означает, что экземпляр лямбда создается JVM только один раз. для более подробной информации вы можете увидеть ответ @ Holger на вопрос Является ли кеширование ссылок на методы хорошей идеей в Java 8? ,

  • Lambdas может реализовывать интерфейсы с несколькими маркерами помимо функционального интерфейса, но анонимные внутренние классы не могут реализовывать больше интерфейсов, например:

    //                 v--- create the lambda locally.
    Consumer<Integer> action = (Consumer<Integer> & Serializable) it -> {/*TODO*/};
Холи-ява
источник
2

Лямбды - это просто синтаксический сахар для анонимных классов.

До лямбда-выражений для достижения того же можно было использовать анонимные классы. Каждое лямбда-выражение можно преобразовать в анонимный класс.

Если вы используете IntelliJ IDEA, он может выполнить преобразование за вас:

  • Поместите курсор в лямбду
  • Нажмите alt / option + enter

введите описание изображения здесь

уборщик
источник
3
... Я думал, @StuartMarks уже разъяснил синтаксическую сахарность (sp? Gr?) Лямбд? ( stackoverflow.com/a/15241404/3475600 , softwareengineering.stackexchange.com/a/181743 )
srborlongan
2

Чтобы ответить на ваш вопрос, на самом деле лямбды не позволяют вам делать то, что вы не могли делать до java-8, а позволяют писать более сжатый код. Преимущества этого заключаются в том, что ваш код будет более ясным и гибким.

Усман Д.
источник
Очевидно, что @Holger развеял любые сомнения в отношении эффективности лямбда. Это не только способ сделать код более чистым, но если лямбда не
фиксирует
Если копнуть глубже, у каждой языковой функции есть свои отличия. Вопрос находится на высоком уровне, поэтому я написал свой ответ на высоком уровне.
Ousmane D.
2

Одна вещь, о которой я еще не упоминал, это то, что лямбда позволяет вам определять функциональность там, где она используется. .

Поэтому, если у вас есть какая-то простая функция выбора, вам не нужно помещать ее в отдельное место с кучей шаблонов, вы просто пишете лямбду, которая является краткой и актуальной для местных условий.

Carlos
источник
1

Да, есть много преимуществ.

  • Нет необходимости определять весь класс, мы можем передать реализацию функции как ссылку.
    • Внутреннее создание класса создаст файл .class, а если вы используете лямбда, компилятор избегает создания класса, потому что в лямбде вы передаете реализацию функции вместо класса.
  • Возможность повторного использования кода выше, чем раньше
  • И, как вы сказали, код короче обычной реализации.
Сунил Канзар
источник