В Java 8 лучше стилистически использовать выражения ссылок на методы или методы, возвращающие реализацию функционального интерфейса?

11

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

У нас с коллегой разные мнения о том, когда лучше использовать ту или иную форму (где «лучший» в данном случае действительно сводится к «наиболее читабельным» и «наиболее в соответствии со стандартной практикой», поскольку они в основном иным образом эквивалент). В частности, это касается случая, когда все следующее верно:

  • рассматриваемая функция не используется вне одной области видимости
  • присвоение экземпляру имени помогает удобочитаемости (в отличие, например, от того, что логика достаточно проста, чтобы сразу увидеть, что происходит)
  • нет других причин программирования, почему одна форма была бы предпочтительнее другой.

Мое текущее мнение по этому вопросу состоит в том, что добавление частного метода и ссылка на него методом ссылки является лучшим подходом. Такое ощущение, что это то, как эта функция была разработана для использования, и кажется, что легче сообщить о происходящем через имена и сигнатуры методов (например, «boolean isResultInFuture (Result result)» явно говорит, что возвращает логическое значение). Это также делает приватный метод более пригодным для повторного использования, если будущее усовершенствование класса хочет использовать ту же проверку, но не нуждается в оболочке функционального интерфейса.

Мой коллега предпочитает иметь метод, который возвращает экземпляр интерфейса (например, «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());
  }
}

против

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

против

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 хуже.
@ Снеговик Это само собой разумеется. Поэтому я предполагаю, что есть некоторая подразумеваемая связь между вашим комментарием и моим вопросом, которую я не совсем уловил. Какой смысл вы пытаетесь сделать?
М. Джастин
2
Я предполагаю, что он имеет в виду, что если бы лямбды не улучшали статус-кво, они бы не стали их вводить.
Роберт Харви
2
@RobertHarvey Конечно. К этому моменту, однако, и лямбда-выражения, и ссылки на методы были добавлены одновременно, поэтому статус-кво ранее не было. Даже без этого конкретного случая бывают случаи, когда ссылки на методы будут еще лучше; например, существующий публичный метод (например Object::toString). Поэтому мой вопрос больше в том, лучше ли в данном конкретном случае, который я выкладываю, чем в том, существуют ли случаи, когда один лучше другого, или наоборот.
М. Джастин

Ответы:

8

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

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

Они функционально эквивалентны, и первый называется точечным стилем, а второй - точечным стилем. Термин «точка» относится к аргументу именованной функции (это происходит из топологии), который отсутствует в последнем случае.

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

Существуют точечные выражения, которые не создаются путем сокращения eta, наиболее классическим примером которого является составление функций . Учитывая функцию от типа 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, но хорошее правило - всегда выполнять eta-сокращение. Нет необходимости использовать лямбды, поведение которых не отличается от функций, которые они переносят.

Джек
источник
1
Я думаю, что мы с коллегой обсуждаем эти два задания: Predicate<Result> pred = this::isResultInFuture;и Predicate<Result> pred = resultInFuture();где resultInFuture () возвращает a Predicate<Result>. Таким образом, хотя это отличное объяснение того, когда следует использовать ссылку на метод, а не на лямбда-выражение, оно не вполне охватывает этот третий случай.
М. Джастин
4
@ M.Justin: resultInFuture();назначение идентично лямбда-назначению, которое я представил, за исключением того, что у меня resultInFuture()метод встроенный. Таким образом, этот подход имеет два уровня косвенности (ни один из которых ничего не делает) - метод построения лямбды и сама лямбда. Еще хуже!
Джек
5

Ни один из текущих ответов на самом деле не затрагивает суть вопроса: должен ли класс иметь private boolean isResultInFuture(Result result)метод или private Predicate<Result> resultInFuture()метод. Хотя они в значительной степени эквивалентны, у каждого есть свои преимущества и недостатки.

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

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

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

В конечном итоге решение субъективно. Но если вы не собираетесь вызывать методы в 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());что является большим улучшением IMO.
JimmyJames
Это отличный пример того, о чем я говорил, @JimmyJames. Когда лямбды короткие и могут быть встроены, это большое улучшение. Когда они становятся такими длинными, что вы хотите отделить их и дать им имя, вам лучше просто определить метод. Спасибо, что помогли мне понять, как мой ответ был неверно истолкован.
Карл Билефельдт
Я думаю, что ваш ответ был просто в порядке. Не уверен, почему за него проголосуют.
JimmyJames