Функциональные интерфейсы в Java 8

Функциональные интерфейсы в Java 8

1. Вступление

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

Дальнейшее чтение:

Возможность итерации для потоковой передачи в Java

В статье объясняется, как преобразовать Iterable в Stream и почему интерфейс Iterable не поддерживает его напрямую.

Read more

Как использовать if / else Logic в Java 8 Streams

Узнайте, как применить логику if / else к Java 8 Streams.

Read more

2. Лямбды в Java 8

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

До Java 8 вы обычно создавали класс для каждого случая, когда вам нужно было инкапсулировать один элемент функциональности. Это подразумевало много ненужного стандартного кода для определения того, что служило примитивным представлением функции.

Лямбды, функциональные интерфейсы и лучшие практики работы с ними в целом описаны в статье“Lambda Expressions and Functional Interfaces: Tips and Best Practices”. В этом руководстве основное внимание уделяется некоторым конкретным функциональным интерфейсам, присутствующим в пакетеjava.util.function.

3. Функциональные интерфейсы

Все функциональные интерфейсы рекомендуется иметь информативную аннотацию@FunctionalInterface. Это не только четко отражает назначение этого интерфейса, но также позволяет компилятору генерировать ошибку, если аннотированный интерфейс не удовлетворяет условиям.

Any interface with a SAM(Single Abstract Method) is a functional interface, и его реализация может рассматриваться как лямбда-выражения.

Обратите внимание, что методыdefault в Java 8 не являютсяabstract и не учитываются: функциональный интерфейс может по-прежнему иметь несколько методовdefault. Вы можете увидеть это, посмотрев наFunction’sdocumentation.

4. функции

Самый простой и общий случай лямбды - это функциональный интерфейс с методом, который получает одно значение и возвращает другое. Эта функция одного аргумента представлена ​​интерфейсомFunction, который параметризуется типами его аргумента и возвращаемого значения:

public interface Function { … }

Одним из вариантов использования типаFunction в стандартной библиотеке является методMap.computeIfAbsent, который возвращает значение из карты по ключу, но вычисляет значение, если ключ еще не присутствует на карте. Чтобы вычислить значение, он использует переданную реализацию функции:

Map nameMap = new HashMap<>();
Integer value = nameMap.computeIfAbsent("John", s -> s.length());

В этом случае значение будет вычислено путем применения функции к ключу, размещения внутри карты, а также возврата из вызова метода. Кстати,we may replace the lambda with a method reference that matches passed and returned value types.

Помните, что объект, для которого вызывается метод, на самом деле является неявным первым аргументом метода, который позволяет привести ссылку на метод экземпляраlength к интерфейсуFunction:

Integer value = nameMap.computeIfAbsent("John", String::length);

ИнтерфейсFunction также имеет методcompose по умолчанию, который позволяет объединить несколько функций в одну и выполнять их последовательно:

Function intToString = Object::toString;
Function quote = s -> "'" + s + "'";

Function quoteIntToString = quote.compose(intToString);

assertEquals("'5'", quoteIntToString.apply(5));

ФункцияquoteIntToString - это комбинация функцииquote, примененная к результату функцииintToString.

5. Специализации примитивных функций

Поскольку примитивный тип не может быть аргументом универсального типа, существуют версии интерфейсаFunction для наиболее часто используемых примитивных типовdouble,int,long и их комбинаций. в типах аргументов и возвращаемых значений:

  • АргументыIntFunction,LongFunction,DoubleFunction: имеют указанный тип, тип возвращаемого значения параметризован

  • ToIntFunction,ToLongFunction,ToDoubleFunction: возвращаемый тип имеет указанный тип, аргументы параметризованы

  • DoubleToIntFunction,DoubleToLongFunction,IntToDoubleFunction,IntToLongFunction,LongToIntFunction,LongToDoubleFunction - с аргументом и типом возвращаемого значения, определенными как примитивные типы, как указано в их имена

Не существует готового функционального интерфейса, скажем, для функции, которая принимаетshort и возвращаетbyte, но ничто не мешает вам написать свой собственный:

@FunctionalInterface
public interface ShortToByteFunction {

    byte applyAsByte(short s);

}

Теперь мы можем написать метод, который преобразует массивshort в массивbyte, используя правило, определяемоеShortToByteFunction:

public byte[] transformArray(short[] array, ShortToByteFunction function) {
    byte[] transformedArray = new byte[array.length];
    for (int i = 0; i < array.length; i++) {
        transformedArray[i] = function.applyAsByte(array[i]);
    }
    return transformedArray;
}

Вот как мы можем использовать его для преобразования массива шортов в массив байтов, умноженный на 2:

short[] array = {(short) 1, (short) 2, (short) 3};
byte[] transformedArray = transformArray(array, s -> (byte) (s * 2));

byte[] expectedArray = {(byte) 2, (byte) 4, (byte) 6};
assertArrayEquals(expectedArray, transformedArray);

6. Специализации функций двух арностей

Чтобы определить лямбда-выражения с двумя аргументами, мы должны использовать дополнительные интерфейсы, которые содержат ключевое слово «Bi” в их именах:BiFunction,ToDoubleBiFunction,ToIntBiFunction иToLongBiFunction». .

BiFunction имеет оба аргумента и тип возвращаемого значения, в то время какToDoubleBiFunction и другие позволяют вам возвращать примитивное значение.

Один из типичных примеров использования этого интерфейса в стандартном API - это методMap.replaceAll, который позволяет заменить все значения на карте некоторым вычисленным значением.

Давайте воспользуемся реализациейBiFunction, которая получает ключ и старое значение, чтобы вычислить новое значение для зарплаты и вернуть его.

Map salaries = new HashMap<>();
salaries.put("John", 40000);
salaries.put("Freddy", 30000);
salaries.put("Samuel", 50000);

salaries.replaceAll((name, oldValue) ->
  name.equals("Freddy") ? oldValue : oldValue + 10000);

7. Поставщики

Функциональный интерфейсSupplier - это еще одна специализацияFunction, которая не принимает никаких аргументов. Обычно используется для ленивой генерации значений. Например, давайте определим функцию, которая возводит в квадрат значениеdouble. Он получит не само значение, аSupplier этого значения:

public double squareLazy(Supplier lazyValue) {
    return Math.pow(lazyValue.get(), 2);
}

Это позволяет нам лениво генерировать аргумент для вызова этой функции, используя реализациюSupplier. Это может быть полезно, если генерация этого аргумента занимает значительное время. Мы смоделируем это, используя метод GuavasleepUninterruptibly:

Supplier lazyValue = () -> {
    Uninterruptibles.sleepUninterruptibly(1000, TimeUnit.MILLISECONDS);
    return 9d;
};

Double valueSquared = squareLazy(lazyValue);

Другой вариант использования для Поставщика - определение логики для генерации последовательности. Чтобы продемонстрировать это, давайте воспользуемся статическим методомStream.generate для созданияStream чисел Фибоначчи:

int[] fibs = {0, 1};
Stream fibonacci = Stream.generate(() -> {
    int result = fibs[1];
    int fib3 = fibs[0] + fibs[1];
    fibs[0] = fibs[1];
    fibs[1] = fib3;
    return result;
});

Функция, переданная методуStream.generate, реализует функциональный интерфейсSupplier. Обратите внимание, что для использования в качестве генератораSupplier обычно требуется какое-то внешнее состояние. В этом случае его состояние состоит из двух последних порядковых номеров Фибоначчи.

Чтобы реализовать это состояние, мы используем массив вместо пары переменных, потому чтоall external variables used inside the lambda have to be effectively final.

Другие специализации функционального интерфейсаSupplier включаютBooleanSupplier,DoubleSupplier,LongSupplier иIntSupplier, чьи возвращаемые типы являются соответствующими примитивами.

8. Потребители

В отличие отSupplier,Consumer принимает обобщенный аргумент и ничего не возвращает. Это функция, которая представляет побочные эффекты.

Например, давайте поприветствуем всех в списке имен, напечатав приветствие в консоли. Лямбда, переданная методуList.forEach, реализует функциональный интерфейсConsumer:

List names = Arrays.asList("John", "Freddy", "Samuel");
names.forEach(name -> System.out.println("Hello, " + name));

Существуют также специализированные версииConsumer -DoubleConsumer,IntConsumer иLongConsumer - которые принимают примитивные значения в качестве аргументов. Более интересен интерфейсBiConsumer. Один из его вариантов использования - перебирать записи карты:

Map ages = new HashMap<>();
ages.put("John", 25);
ages.put("Freddy", 24);
ages.put("Samuel", 30);

ages.forEach((name, age) -> System.out.println(name + " is " + age + " years old"));

Другой набор специализированных версийBiConsumer состоит изObjDoubleConsumer,ObjIntConsumer иObjLongConsumer, которые получают два аргумента, один из которых является обобщенным, а другой - примитивным типом.

9. Предикаты

В математической логике предикат - это функция, которая получает значение и возвращает логическое значение.

Функциональный интерфейсPredicate - это специализацияFunction, которая получает обобщенное значение и возвращает логическое значение. Типичный вариант использования лямбдаPredicate - фильтровать набор значений:

List names = Arrays.asList("Angela", "Aaron", "Bob", "Claire", "David");

List namesWithA = names.stream()
  .filter(name -> name.startsWith("A"))
  .collect(Collectors.toList());

В приведенном выше коде мы фильтруем список с помощью APIStream и оставляем только имена, начинающиеся с буквы «A». Логика фильтрации инкапсулирована в реализацииPredicate.

Как и во всех предыдущих примерах, существуют версии этой функцииIntPredicate,DoublePredicate иLongPredicate, которые получают примитивные значения.

10. операторы

ИнтерфейсыOperator - это особые случаи функции, которая получает и возвращает один и тот же тип значения. ИнтерфейсUnaryOperator получает единственный аргумент. Один из вариантов его использования в API коллекций - замена всех значений в списке некоторыми вычисленными значениями того же типа:

List names = Arrays.asList("bob", "josh", "megan");

names.replaceAll(name -> name.toUpperCase());

ФункцияList.replaceAll возвращаетvoid, поскольку она заменяет значения на месте. Чтобы соответствовать цели, лямбда, используемая для преобразования значений списка, должна возвращать тот же тип результата, что и при получении. Вот почемуUnaryOperator здесь полезен.

Конечно, вместоname → name.toUpperCase() вы можете просто использовать ссылку на метод:

names.replaceAll(String::toUpperCase);

Один из наиболее интересных вариантов использованияBinaryOperator - это операция сокращения. Предположим, мы хотим объединить коллекцию целых чисел в сумму всех значений. С APIStream мы могли бы сделать это с помощью сборщика,, но более общий способ сделать это - использовать методreduce:

List values = Arrays.asList(3, 5, 8, 9, 12);

int sum = values.stream()
  .reduce(0, (i1, i2) -> i1 + i2);

Методreduce получает начальное значение аккумулятора и функциюBinaryOperator. Аргументы этой функции - это пара значений одного типа, а сама функция содержит логику для объединения их в одно значение одного типа. Passed function must be associative, что означает, что порядок агрегирования значений не имеет значения, т.е. должно соблюдаться следующее условие:

op.apply(a, op.apply(b, c)) == op.apply(op.apply(a, b), c)

Ассоциативное свойство операторной функцииBinaryOperator позволяет легко распараллелить процесс редукции.

Конечно, есть также специализацииUnaryOperator иBinaryOperator, которые можно использовать с примитивными значениями, а именноDoubleUnaryOperator,IntUnaryOperator,LongUnaryOperator,DoubleBinaryOperator,IntBinaryOperator иLongBinaryOperator.

11. Устаревшие функциональные интерфейсы

Не все функциональные интерфейсы появились в Java 8. Многие интерфейсы из предыдущих версий Java соответствуют ограничениямFunctionalInterface и могут использоваться как лямбда-выражения. Ярким примером являются интерфейсыRunnable иCallable, которые используются в API параллелизма. В Java 8 эти интерфейсы также отмечены аннотацией@FunctionalInterface. Это позволяет нам значительно упростить код параллелизма:

Thread thread = new Thread(() -> System.out.println("Hello From Another Thread"));
thread.start();

12. Заключение

В этой статье мы описали различные функциональные интерфейсы, присутствующие в API Java 8, которые можно использовать как лямбда-выражения. Исходный код статьи доступенover on GitHub.