Функциональный интерфейс Function в Java. Методы apply, andThen, compose и identity

User Rating: 5 / 5

В этой статье мы рассмотрим функциональный интерфейс Function (а точнее Function<T, R>), который появился, начиная с версии Java 1.8. Статья будет носить практический характер, нацеленный на демонстрацию некоторых примеров использования этого полезного функционального интерфейса.

Итак, функциональный интерфейс Function<T, R>, расположенный в стандартном пакете java.util.function, имеет один абстрактный метод, требующий обязательной реализации:

R apply(T t);

Также у него есть пара методов с названиями compose и andThen с реализацией по умолчанию (default):

Контракт метода compose:

default <V> Function<V, R> compose(Function<? super V, ? extends T> before)

Контракт метода andThen:

default <V> Function<T, V> andThen(Function<? super R, ? extends V> after)

Мы рассмотрим два основных сценария использования функционального интерфейса Function<T, R> в программе на Java:

  • посредством определения отдельного класса, реализующего интерфейс Function<T, R>
  • посредством использования лямбда-выражения непосредственно в коде, где требуется использование функционального интерфейса Function<T, R>

Для первого сценария давайте создадим новый класс Int2DoubleMultiplyFunction в пакете ru.allineed.samples.function_interface, который реализует интерфейс следующим образом:

package ru.allineed.samples.function_interface;

import java.util.function.Function;

/**
 * [RU] Класс, реализующий функциональный интерфейс {@code Function<T, R>} как {@code Function<Integer, Double>}<br/>
 * [EN] Class implementing the function interface {@code Function<T, R>} as {@code Function<Integer, Double>}
 */
public class Int2DoubleMultiplyFunction implements Function<Integer, Double> {

    @Override
    public Double apply(Integer integer) {
        if (integer == null) {
            return null;
        }

        if (integer == 0) {
            return 0.;
        }

        return 2. * integer;
    }
}

Здесь мы реализуем функциональный интерфейс Function<T, R> с двумя аргументами типа - Integer и Double. Это означает, что наша функция будет принимать на вход параметр типа Integer, а возвращать будет значение типа Double - это хорошо прослеживается в контракте метода apply.

Логика метода apply в нашем примере простая:

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

Теперь создадим класс FunctionInterfaceExample для проверки нашей реализации функционального интерфейса:

package ru.allineed.samples.function_interface;

import ru.allineed.samples.config.Localization;

import java.util.function.Function;

public class FunctionInterfaceExample {

    public static void main(String[] args) {
        Function<Integer, Double> funcLambda = (Integer intVal) -> {
            if (intVal == null) {
                return null;
            }
            if (intVal == 0) {
                return 0.;
            }
            return 2. * intVal;
        };

        Int2DoubleMultiplyFunction funcInstance = new Int2DoubleMultiplyFunction();

        String sampleTitle = Localization.getLocalized(
                "Пример использования функции, заданной через лямбда-выражение:",
                "Example of using a function specified as lambda expression:");
        applyFunction(sampleTitle, funcLambda);

        sampleTitle = Localization.getLocalized(
                "Пример использования функции, заданной через отдельный экземпляр класса Int2DoubleMultiplyFunction:",
                "Example of using a function specified as a separate instance of Int2DoubleMultiplyFunction class:");
        applyFunction(sampleTitle, funcInstance);

        Function<Double, Double> identityFunc = Function.identity();
        Double identityResult = identityFunc.apply(357.218);

        System.out.println();
        System.out.format("identityResult = %.5f%n", identityResult);
    }

    /**
     * [RU] Применяет переданную функцию {@code func} к различным входным аргументам<br/>
     * [EN] Applies the given function {@code func} to the different input arguments
     * @param title [RU] Текст заголовка для вывода на консоль; [EN] Header text for console output
     * @param func [RU] Экземпляр функции {@code Function<Integer, Double>}; [EN] An instance of function
     * {@code Function<Integer, Double>}
     */
    public static void applyFunction(String title, Function<Integer, Double> func) {
        System.out.println();
        System.out.println(title);

        Double zeroResult = func.apply(0);
        System.out.format("zeroResult = %.2f%n", zeroResult);

        Double negativeResult1 = func.apply(-200);
        System.out.format("negativeResult1 = %.2f%n", negativeResult1);

        Double negativeResult2 = func.apply(-50);
        System.out.format("negativeResult2 = %.2f%n", negativeResult2);

        Double positiveResult1 = func.apply(777);
        System.out.format("positiveResult1 = %.2f%n", positiveResult1);

        Double positiveResult2 = func.apply(55);
        System.out.format("positiveResult2 = %.2f%n", positiveResult2);

        Double nullResult = func.apply(null);
        System.out.format("nullResult = %s%n", nullResult);

        Double andThenResult = func.andThen(aDouble -> aDouble + 3).andThen(aDouble -> aDouble + 5).apply(10);
        System.out.format("andThenResult = %.2f%n", andThenResult);

        Double composeResult = func.compose(x -> (Integer)x + 5).compose(x -> (Integer)x + 1).apply(10);
        System.out.format("composeResult = %.2f%n", composeResult);
    }
}

В начале метода main мы создали экземпляр функции с именем funcLambda и задали через лямбда-выражение точно такое же тело метода, как у вышеупомянутого метода apply. Это сделано специально для того, чтобы показать сразу второй сценарий, где не требуется создавать отдельный класс, реализующий функциональный интерфейс Function<T, R>.

Далее мы создаём экземпляр нашего класса Int2DoubleMultiplyFunction и сохраняем ссылку на него в переменной funcInstance:

Int2DoubleMultiplyFunction funcInstance = new Int2DoubleMultiplyFunction();

В классе также есть метод applyFunction, который и демонстрирует возврат различных значений из функции для различных входных значений функции.

Сначала мы вызовем этот метод, передав ему заголовок сценария, где протестируем работу функции через лямбда-выражение, а также переменную funcLambda:

        String sampleTitle = Localization.getLocalized(
                "Пример использования функции, заданной через лямбда-выражение:",
                "Example of using a function specified as lambda expression:");
        applyFunction(sampleTitle, funcLambda);

При выполнении этого участка кода на экран консоли будет выведен следующий текст:

Пример использования функции, заданной через лямбда-выражение:
zeroResult = 0,00
negativeResult1 = -400,00
negativeResult2 = -100,00
positiveResult1 = 1554,00
positiveResult2 = 110,00
nullResult = null
andThenResult = 28,00
composeResult = 32,00

Далее мы делаем практически то же самое, но передаём экземпляр funcInstance для нашего отдельного класса Int2DoubleMultiplyFunction:

        sampleTitle = Localization.getLocalized(
                "Пример использования функции, заданной через отдельный экземпляр класса Int2DoubleMultiplyFunction:",
                "Example of using a function specified as a separate instance of Int2DoubleMultiplyFunction class:");
        applyFunction(sampleTitle, funcInstance);

Результат на экране консоли при запуске этого участка кода:

Пример использования функции, заданной через отдельный экземпляр класса Int2DoubleMultiplyFunction:
zeroResult = 0,00
negativeResult1 = -400,00
negativeResult2 = -100,00
positiveResult1 = 1554,00
positiveResult2 = 110,00
nullResult = null
andThenResult = 28,00
composeResult = 32,00

В самом конце метода main мы тестируем работу интересного статического метода identity, который есть у функционального интерфейса Function<T, R>:

        Function<Double, Double> identityFunc = Function.identity();
        Double identityResult = identityFunc.apply(357.218);

        System.out.println();
        System.out.format("identityResult = %.5f%n", identityResult);

Данный метод возвращает экземпляр для функции, которая будет возвращать то же самое значение, что подаётся ей на вход. В этом легко убедиться, запустив этот пример - на экран консоли будет выведено следующее значение переменной identityResult (обратите внимание, что оно равно тому же числу 357.218, что мы подали на вход методу apply):

identityResult = 357,21800

Давайте ещё разберём некоторые части метода applyFunction. Они во многом похожи и просто тестируют нашу функцию с разными входными значениями. Например, так мы вызываем нашу функцию с входным целочисленным значением 0, вызывая метод apply и записывая результат функции в переменную zeroResult:

        Double zeroResult = func.apply(0);
        System.out.format("zeroResult = %.2f%n", zeroResult);

Аналогичным же образом следуют участки кода с вызовом метода apply для разных целых чисел: -200, -50, 777, 55. Результаты вычисления нашей функции для этих чисел записываются в соответствующие переменные и также выводятся на экран консоли.

Отдельного внимания заслуживают методы andThen и compose. Метод andThen позволяет сцепить в цепочку дополнительные функции, которые будут применены и вычислены после того, как будет вызван метод apply и его реализация.

То есть для строки кода:

Double andThenResult = func.andThen(aDouble -> aDouble + 3).andThen(aDouble -> aDouble + 5).apply(10);

будет соблюдён следующий порядок вычисления:

  1. для входного значения 10 в метод apply будет сначала применена логика метода apply: т.к. число положительное, то функция вернёт результат 2 * 10 = 20
  2. далее будет вычислены функции, переданные в оба метода andThen, а именно: к предыдущему результату 20, который возвращает apply будет применено сложение с числами 5 и 3: 20 + 5 + 3 = 28

Метод compose работает противоположным образом - сначала он выполнит вычисления всех функций, переданных в compose, а в самом конце вызовет уже логику, находящуюся в реализации метода apply.

То есть для строки кода:

Double composeResult = func.compose(x -> (Integer)x + 5).compose(x -> (Integer)x + 1).apply(10);

 будет соблюдён следующий порядок вычисления:

  1. для входного значения 10 в метод apply будут вычислены функции, переданные в метод compose: 10 + 1 + 5 = 16
  2. к получившемуся результирующему значению будет применена логика метода apply: т.е. на вход получим число 16, и так как оно положительное, то наша функция вернёт результат 2 * 16 = 32

Можете обратить внимание на вывод консоли с описанными результатами для переменных andThenResult и composeResult:

andThenResult = 28,00
composeResult = 32,00

На этом всё, в комментариях напишите свои мысли по поводу функционального интерфейса Function<T, R>, поделитесь насколько часто используете его при программировании на Java.

Внизу вы найдете ссылки на наш Git-репозиторий со всеми примерами на Java, а также ссылку на пример из этой статьи. Если вы ещё не настроили запуск примеров кода с нашего сайта, то внизу вы также найдете инструкцию по запуску примеров с сайта.

Яндекс.Метрика