Java всегда была объектно-ориентированным языком программирования. А это означает, что все вращается вокруг объектов (за исключением некоторых примитивных типов). Любые методы и функции в Java являются частью класса, поэтому мы должны использовать класс/объект для вызова любой функции. Так что же в этот гармоничный мир привнесла Java 8?
Если мы посмотрим на такие языки программирования, как C++
, JavaScript
, то они официально называются функциональными языками. Это значит, что мы можем писать функции и использовать их при необходимости. Такие языки поддерживают объектно-ориентированное программирование, а также функциональное программирование.
Но объектно-ориентированность языка не является его недостатком, однако влечет за собой написание много служебного кода. Например, нам надо создать экземпляр Runnable
. Обычно в Java мы делаем это с помощью анонимных классов, как показано ниже:
1 2 3 4 5 |
Runnable r = new Runnable(){ @Override public void run() { System.out.println("Это Runnable"); }}; |
Обратите внимание, нам нужно лишь то, что находится внутри метода run()
. Остальной код является лишь описательной частью, особенностью написания кода на Java.
Что предлагает нам Java 8?
Java 8 вводит такое понятие, как функциональные интерфейсы и лямбда-выражения, которые призваны упростить и максимально очистить программу от бесполезного кода.
Функциональные интерфейсы
Интерфейс с одним абстрактным методом называется функциональным интерфейсом. Чтобы отметить интерфейс как функциональный, используется аннотация @FunctionalInterface
. Она не является обязательной, но ее использование считается хорошим тоном программирования на Java 8. Также эта аннотация помогает избежать случайного добавления дополнительных методов.
Основным преимуществом функционального интерфейса является то, что мы можем использовать лямбда-выражения для создания экземпляра и, как следствие, избежать использования громоздких реализаций анонимного класса.
Collections API
в Java 8 был полностью переписан для добавления возможности использовать функциональные интерфейсы. На данный момент в Java 8 есть множество функциональных интерфейсов, которые находятся в пакете java.util.function
. Наиболее полезными для нас являются интерфейсы Consumer
, Supplier
, Function
и Predicate
. Подробнее о них читайте в нашей статье Полное руководство по Java 8 Strem API.
Лучшим примером функционального интерфейса является интерфейс java.lang.Runnable
с одним абстрактным методом r
un
()
.
А теперь давайте рассмотрим некоторые рекомендации по использованию функциональных интерфейсов:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// Интерфейс Foo не является функциональным, потому что // метод equals() также находится в классе Object interface Foo { boolean equals(Object obj); } // Интерфейс является функциональным, потому что // содержит только один метод, который не // является методом класса Object interface Comparator<T> { boolean equals(Object obj); int compare(T o1, T o2); } // Не является функциональным интерфейсом, потому что // метод clone() класса Object не публичный interface Foo { int m(); Object clone(); } |
Лямбда-выражения в Java 8
Лямбда-выражения способ визуализировать функциональное программирование в объектно-ориентированном Java-мире. Объекты являются основой языка программирования Java и мы не представляем себе функцию без объекта. Именно поэтому Java обеспечивает поддержку лямбда-выражений только с функциональными интерфейсами.
Мы уже знаем, что в функциональных интерфейсах есть только один метод, именно поэтому не возникает путаницы в применении лямбда-выражений. Синтаксис лямбда-выражения представляет собой такую структуру (аргумент) -> (содержимое лямбда-выражения). Теперь давайте перепишем приведенный выше Runnable
с помощью лямбда-выражения:
1 |
Runnable r1 = () -> System.out.println("Это Runnable с помощью лямбд"); |
Давайте разберемся, что же происходит в лямбда-выражении выше.
Runnable
— это функциональный интерфейс, поэтому мы можем использовать лямбда-выражения для создания его экземпляра.- Метод
run()
не принимает аргументов, поэтому наша лямбда также не содержит аргумент. - Мы не используем фигурные скобки ({}), потому что у нас только одно выражение в теле метода. В других случаях мы должны использовать фигурные скобки, как и в любом другом методе.
Почему мы должны использовать лямбда-выражения?
- Меньше строк кода. Одной из очевидных преимуществ лямбда-выражений является уменьшение объема кода, потому что мы можем создать экземпляр интерфейса с помощью функционального лямбда-выражения, а не с помощью анонимного класса.
- Поддержка последовательного и параллельного выполнения. Еще одно преимущество использования лямбда-выражения в том, что мы можем использовать Stream API для выполнения последовательных или параллельных операций.Чтобы объяснить это, давайте возьмем простой пример, где мы должны написать метод проверки числа на простоту.В Java 7 и ниже мы бы написали код так, как показано ниже. Код не очень оптимизирован, но хорошо подходит для примера:
1 2 3 4 5 6 7 8 9 10 11 12 |
// Java 7 и ниже 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:
1 2 3 4 5 6 |
// Функциональный Java 8 стиль private static boolean isPrime(int number) { return number > 1 && IntStream.range(2, number).noneMatch( index -> number % index == 0); } |
Из примера видно, что кода стало меньше, а вот читабельность не очень увеличилась, правда?
Маленькое пояснение к коду: IntStream
— последовательность примитивных int-овых элементов, поддерживающих последовательные и параллельные операции. Это примитивная int-специализация для Stream.
Давайте же улучшим читаемость кода и немного перепишем наш код:
1 2 3 4 5 6 7 8 |
// Переписанный код в функциональном стиле Java 8 с улучшенной читабельностью private static boolean isPrime(int number) { IntPredicate isDivisible = index -> number % index == 0; return number > 1 && IntStream.range(2, number).noneMatch( isDivisible); } |
Использование лямбда-выражений. Пример №1
Давайте посмотрим еще один пример использования лямбда-выражений. Например, нам нужно написать метод суммирования элементов списка по определенному критерию. Для этого мы можем использовать предикат и написать такой метод:
1 2 3 4 5 6 |
public static int sumWithCondition(List<Integer> numbers, Predicate<Integer> predicate) { return numbers.parallelStream() .filter(predicate) .mapToInt(i -> i) .sum(); } |
И пример использования этого метода:
1 2 3 4 5 6 |
//найдем сумму всех элементов sumWithCondition(numbers, n -> true) //суммируем только четные элементы sumWithCondition(numbers, i -> i%2==0) // найдем сумму всех элементов, больших 5 sumWithCondition(numbers, i -> i > 5) |
Лямбда-выражения. Пример 2
В следующем примере мы рассмотрим повышение эффективности вычислений с помощью «ленивых вычислений». Возьмем задание из какой-то лабораторной по курсу Java программирования, например, нам нужно написать метод вычисления максимального нечетного числа в диапазоне от 5 до 13 и вернуться его квадрат:
Обычно (в Java 7 и ниже) мы бы написали что-то похожее на это:
1 2 3 4 5 6 7 8 9 10 |
// Стиль Java 7 и ниже private static int findSquareOfMaxOdd(List<Integer> numbers) { int max = 0; for (int i : numbers) { if (i % 2 != 0 && i > 5 && i < 13 && i > max) { max = i; } } return max * max; } |
Приведенная выше программа всегда будет работать последовательно, но мы можем использовать Stream API используя ленивые вычисления. Давайте перепишем код выше в функциональный стиль со Stream API и лямбдами.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
public static int findSquareOfMaxOdd(List<Integer> numbers) { return numbers.stream() //Предикат (Predicate) это функциональный интерфейс // и мы используем лямбды для инициализации // а не анонимные классы .filter(NumberTest::isOdd) .filter(NumberTest::isGreaterThan5) .filter(NumberTest::isLessThan13) .max(Comparator.naturalOrder()) .map(i -> i * i) .get(); } public static boolean isOdd(int i) { return i % 2 != 0; } public static boolean isGreaterThan5(int i){ return i > 5; } public static boolean isLessThan13(int i){ return i < 13; } |
В нашем коде появился оператор двойного двоеточия (::
), который также введен в Java 8 и используется для ссылок на методы. Java компилятор заботится о маппинге аргументов вызываемого метода.
То есть по сути это краткая форма лямбда-выражений i -> isGreaterThan5(i)
или i -> NumberTest.isGreaterThan5(i)
.
Методы в Java 8
Ссылка на метод используется для того, чтобы обратиться к методу без его выполнения. Ссылка на Конструктор аналогично используется не создавая новый экземпляр класса.
1 2 3 4 5 6 |
// Java 8 позволяет нам писать такое: ArrayList::new int[]::new System::getProperty System.out::println "abc"::length |
В этой статья я попытался дать ту минимальную базу знаний для понимания функциональных интерфейсов и лямбда-выражений. Если вас заинтересовала Java 8, то почитайте о ней в отдельной статье «Новые возможности Java 8«, а также подробное руководство по Stream API. Также есть отдельная статья о Статических методах и методах по умолчанию в Java 8.
Следите за обновлениями раздела Java 8.
Спасибо!
А это оригинальная статья или переводная?