Рубрики
Без рубрики

Лямбда-выражения и функциональные интерфейсы: Советы и рекомендации

Советы и рекомендации по использованию лямбд Java 8 и функциональных интерфейсов.

Автор оригинала: baeldung.

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, Function fn) {
    return fn.apply(string);
}

Чтобы выполнить это, мы можем написать:

Function fn = 
  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(Callable c) 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(Callable c) 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, поэтому его можно импортировать и использовать как есть.