Чи стилістично краще в Java 8 використовувати опорні вирази методів або методи, що повертають реалізацію функціонального інтерфейсу?


11

Java 8 додала концепцію функціональних інтерфейсів , а також численні нові методи, розроблені для прийняття функціональних інтерфейсів. Екземпляри цих інтерфейсів можуть бути створені лаконічно, використовуючи вирази опорних методів (наприклад SomeClass::someMethod) та лямбда-вирази (наприклад (x, y) -> x + y).

Ми з колегою маємо різні думки щодо того, коли найкраще використовувати ту чи іншу форму (де "найкраще" в цьому випадку дійсно зводиться до "найбільш читабельного" і "найбільш відповідного стандартним практикам", оскільки вони в основному інакше еквівалент). Зокрема, це стосується випадку, коли всі справжні:

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

Моя сьогоднішня думка з цього приводу полягає в тому, що кращим підходом є додавання приватного методу та посилання на це шляхом посилання на метод. Складається враження, що саме така функція була розроблена для використання, і, здається, простіше повідомляти про те, що відбувається через назви методів та підписи (наприклад, "булева isResultInFuture (результат результату)" чітко говорить про повернення булева). Це також робить приватний метод більш багаторазовим, якщо майбутні розширення класу хочуть використовувати ту саму перевірку, але не потребує обгортки функціонального інтерфейсу.

Перевагою мого колеги є використання методу, який повертає примірник інтерфейсу (наприклад, "Predicate resultInFuture ()"). Мені здається, що це не зовсім те, як функція була призначена для використання, відчуває трохи незграбність і здається, що важче реально спілкуватися про наміри через іменування.

Щоб зробити цей приклад конкретним, ось той самий код, написаний у різних стилях:

public class ResultProcessor {
  public void doSomethingImportant(List<Result> results) {
    results.filter(this::isResultInFuture).forEach({ result ->
      // Do something important with each future result line
    });
  }

  private boolean isResultInFuture(Result result) {
    someOtherService.getResultDateFromDatabase(result).after(new Date());
  }
}

vs.

public class ResultProcessor {
  public void doSomethingImportant(List<Result> results) {
    results.filter(resultInFuture()).forEach({ result ->
      // Do something important with each future result line
    });
  }

  private Predicate<Result> resultInFuture() {
    return result -> someOtherService.getResultDateFromDatabase(result).after(new Date());
  }
}

vs.

public class ResultProcessor {
  public void doSomethingImportant(List<Result> results) {
    Predicate<Result> resultInFuture = result -> someOtherService.getResultDateFromDatabase(result).after(new Date());

    results.filter(resultInFuture).forEach({ result ->
      // Do something important with each future result line
    });
  }
}

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


1
Лямбди були додані до мови чомусь, і Java не стала гіршою.

@Snowman Само собою зрозуміло. Тому я припускаю, що між вашим коментарем та моїм запитанням є певний нав'язливий зв’язок, який я не дуже сприймаю. Який сенс ти намагаєшся зробити?
М. Юстін

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

2
@RobertHarvey Звичайно. До цього моменту, однак, були додані одночасно і лямбдати, і посилання на методи, тому жодне з них не було статусом. Навіть без цього конкретного випадку бувають випадки, коли посилання на методи все-таки будуть кращими; наприклад, існуючий публічний метод (наприклад Object::toString). Тож моє питання стосується того, чи краще в цьому конкретному типі екземплярів, які я тут викладаю, ніж чи існують випадки, коли один кращий за інший, чи навпаки.
М. Джастін

Відповіді:


8

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

Predicate<Result> pred = result -> this.isResultInFuture(result);
Predicate<Result> pred = this::isResultInFuture;

Вони є оперативно еквівалентними, і перший називається точковим стилем, а другий - точковим вільним стилем. Термін "точка" відноситься до названого аргументу функції (це походить від топології), який відсутній в останньому випадку.

Більш загально, для всіх функцій F лямбда-обгортка, яка приймає всі задані аргументи і передає їх у F без змін, а потім повертає результат F незмінним, ідентична самому F. Видалення лямбда та використання F безпосередньо (переходячи від стильового стилю до стилю без точок вище) називається скороченням ета , терміном, що походить від обчислення лямбда .

Існують точкові вільні вирази, які не створюються ета-редукцією, найкласичнішим прикладом яких є композиція функції . Враховуючи функцію від типу A до типу B і функцію від типу B до типу C, ми можемо скласти їх разом у функцію від типу A до типу C:

public static Function<A, C> compose(Function<A, B> first, Function<B, C> second) {
  return (value) -> second(first(value));
}

Тепер припустимо, у нас є метод Result deriveResult(Foo foo). Оскільки предикат є функцією, ми можемо побудувати новий предикат, який спочатку викликає, deriveResultа потім перевіряє отриманий результат:

Predicate<Foo> isFoosResultInFuture =
    compose(this::deriveResult, this::isResultInFuture);

Хоча реалізація в composeвикористанні доречний стиль з лямбда, то використання в композит для визначення isFoosResultInFutureє вільна , тому що ніякі аргументи не повинні бути згадані.

Безпроблемне програмування також називається мовчазним програмуванням, і воно може бути потужним інструментом для збільшення читабельності, видаляючи безглузді (призначені для каламбура) деталі з визначення функції. Java 8 не підтримує вільне програмування настільки ж ретельно, як і більш функціональні мови, як Haskell, але гарним правилом є завжди виконувати ета-скорочення. Не потрібно використовувати лямбда, які не відрізняються від функцій, якими вони займаються.


1
Я думаю, що ми з колегою обговорюємо більше ці два завдання: Predicate<Result> pred = this::isResultInFuture;і Predicate<Result> pred = resultInFuture();де результатInFuture () повертає a Predicate<Result>. Тому, хоча це чудове пояснення, коли використовувати посилання методу проти лямбда-виразу, це не зовсім охоплює цей третій випадок.
М. Джастін

4
@ M.Justin: resultInFuture();Призначення ідентичне лямбда-призначенню, яке я представив, за винятком випадків, коли ваш resultInFuture()метод є вкладеним. Таким чином, цей підхід має два шари непрямості (жоден з яких нічого не робить) - метод конструювання лямбда і сам лямбда. Навіть гірше!
Джек

5

Жоден із нинішніх відповідей насправді не стосується основної частини питання, а саме те, чи повинен клас мати private boolean isResultInFuture(Result result)метод чи private Predicate<Result> resultInFuture()метод. Хоча вони значною мірою еквівалентні, у кожного є свої переваги та недоліки.

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

Перший підхід також є кращим, якщо ви хочете адаптувати цей метод до різних функціональних інтерфейсів або різних об'єднань інтерфейсів. Наприклад, ви можете хотіти, щоб посилання на метод було такого типу Predicate<Result> & Serializable, що другий підхід не дає вам гнучкості у досягненні.

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

Зрештою рішення суб'єктивне. Але якщо ви не збираєтесь називати методи на Predicate, я схиляюся до переваги першого підходу.


Операції вищого порядку над присудком все ще доступні, передаваючи посилання методу:((Predicate<Resutl>) this::isResultInFuture).whatever(...)
Джек

4

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

Так, люди постійно призначають лямбдаз змінним у навчальних посібниках. Це лише навчальні посібники, які допоможуть вам глибше зрозуміти, як вони працюють. Вони фактично не використовуються таким чином на практиці, за винятком порівняно рідкісних обставин (наприклад, вибору між двома лямбдахами, використовуючи вираз if).

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

people = sort(people, (a,b) -> b.age() - a.age());

Порівняйте це з читабельністю, коли ви намагаєтеся вкласти свій приклад лямбда:

results.filter(result -> someOtherService.getResultDateFromDatabase(result).after(new Date()))...

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


2
Re: багатослівність - Хоча нові функціональні доповнення самі по собі не зменшують багато багатослівностей, вони дозволяють зробити так. Наприклад, ви можете визначити: public static final <T> List<T> sort(List<T> list, Comparator<T> comparator)з очевидною реалізацією, а потім ви можете написати такий код: people = sort(people, (a,b) -> b.age() - a.age());що є великим вдосконаленням ІМО.
JimmyJames

Це чудовий приклад того, про що я говорив, @JimmyJames. Коли лямбда короткі і можуть бути накресленими, вони є великим поліпшенням. Коли вони настільки довгі, що ви хочете відокремити їх і дати ім’я, вам краще просто визначити метод. Дякуємо, що допомогли мені зрозуміти, як моя відповідь була неправильно витлумачена.
Карл Білефельдт

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