В 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
});
}
}
Есть ли официальная или полуофициальная документация или комментарии относительно того, является ли один подход более предпочтительным, более ли он соответствует намерениям разработчиков языка или более читабельным? За исключением официального источника, есть ли какие-либо четкие причины, по которым можно было бы выбрать лучший подход?
Object::toString
). Поэтому мой вопрос больше в том, лучше ли в данном конкретном случае, который я выкладываю, чем в том, существуют ли случаи, когда один лучше другого, или наоборот.Ответы:
Что касается функционального программирования, то, что вы и ваш коллега обсуждаете, - это стиль без точек , а точнее - это сокращение . Рассмотрим следующие два задания:
Они функционально эквивалентны, и первый называется точечным стилем, а второй - точечным стилем. Термин «точка» относится к аргументу именованной функции (это происходит из топологии), который отсутствует в последнем случае.
В более общем смысле для всех функций F лямбда-обертка, которая принимает все заданные аргументы и передает их F без изменений, а затем возвращает результат F без изменений, идентична самой F. Удаление лямбды и непосредственное использование F (переход от точечного стиля к стилю без точек выше) называется сокращением eta , термином, который происходит от лямбда-исчисления .
Существуют точечные выражения, которые не создаются путем сокращения eta, наиболее классическим примером которого является составление функций . Учитывая функцию от типа A до типа B и функцию от типа B до типа C, мы можем объединить их вместе в функцию от типа A до типа C:
Теперь предположим, что у нас есть метод
Result deriveResult(Foo foo)
. Поскольку Предикат является Функцией, мы можем создать новый предикат, который сначала вызывает,deriveResult
а затем проверяет полученный результат:Хотя реализация в
compose
использовании уместный стиль с лямбды, то использование в композе для определенияisFoosResultInFuture
является свободна , потому что никакие аргументы не должны быть упомянуты.Бессмысленное программирование также называется молчаливым программированием, и оно может быть мощным инструментом для повышения читабельности путем удаления бессмысленных (каламбурных) деталей из определения функции. Java 8 не поддерживает бессмысленное программирование почти так же тщательно, как в более функциональных языках, как это делает Haskell, но хорошее правило - всегда выполнять eta-сокращение. Нет необходимости использовать лямбды, поведение которых не отличается от функций, которые они переносят.
источник
Predicate<Result> pred = this::isResultInFuture;
иPredicate<Result> pred = resultInFuture();
где resultInFuture () возвращает aPredicate<Result>
. Таким образом, хотя это отличное объяснение того, когда следует использовать ссылку на метод, а не на лямбда-выражение, оно не вполне охватывает этот третий случай.resultInFuture();
назначение идентично лямбда-назначению, которое я представил, за исключением того, что у меняresultInFuture()
метод встроенный. Таким образом, этот подход имеет два уровня косвенности (ни один из которых ничего не делает) - метод построения лямбды и сама лямбда. Еще хуже!Ни один из текущих ответов на самом деле не затрагивает суть вопроса: должен ли класс иметь
private boolean isResultInFuture(Result result)
метод илиprivate Predicate<Result> resultInFuture()
метод. Хотя они в значительной степени эквивалентны, у каждого есть свои преимущества и недостатки.Первый подход более естественен, если вы вызываете метод непосредственно в дополнение к созданию ссылки на метод. Например, если вы тестируете этот метод модульно, возможно, будет проще использовать первый подход. Еще одним преимуществом первого подхода является то, что метод не должен заботиться о том, как он используется, отделив его от своего сайта вызова. Если позже вы измените класс так, чтобы вызывать метод напрямую, эта развязка окупится очень незначительным образом.
Первый подход также лучше, если вы хотите адаптировать этот метод к различным функциональным интерфейсам или различным объединениям интерфейсов. Например, вы можете захотеть, чтобы ссылка на метод
Predicate<Result> & Serializable
имела тип , а второй подход не дает вам гибкости.С другой стороны, второй подход более естествен, если вы собираетесь выполнять операции с предикатом более высокого порядка. В Predicate есть несколько методов, которые не могут быть вызваны напрямую из ссылки на метод. Если вы намерены использовать эти методы, второй подход лучше.
В конечном итоге решение субъективно. Но если вы не собираетесь вызывать методы в Predicate, я бы предпочел предпочесть первый подход.
источник
((Predicate<Resutl>) this::isResultInFuture).whatever(...)
Я больше не пишу код Java, но я пишу на функциональном языке для жизни, а также поддерживаю другие команды, которые изучают функциональное программирование. Лямбды имеют тонкое сладкое пятно читаемости. Общепринятый для них стиль - вы используете их, когда можете встроить их, но если вы чувствуете необходимость присвоить его переменной для последующего использования, вам, вероятно, следует просто определить метод.
Да, люди постоянно присваивают лямбда-переменные в учебниках. Это всего лишь учебники, которые помогут вам полностью понять, как они работают. На самом деле они не используются таким образом на практике, за исключением относительно редких обстоятельств (например, выбор между двумя лямбдами с использованием выражения if).
Это означает, что если ваша лямбда достаточно короткая, чтобы ее можно было легко прочитать после вставки, как в примере из комментария @JimmyJames, то сделайте это:
Сравните это с удобочитаемостью, когда вы попытаетесь встроить лямбду вашего примера:
Для вашего конкретного примера я бы лично пометил его в запросе на извлечение и попросил бы вытащить его в именованный метод из-за его длины. Java является досадно многословным языком, поэтому, возможно, со временем его собственные специфичные для языка практики будут отличаться от других языков, но до тех пор ваши лямбды будут короткими и встроенными.
источник
public static final <T> List<T> sort(List<T> list, Comparator<T> comparator)
с очевидной реализацией, а затем вы можете написать код следующим образом:people = sort(people, (a,b) -> b.age() - a.age());
что является большим улучшением IMO.