Добро пожаловать в пример учебника по функциональным интерфейсам Java 8. Java всегда был языком объектно-ориентированного программирования. Это означает, что все в программировании на Java вращается вокруг объектов (за исключением некоторых примитивных типов для простоты). У нас нет только функций в Java, они являются частью класса, и нам нужно использовать класс/объект для вызова любой функции.
Функциональные интерфейсы Java 8
Если мы посмотрим на некоторые другие языки программирования, такие как C++, JavaScript, их называют функциональными языками программирования, потому что мы можем писать функции и использовать их по мере необходимости. Некоторые из этих языков поддерживают как объектно-ориентированное программирование, так и функциональное программирование. Быть объектно-ориентированным не плохо, но это приносит много многословности в программу. Например, предположим, мы должны создать экземпляр Runnable. Обычно мы делаем это с использованием анонимных классов, как показано ниже.
Runnable r = new Runnable(){
@Override
public void run() {
System.out.println("My Runnable");
}};
Если вы посмотрите на приведенный выше код, реальная часть, которая имеет значение, это код внутри метода run(). Весь остальной код присутствует из-за структуры программ на Java. Функциональные интерфейсы Java 8 и лямбда-выражения помогают нам писать более короткий и чистый код, удаляя много шаблонного кода.
Функциональный интерфейс Java 8
Интерфейс с ровно одним абстрактным методом называется функциональным интерфейсом. Аннотация @FunctionalInterface
добавляется, чтобы мы могли пометить интерфейс как функциональный. Использовать ее необязательно, но это лучшая практика для функциональных интерфейсов, чтобы избежать случайного добавления дополнительных методов. Если интерфейс помечен аннотацией @FunctionalInterface
, и мы попытаемся иметь более одного абстрактного метода, это вызовет ошибку компилятора. Основное преимущество функциональных интерфейсов Java 8 заключается в том, что мы можем использовать лямбда-выражения для их создания и избежать использования громоздкой реализации анонимного класса. API коллекций Java 8 был переписан, и введен новый API потоков, который использует множество функциональных интерфейсов. Java 8 определила множество функциональных интерфейсов в пакете java.util.function
. Некоторые из полезных функциональных интерфейсов Java 8: Consumer
, Supplier
, Function
и Predicate
. Подробнее о них можно узнать в Примере потока Java 8. java.lang.Runnable
– отличный пример функционального интерфейса с одним абстрактным методом run()
. Ниже приведен фрагмент кода, предоставляющий руководство по функциональным интерфейсам:
interface Foo { boolean equals(Object obj); }
// Не функционально, потому что equals уже является неявным элементом (класс Object)
interface Comparator {
boolean equals(Object obj);
int compare(T o1, T o2);
}
// Функционально, потому что у Comparator есть только один абстрактный ненулевой метод
interface Foo {
int m();
Object clone();
}
// Не функционально, потому что метод Object.clone не является общедоступным
interface X { int m(Iterable arg); }
interface Y { int m(Iterable arg); }
interface Z extends X, Y {}
// Функционально: два метода, но они имеют одинаковую сигнатуру
interface X { Iterable m(Iterable arg); }
interface Y { Iterable m(Iterable arg); }
interface Z extends X, Y {}
// Функционально: Y.m является подсигнатурой и возвращаемым типом
interface X { int m(Iterable arg); }
interface Y { int m(Iterable arg); }
interface Z extends X, Y {}
// Не функционально: Ни один метод не имеет подсигнатуры всех абстрактных методов
interface X { int m(Iterable arg, Class c); }
interface Y { int m(Iterable arg, Class > c); }
interface Z extends X, Y {}
// Не функционально: Ни один метод не имеет подсигнатуры всех абстрактных методов
interface X { long m(); }
interface Y { int m(); }
interface Z extends X, Y {}
// Ошибка компилятора: нет метода с заменяемым типом возвращаемого значения
interface Foo { void m(T arg); }
interface Bar { void m(T arg); }
interface FooBar extends Foo, Bar {}
// Ошибка компилятора: разные сигнатуры, одинаковое стирание
Лямбда-выражение
Лямбда-выражения – это способ визуализации функционального программирования в объектно-ориентированном мире Java. Объекты являются основой языка программирования Java, и мы никогда не можем иметь функцию без объекта, поэтому язык Java предоставляет поддержку использования лямбда-выражений только с функциональными интерфейсами. Поскольку в функциональных интерфейсах есть только одна абстрактная функция, нет путаницы в применении лямбда-выражения к методу. Синтаксис лямбда-выражений – (аргумент) -> (тело). Теперь давайте посмотрим, как мы можем написать вышеанонимный Runnable, используя лямбда-выражение.
Runnable r1 = () -> System.out.println("My Runnable");
Давайте попробуем понять, что происходит в вышеуказанном лямбда-выражении.
- Runnable – это функциональный интерфейс, поэтому мы можем использовать лямбда-выражение для создания его экземпляра.
- Поскольку метод run() не принимает аргументов, наше лямбда-выражение также не имеет аргументов.
- Точно так же, как в блоках if-else, мы можем избежать фигурных скобок ({}) , поскольку у нас есть только один оператор в теле метода. Для нескольких операторов нам пришлось бы использовать фигурные скобки, как в любых других методах.
Зачем нам нужно лямбда-выражение
-
Сокращение количества строк кода Одним из явных преимуществ использования лямбда-выражения является сокращение количества кода, мы уже видели, насколько легко можно создать экземпляр функционального интерфейса, используя лямбда-выражение, а не анонимный класс.
-
Поддержка последовательного и параллельного выполнения Еще одно преимущество использования лямбда-выражений заключается в том, что мы можем воспользоваться поддержкой последовательных и параллельных операций Stream API. Чтобы объяснить это, давайте рассмотрим простой пример, где нам нужно написать метод для проверки, является ли переданное число простым числом или нет. Традиционно мы бы написали код так:
//Традиционный подход private static boolean isPrime(int number) { if(number < 2) return false; for(int i=2; i<number; i++){ if(number % i == 0) return false; } return true; }
Проблема с вышеприведенным кодом заключается в том, что он последовательный по своей природе, если число очень большое, то это займет значительное количество времени. Еще одна проблема с кодом заключается в том, что здесь так много точек выхода, и он нечитаемый. Давайте посмотрим, как мы можем написать тот же метод, используя лямбда-выражения и Stream API.
//Декларативный подход private static boolean isPrime(int number) { return number > 1 && IntStream.range(2, number).noneMatch( index -> number % index == 0); }
IntStream
– это последовательность примитивных элементов типа int, поддерживающая последовательные и параллельные агрегатные операции. Это специализация примитивного типа int дляStream
. Для большей читаемости мы также можем написать метод следующим образом.private static boolean isPrime(int number) { IntPredicate isDivisible = index -> number % index == 0; return number > 1 && IntStream.range(2, number).noneMatch( isDivisible); }
Если вы не знакомы с IntStream, его метод range() возвращает последовательный упорядоченный IntStream от startInclusive (включительно) до endExclusive (исключительно) с инкрементом шага 1. Метод noneMatch() возвращает true, если ни один элемент этого потока не удовлетворяет предоставленному предикату. Он может не оценивать предикат для всех элементов, если это необходимо для определения результата.
-
Передача поведения в методы Давайте посмотрим, как мы можем использовать лямбда-выражения для передачи поведения метода на примере. Предположим, у нас есть метод для суммирования чисел в списке, если они соответствуют определенным критериям. Мы можем использовать предикат и написать метод следующим образом.
public static int sumWithCondition(List<Integer> numbers, Predicate<Integer> predicate) { return numbers.parallelStream() .filter(predicate) .mapToInt(i -> i) .sum(); }
Пример использования:
//сумма всех чисел sumWithCondition(numbers, n -> true) //сумма всех четных чисел sumWithCondition(numbers, i -> i%2==0) //сумма всех чисел больше 5 sumWithCondition(numbers, i -> i>5)
-
Большая эффективность с ленивостью Еще одним преимуществом использования лямбда-выражения является ленивая оценка. Например, допустим, нам нужно написать метод для определения максимального нечетного числа в диапазоне от 3 до 11 и вернуть его квадрат. Обычно мы будем писать код для этого метода так:
private static int findSquareOfMaxOdd(List<Integer> numbers) { int max = 0; for (int i : numbers) { if (i % 2 != 0 && i > 3 && i < 11 && i > max) { max = i; } } return max * max; }
Вышеуказанная программа всегда будет выполняться последовательно, но мы можем использовать API Stream для достижения этого и получения выгоды от поиска ленивости. Давайте посмотрим, как мы можем переписать этот код в функциональном стиле, используя API Stream и лямбда-выражения.
public static int findSquareOfMaxOdd(List<Integer> numbers) { return numbers.stream() .filter(NumberTest::isOdd) //Predicate является функциональным интерфейсом и .filter(NumberTest::isGreaterThan3) // мы используем лямбды для его инициализации .filter(NumberTest::isLessThan11) // вместо анонимных внутренних классов .max(Comparator.naturalOrder()) .map(i -> i * i) .get(); } public static boolean isOdd(int i) { return i % 2 != 0; } public static boolean isGreaterThan3(int i){ return i > 3; } public static boolean isLessThan11(int i){ return i < 11; }
Если вы удивлены оператором двойного двоеточия (::), он был введен в Java 8 и используется для ссылок на методы. Компилятор Java заботится о сопоставлении аргументов с вызываемым методом. Это сокращенная форма лямбда-выражений
i -> isGreaterThan3(i)
илиi -> NumberTest.isGreaterThan3(i)
.
Примеры лямбда-выражений
Ниже я привожу несколько фрагментов кода для лямбда-выражений с небольшими комментариями, объясняющими их.
() -> {} // No parameters; void result
() -> 42 // No parameters, expression body
() -> null // No parameters, expression body
() -> { return 42; } // No parameters, block body with return
() -> { System.gc(); } // No parameters, void block body
// Сложное тело блока с несколькими возвратами
() -> {
if (true) return 10;
else {
int result = 15;
for (int i = 1; i < 10; i++)
result *= i;
return result;
}
}
(int x) -> x+1 // Single declared-type argument
(int x) -> { return x+1; } // same as above
(x) -> x+1 // Single inferred-type argument, same as below
x -> x+1 // Parenthesis optional for single inferred-type case
(String s) -> s.length() // Single declared-type argument
(Thread t) -> { t.start(); } // Single declared-type argument
s -> s.length() // Single inferred-type argument
t -> { t.start(); } // Single inferred-type argument
(int x, int y) -> x+y // Multiple declared-type parameters
(x,y) -> x+y // Multiple inferred-type parameters
(x, final y) -> x+y // Illegal: can't modify inferred-type parameters
(x, int y) -> x+y // Illegal: can't mix inferred and declared types
Ссылки на методы и конструкторы
A method reference is used to refer to a method without invoking it; a constructor reference is similarly used to refer to a constructor without creating a new instance of the named class or array type. Examples of method and constructor references:
System::getProperty
System.out::println
"abc"::length
ArrayList::new
int[]::new
Вот и все для руководства по функциональным интерфейсам Java 8 и лямбда-выражениям. Я настоятельно рекомендую изучить их использование, потому что эта синтаксическая конструкция нова для Java, и ее освоение займет некоторое время. Также вам следует ознакомиться с Особенностями Java 8, чтобы узнать обо всех улучшениях и изменениях в выпуске Java 8.
Source:
https://www.digitalocean.com/community/tutorials/java-8-functional-interfaces