Чи використовуються лямбда-вирази, окрім збереження рядків коду?


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());

27
Зауважте, що він може бути і коротшим: (o1, o2) -> o1.getName().compareTo(o2.getName())
Thorbjørn Ravn Andersen

88
або ще коротше:Comparator.comparing(Developer::getName)
Томас Клагер

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 .


1
По суті, лямбда-вирази - це тип функціонального програмування. Зважаючи на те, що будь-яке завдання можна виконати за допомогою функціонального програмування або імперативного програмування, немає ніякої переваги, крім читабельності та ремонтопридатності , що може перевершити інші проблеми.
Draco18s більше не довіряє SE

1
@Trilarion: ви можете заповнити цілі книги визначенням функції . Вам також доведеться вирішити, чи орієнтуватися на мету або на аспекти, підтримувані лямбда-виразами Java. Остання посилання моєї відповіді ( ця ) вже обговорює деякі з цих аспектів у контексті Java. Але, щоб зрозуміти мою відповідь, вже достатньо зрозуміти, що функції - це поняття, яке відрізняється від класів. Для більш детальної інформації, вже існують ресурси та Q & As…
Holger

1
@Trilarion: і те й інше, функції не дозволяють вирішити більше проблем, якщо ви не вважаєте кількість розміру / складності коду або обхідних шляхів, необхідних іншими рішеннями, як неприйнятну (як сказано в першому абзаці), і вже був спосіб визначити функції як було сказано, внутрішні класи можуть бути використані як спосіб вирішення відсутності кращого способу визначення функцій (але внутрішні класи не є функціями, оскільки вони можуть слугувати набагато більшим цілям). Це те саме, що і з OOP - це не дозволяє робити все, що ви не могли б зі складанням, але все-таки, чи можете ви зобразити програму на зразок Eclipse, написану в збірці?
Холгер

3
@EricDuminil: Для мене повнота Тьюрінга є занадто теоретичною. Наприклад, незалежно від того, чи є Тюрінг повною чи ні, ви не можете написати драйвер пристрою Windows на Java. (Або в PostScript, який також є Turing завершеним) Додавання до Java функцій, що дозволяють це зробити, змінило б набір проблем, які ви можете вирішити з цим, однак це не відбудеться. Тому я вважаю за краще збірний пункт; оскільки все, що ви можете зробити, реалізовується у виконуваних інструкціях процесора, у Асамблеї немає нічого, чого ви не могли б зробити, що підтримує всі інструкції процесора (також простіше зрозуміти, ніж формалізм машини Тьюрінга).
Холгер

2
@abelito загальноприйнято для всіх конструкцій програмування вищого рівня, щоб забезпечити меншу «видимість того, що відбувається насправді», оскільки це все, про що йдеться, менше деталей, більше уваги на власне проблему програмування. Ви можете використовувати його, щоб зробити код кращим або гіршим; це просто інструмент. Як і функція голосування; це інструмент, який ви можете використовувати призначеним способом, щоб висловити обґрунтовану думку про фактичну якість публікації. Або ви використовуєте його, щоб спростувати розроблену, розумну відповідь, просто тому, що ви ненавидите лямбдів.
Холгер

52

Мови програмування не призначені для виконання машин.

Вони для того, щоб програмісти задумалися .

Мови - це розмова з компілятором, щоб перетворити наші думки у те, що може виконати машина. Однією з головних скарг на Яву від людей, які приходять до неї з інших мов (або залишають її для інших мов), було те, що вона змушує програміста певну ментальну модель (тобто все - це клас).

Я не збираюся зважуватись на те, добре чи погано: все є компромісом. Але лямбди Java 8 дозволяють програмістам мислити з точки зору функцій , що раніше ви не могли зробити в Java.

Це те саме, що процедурний програміст, який навчається думати через класи, коли вони приходять на Яву: ви бачите, як вони поступово переходять від класів, які прославляють структури, і мають "помічницькі" класи з купою статичних методів і переходять до чогось, що більше нагадує раціональну конструкцію ОО (mea culpa).

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


5
Будьте обережні з цим, хоч я думав, що OOP - це все, і тоді я жахливо плутався з архітектурою систем-компонентів системи (що означає, що у вас немає класу для кожного виду ігрових об'єктів ?! І кожного об’єкта гри не є об’єктом OOP ?!)
користувач253751

3
Іншими словами, мислення в OOP, а не просто мислення * o f * класів як іншого інструменту, y обмежило моє мислення погано.
користувач253751

2
@immibis: Є багато різних способів «думати в OOP», тому, крім поширеної помилки, що плутають «мислення в ООП» з «мисленням на заняттях», існує багато способів мислити протилежним результатом. На жаль, це трапляється занадто часто з самого початку, оскільки навіть книги та підручники навчають неповноцінних, показують антиприклад у прикладах і поширюють це мислення. Передбачувана потреба "мати клас для кожного виду", що завгодно, є лише одним із прикладів, що суперечить концепції абстрагування. Так само, мабуть, існує міф "OOP означає написати якомога більше котельні" міфу тощо
Holger

@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
Я думаю, це ключове розуміння. Можна стверджувати, що щось практично неможливо зробити мовою програмування, якщо синтаксичний та когнітивний наклад занадто великий, щоб бути корисним, щоб інші підходи були вищими. Тому, зменшуючи синтаксичні та когнітивні накладні витрати (як це робиться лямбдаз у порівнянні з анонімними класами), стають можливими такі трубопроводи, як описано вище. Більше язика, якщо написання фрагмента коду звільнить вас із роботи, можна сказати, що це неможливо.
Себастьян Редл

Чіплятися: Ви на насправді не потрібні Comparatorдля , sortedяк він порівнює в природному порядку в будь-якому випадку. Може, змінити його на порівняння довжини струн або подібне, щоб зробити приклад ще кращим?
tobias_k

@tobias_k немає проблем. Я змінив це, щоб compareToIgnoreCaseзмусити себе вести себе інакше, ніж sorted().
Еран

1
compareToIgnoreCaseвсе ще є поганим прикладом, оскільки ви могли просто використовувати існуючий String.CASE_INSENSITIVE_ORDERкомпаратор. Пропозиція @ tobias_k щодо порівняння за довжиною (або будь-якої іншої властивості, яка не має вбудованого компаратора) підходить краще. Судячи з прикладів коду SO & Q, лямбдаси викликали багато зайвих реалізацій компаратора…
Holger

Чи я єдиний, хто вважає, що другий приклад легше зрозуміти?
Битва Кларка

8

Внутрішня ітерація

Під час ітерації Java Collection, більшість розробників прагне отримати елемент і потім обробити його. Це - вийміть цей елемент, а потім використовуйте його або повторно вставляйте його тощо. За допомогою попередніх 8 версій Java ви можете реалізувати внутрішній клас і зробити щось на кшталт:

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 ви можете краще зробити так:

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));вираз лямбда складається з двох частин: ліворуч від символу стрілки (->) із переліком його параметрів та праворуч, що містить його тіло . Компілятор автоматично з'ясовує, що лямбда-вираз має однаковий підпис єдиного не реалізованого методу інтерфейсу споживача.
alseether

5
Я не запитував, що таке лямбда-вираз, я просто кажу, що внутрішня ітерація не має нічого спільного з лямбда-виразами.
Ousmane D.

1
@ Aominè Я бачу вашу точку зору, він не має нічого з лямбда самі по собі , але , як ви відзначаєте, це більше схоже на більш чистим способом творчість. Я думаю, що з точки зору ефективності це так само добре, як і до 8 версій
ще

4

Існує багато переваг використання лямбда замість внутрішнього класу, як описано нижче:

  • Зробіть код більш компактним та виразним, не вводячи більше семантики синтаксису мови. ви вже наводили приклад у своєму запитанні.

  • Використовуючи лямбда, ви раді програмуванню за допомогою операцій у функціональному стилі на потоках елементів, таких як перетворення на зменшення карти на колекції. див . документацію щодо пакетів 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 розвіяв будь-які сумніви щодо ефективності лямбда. Це не тільки спосіб зробити код чистішим, але якщо лямбда не захопить жодне значення, це буде клас Singleton (багаторазовий використання)
alseether

Якщо ви копаєте глибоко внизу, кожна мовна особливість має різницю. Питання на високому рівні, отже, я відповідь написав на високому рівні.
Ousmane D.

2

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

Тож якщо у вас є якась проста функція вибору, вам не потрібно ставити її в окремому місці з купою котла, просто напишіть лямбда, який є стислим і місцевим.


1

Так, багато переваг є.

  • Не потрібно визначати цілий клас, ми можемо передати реалізацію функції як самостійна.
    • Внутрішнє створення класу створить .class файл, тоді як якщо ви використовуєте лямбда, то створення класу уникає компілятор, оскільки в лямбда ви передаєте реалізацію функції замість класу.
  • Повторне використання коду вище, ніж раніше
  • І як ви сказали, код коротший, ніж звичайна реалізація.
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.