1. Обзор
Теперь, когда Java 8 получила широкое распространение, начали появляться шаблоны и лучшие практики для некоторых из ее основных функций. В этом уроке мы подробнее рассмотрим функциональные интерфейсы и лямбда-выражения.
Дальнейшее чтение:
Почему локальные переменные, используемые в Лямбдах, должны быть окончательными или Фактически окончательными?
Java 8 – Мощное сравнение с лямбдами
2. Предпочитайте Стандартные Функциональные Интерфейсы
Функциональные интерфейсы, собранные в пакете java.util.function , удовлетворяют потребности большинства разработчиков в предоставлении целевых типов для лямбда-выражений и ссылок на методы. Каждый из этих интерфейсов является общим и абстрактным, что позволяет легко адаптировать их практически к любому лямбда-выражению. Разработчики должны изучить этот пакет, прежде чем создавать новые функциональные интерфейсы.
Рассмотрим интерфейс Foo :
@FunctionalInterface public interface Foo { String method(String string); }
и метод add() в некотором классе Use Foo , который принимает этот интерфейс в качестве параметра:
public String add(String string, Foo foo) { return foo.method(string); }
Чтобы выполнить его, вы должны написать:
Foo foo = parameter -> parameter + " from lambda"; String result = useFoo.add("Message ", foo);
Посмотрите внимательнее, и вы увидите, что Foo – это не более чем функция, которая принимает один аргумент и выдает результат. Java 8 уже предоставляет такой интерфейс в Function из пакета java.util.function .
Теперь мы можем полностью удалить интерфейс Foo и изменить наш код на:
public String add(String string, Functionfn) { return fn.apply(string); }
Чтобы выполнить это, мы можем написать:
Functionfn = parameter -> parameter + " from lambda"; String result = useFoo.add("Message ", fn);
3. Используйте аннотацию @Functional Interface
Аннотируйте свои функциональные интерфейсы с помощью @Functional interface . Поначалу эта аннотация кажется бесполезной. Даже без него ваш интерфейс будет рассматриваться как функциональный, если у него есть только один абстрактный метод.
Но представьте себе большой проект с несколькими интерфейсами – трудно управлять всем вручную. Интерфейс, который был разработан, чтобы быть функциональным, может быть случайно изменен путем добавления других абстрактных методов/методов, что делает его непригодным для использования в качестве функционального интерфейса.
Но, используя аннотацию @Functional Interface , компилятор вызовет ошибку в ответ на любую попытку нарушить предопределенную структуру функционального интерфейса. Это также очень удобный инструмент для облегчения понимания архитектуры вашего приложения другими разработчиками.
Итак, используйте это:
@FunctionalInterface public interface Foo { String method(); }
вместо того, чтобы просто:
public interface Foo { String method(); }
4. Не злоупотребляйте методами по умолчанию в Функциональных интерфейсах
Мы можем легко добавить методы по умолчанию в функциональный интерфейс. Это приемлемо для контракта функционального интерфейса до тех пор, пока существует только одно объявление абстрактного метода:
@FunctionalInterface public interface Foo { String method(String string); default void defaultMethod() {} }
Функциональные интерфейсы могут быть расширены другими функциональными интерфейсами, если их абстрактные методы имеют одинаковую сигнатуру.
Например:
@FunctionalInterface public interface FooExtended extends Baz, Bar {} @FunctionalInterface public interface Baz { String method(String string); default String defaultBaz() {} } @FunctionalInterface public interface Bar { String method(String string); default String defaultBar() {} }
Как и в случае с обычными интерфейсами, расширение различных функциональных интерфейсов одним и тем же методом по умолчанию может быть проблематичным .
Например, давайте добавим метод default Common() в интерфейсы Bar и Baz :
@FunctionalInterface public interface Baz { String method(String string); default String defaultBaz() {} default String defaultCommon(){} } @FunctionalInterface public interface Bar { String method(String string); default String defaultBar() {} default String defaultCommon() {} }
В этом случае мы получим ошибку во время компиляции:
interface FooExtended inherits unrelated defaults for defaultCommon() from types Baz and Bar...
Чтобы исправить это, метод default Common() должен быть переопределен в интерфейсе Food Extended . Мы, конечно, можем предоставить пользовательскую реализацию этого метода. Однако мы также можем повторно использовать реализацию из родительского интерфейса :
@FunctionalInterface public interface FooExtended extends Baz, Bar { @Override default String defaultCommon() { return Bar.super.defaultCommon(); } }
Но мы должны быть осторожны. Добавление слишком большого количества методов по умолчанию в интерфейс – не очень хорошее архитектурное решение. Это следует рассматривать как компромисс, который следует использовать только в случае необходимости для обновления существующих интерфейсов без нарушения обратной совместимости.
5. Создание Экземпляров Функциональных Интерфейсов С Помощью Лямбда-Выражений
Компилятор позволит вам использовать внутренний класс для создания экземпляра функционального интерфейса. Однако это может привести к очень подробному коду. Вы должны предпочесть лямбда-выражения:
Foo foo = parameter -> parameter + " from Foo";
над внутренним классом:
Foo fooByIC = new Foo() { @Override public String method(String string) { return string + " from Foo"; } };
Подход лямбда-выражения может быть использован для любого подходящего интерфейса из старых библиотек. Он может использоваться для таких интерфейсов, как Runnable , Comparator и так далее. Однако это не означает, что вы должны пересмотреть всю свою старую кодовую базу и все изменить.
6. Избегайте Перегрузки Методов С Функциональными Интерфейсами в качестве Параметров
Используйте методы с разными именами, чтобы избежать коллизий; давайте рассмотрим пример:
public interface Processor { String process(Callablec) throws Exception; String process(Supplier s); } public class ProcessorImpl implements Processor { @Override public String process(Callable c) throws Exception { // implementation details } @Override public String process(Supplier s) { // implementation details } }
На первый взгляд это кажется разумным. Но любая попытка выполнить любой из методов ProcessorImpl :
String result = processor.process(() -> "abc");
заканчивается ошибкой со следующим сообщением:
reference to process is ambiguous both method process(java.util.concurrent.Callable) in com.baeldung.java8.lambda.tips.ProcessorImpl and method process(java.util.function.Supplier ) in com.baeldung.java8.lambda.tips.ProcessorImpl match
Чтобы решить эту проблему, у нас есть два варианта. Первый-использовать методы с разными именами:
String processWithCallable(Callablec) throws Exception; String processWithSupplier(Supplier s);
Второй – выполнить кастинг вручную. Это не является предпочтительным.
String result = processor.process((Supplier) () -> "abc");
7. Не рассматривайте Лямбда-выражения как внутренние классы
Несмотря на наш предыдущий пример, где мы по существу заменили внутренний класс лямбда-выражением, эти два понятия отличаются важным образом: область действия.
Когда вы используете внутренний класс, он создает новую область. Вы можете скрыть локальные переменные из заключающей области, создав новые локальные переменные с теми же именами. Вы также можете использовать ключевое слово this внутри вашего внутреннего класса в качестве ссылки на его экземпляр.
Однако лямбда-выражения работают с заключающей областью. Вы не можете скрыть переменные из заключающей области внутри тела лямбды. В этом случае ключевое слово this является ссылкой на заключающий экземпляр.
Например, в классе Use Foo у вас есть переменная экземпляра value:
private String value = "Enclosing scope value";
Затем в каком-либо методе этого класса поместите следующий код и выполните этот метод.
public String scopeExperiment() { Foo fooIC = new Foo() { String value = "Inner class value"; @Override public String method(String string) { return this.value; } }; String resultIC = fooIC.method(""); Foo fooLambda = parameter -> { String value = "Lambda value"; return this.value; }; String resultLambda = fooLambda.method(""); return "Results: resultIC = " + resultIC + ", resultLambda = " + resultLambda; }
Если вы выполните метод scope Experiment () , вы получите следующий результат: Результаты: значение класса, значение области
Как вы можете видеть, вызывая this.value в IC, вы можете получить доступ к локальной переменной из ее экземпляра. Но в случае лямбды вызов this.value дает вам доступ к переменной value , которая определена в классе Use Foo , но не к переменной value , определенной внутри тела лямбды.
8. Держите Лямбда-выражения короткими и понятными
Если возможно, используйте конструкции из одной строки вместо большого блока кода. Помните, что лямбды должны быть выражением , а не повествованием. Несмотря на свой краткий синтаксис, лямбды должны точно выражать функциональность, которую они предоставляют.
Это в основном стилистический совет, так как производительность не изменится кардинально. В целом, однако, гораздо легче понять и работать с таким кодом.
Этого можно достичь многими способами – давайте посмотрим поближе.
8.1. Избегайте блоков кода в теле Лямбды
В идеальной ситуации лямбды должны быть написаны в одной строке кода. При таком подходе лямбда-это самоочевидная конструкция, которая объявляет, какое действие должно быть выполнено с какими данными (в случае лямбд с параметрами).
Если у вас большой блок кода, функциональность лямбды не сразу понятна.
Имея это в виду, выполните следующие действия:
Foo foo = parameter -> buildString(parameter);
private String buildString(String parameter) { String result = "Something " + parameter; //many lines of code return result; }
вместо:
Foo foo = parameter -> { String result = "Something " + parameter; //many lines of code return result; };
Однако, пожалуйста, не используйте это правило “однострочной лямбды” в качестве догмы . Если у вас есть две или три строки в определении лямбды, может оказаться бесполезным извлекать этот код в другой метод.
8.2. Избегайте Указания Типов Параметров
Компилятор в большинстве случаев способен определить тип лямбда-параметров с помощью вывода типа . Поэтому добавление типа к параметрам является необязательным и может быть опущено.
Сделай это:
(a, b) -> a.toLowerCase() + b.toLowerCase();
вместо этого:
(String a, String b) -> a.toLowerCase() + b.toLowerCase();
8.3. Избегайте круглых скобок Вокруг Одного параметра
Лямбда-синтаксис требует круглых скобок только вокруг более чем одного параметра или когда параметра вообще нет. Вот почему безопасно сделать ваш код немного короче и исключить круглые скобки, когда есть только один параметр.
Итак, сделайте это:
a -> a.toLowerCase();
вместо этого:
(a) -> a.toLowerCase();
8.4. Избегайте оператора возврата и фигурных скобок
Скобки и return операторы необязательны в однострочных лямбда-телах. Это означает, что их можно опустить для ясности и краткости.
Сделай это:
a -> a.toLowerCase();
вместо этого:
a -> {return a.toLowerCase()};
8.5. Использование ссылок на методы
Очень часто, даже в наших предыдущих примерах, лямбда-выражения просто вызывают методы, которые уже реализованы в других местах. В этой ситуации очень полезно использовать другую функцию Java 8: ссылки на методы .
Итак, лямбда-выражение:
a -> a.toLowerCase();
может быть заменен:
String::toLowerCase;
Это не всегда короче, но делает код более читабельным.
9. Используйте “Эффективно Окончательные” Переменные
Доступ к неконечной переменной внутри лямбда-выражений приведет к ошибке во время компиляции. Но это не означает, что вы должны отмечать каждую целевую переменную как окончательную.
В соответствии с концепцией ” эффективно final ” компилятор обрабатывает каждую переменную как final, до тех пор, пока она назначается только один раз.
Безопасно использовать такие переменные внутри лямбд, поскольку компилятор будет контролировать их состояние и вызывать ошибку во время компиляции сразу же после любой попытки их изменения.
Например, следующий код не будет компилироваться:
public void method() { String localVariable = "Local"; Foo foo = parameter -> { String localVariable = parameter; return localVariable; }; }
Компилятор сообщит вам, что:
Variable 'localVariable' is already defined in the scope.
Такой подход должен упростить процесс обеспечения потокобезопасности выполнения лямбды.
10. Защита переменных объекта от мутации
Одной из основных целей лямбд является использование в параллельных вычислениях, что означает, что они действительно полезны, когда речь заходит о потокобезопасности.
Парадигма “эффективного финала” здесь очень помогает, но не в каждом случае. Лямбды не могут изменить значение объекта из области охвата. Но в случае изменяемых объектных переменных состояние может быть изменено внутри лямбда-выражений.
Рассмотрим следующий код:
int[] total = new int[1]; Runnable r = () -> total[0]++; r.run();
Этот код является законным, так как переменная total остается “фактически окончательной”. Но будет ли объект, на который он ссылается, иметь то же состояние после выполнения лямбды? Нет!
Сохраните этот пример в качестве напоминания, чтобы избежать кода, который может вызвать неожиданные мутации.
11. Заключение
В этом уроке мы рассмотрели некоторые лучшие практики и подводные камни в лямбда-выражениях и функциональных интерфейсах Java 8. Несмотря на полезность и мощь этих новых функций, они являются всего лишь инструментами. Каждый разработчик должен обращать внимание при их использовании.
Полный исходный код для примера доступен в этом проекте GitHub – это проект Maven и Eclipse, поэтому его можно импортировать и использовать как есть.