Недавно я изучал концепции функционального программирования с помощью Haskell, чтобы лучше понять, как писать код декларативным способом. Это исследование привело к тому, что я углубился в изучение того, как я могу применить некоторые из этих концепций в Java. Вам не нужно слишком глубоко заглядывать в “Функциональную Java”, чтобы обнаружить функциональные интерфейсы Java. Ниже приводится краткое изложение некоторых основных концепций функционального интерфейса Java. Конечно, в Java есть и другие функциональные концепции (например, потоки); но я постараюсь изучить их в других постах.
Что такое Функциональный интерфейс?
Интерфейс нуждается в одном (и только в одном) абстрактном методе, чтобы считаться функциональным интерфейсом.
// This is a Functional Interface interface MyFunctionalCalculator { int multiplier(int a); }
Java дает нам информационную аннотацию @FunctionalInterface.
@FunctionalInterface interface MyFunctionalCalculator { int multiplier(int a); }
Согласно documentation , аннотация дает нам следующие преимущества:
Если тип имеет этот тип аннотации, компиляторы должны генерировать сообщение об ошибке, если только:
- Тип является типом интерфейса, а не типом аннотации, перечисления или класса.
- Аннотированный тип удовлетворяет требованиям функционального интерфейса (один и только один абстрактный метод).
Я рекомендую использовать аннотацию, потому что она помогает документировать назначение интерфейса.
Отлично, как я могу их использовать?
На мой взгляд, реальное преимущество функционального интерфейса заключается в том, что он предоставляет целевой тип для лямбда-выражений и ссылок на методы. Эта функция позволяет нам передавать лямбда-выражения в качестве параметров и использовать композицию функций для написания декларативного кода.
Мы можем написать код, подобный следующему:
MyFunctionalCalculator calc = a -> a * 2; calc.multiplier(2); // returns 4
или что-то вроде этого:
int multiplierPlusOne(MyFunctionalCalculator calc, int a) { return calc.multiplier(a) + 1 } MyFunctionalCalculator doubler = a -> a * 2; MyFunctionalCalculator trippler = a -> a * 3; multiplierPlusOne(doubler, 2); // returns 5 multiplierPlusOne(trippler, 2); // returns 7
Если вы раньше работали с потоками Java, вы знакомы с этой концепцией. Многие потоковые методы принимают функциональный интерфейс в качестве параметра. Возможно, вы уже использовали Stream Filter метод раньше.
// Predicate is a Functional Interface // definition of filter filter(Predicate super T> predicate) // looks something like this when used. personList.stream().filter(p -> p.age > 21) // the (p -> p.age > 21) is the Predicate
Согласно документации, метод filter возвращает поток, состоящий из элементов потока, которые соответствуют заданному предикату. Предикат – это функциональный интерфейс, предоставляемый пакетом Java Util.
Некоторые из встроенных функциональных интерфейсов Java
Пакет Java Util предоставляет нам довольно много удобных функциональных интерфейсов, которые мы можем использовать для самых разных случаев. Ниже приводится краткое описание некоторых функций из docs .
Функция
Функция представляет собой функцию, которая принимает аргумент, а затем возвращает результат. Это выглядит так:
@FunctionalInterface public interface Function
Потребитель
Потребитель принимает один аргумент и не возвращает никаких результатов. В документах говорится, что ожидается, что он будет действовать через побочные эффекты.
@FunctionalInterface public interface Consumer
Поставщик
Поставщик предоставляет результат.
@FunctionalInterface public interface Supplier
Сказуемое
Представляет собой предикат (функцию с логическим значением) одного аргумента.
@FunctionalInterface public interface Predicate
бифункциональный
Функция Bi принимает два аргумента и возвращает результат.
@FunctionalInterface public interface BiFunction
Многие функции имеют версию “Bi”, которая допускает два аргумента. В пакете java util доступно гораздо больше функциональных интерфейсов для использования. Убедитесь, что вы ознакомились с документами, чтобы узнать, что еще доступно. Теперь давайте посмотрим, можем ли мы закодировать что-то более полезное, используя некоторые из этих функциональных интерфейсов.
Более реальный пример из реального мира
Вот небольшой пример того, как мы можем использовать функциональные интерфейсы, чтобы помочь нам создавать повторно используемый код. Недавно я наткнулся на некоторый код, в котором, как мне показалось, я мог бы использовать функциональный интерфейс в сочетании с потоками для написания более декларативного кода. Ниже приведен пример того, как я смог это сделать.
В коде есть перечисление под названием Galaxy, которое представляет различные галактики, на которые ссылаются в нашей системе. Это перечисление является ключом для загрузки значений в регистратор MDC (Сопоставленный диагностический контекст). MDC действует немного как Карта, которая хранит значения галактик для целей ведения журнала.
public enum Galaxy { ANDROMEDAE("andromedae"), ANTENNAE("Antennae"), EYE_OF_SAURON("eyeOfSauron"), MEDUSA_MERGER("medusaMerger"); private String key; // puts value in MDC public void put(final String value) { MDC.put(key, value) } // constructor and getter removed for brevity... }
Работая с некоторым кодом в другом месте приложения, я столкнулся с чем-то вроде этого. Приведенный ниже код находит звезду, затем проверяет MDC, чтобы увидеть, содержит ли он какие-либо Галактики. Для каждой Галактики, найденной в MDC, он ведет некоторую регистрацию.
Star foundStar = starRepository.getStarById(1L); for (final Galaxy g :: Galaxy.values()) { final String gal = MDC.get(g.getKey()) // if the galaxy is loaded in the MDC if(gal != null) { // do work with found galaxy logStarToGalaxy(foundStar, gal); } }
Теперь, на мой взгляд, этот код в порядке, но я хочу сделать этот код более декларативным и менее императивным. Я знаю, что хочу использовать поток вместо цикла. Чтобы сделать это, мне нужно будет добавить вспомогательный метод к перечислению.
public enum Galaxy { ANDROMEDAE("andromedae"), ANTENNAE("Antennae"), EYE_OF_SAURON("eyeOfSauron"), MEDUSA_MERGER("medusaMerger"); private String key; // puts value in MDC public void put(final String value) { MDC.put(key, value) } // constructor and getter removed for brevity... public static streamstream() { return Stream.of(Galaxy.values()); } }
Я только что добавил простой метод, который возвращает значения Enum в виде потока. Затем я могу реорганизовать цикл for в коде.
Galaxy.stream().forEach(g -> { final String gal = MDC.get(g.getKey()) // if the galaxy is loaded in the MDC if(gal != null) { // do work with found galaxy logStarToGalaxy(foundStar, gal); }
Хорошо, неплохо, но это не слишком помогло нам в стремлении сделать код более читабельным. Давайте посмотрим, можем ли мы использовать фильтр и функциональный интерфейс, чтобы избавиться от оператора if. Мы знаем, что метод filter принимает предикат в качестве параметра. Глядя на приведенный выше код, я могу определить логику, необходимую для создания предиката. Мы хотим регистрировать только те галактики, которые находятся в MDC. Поскольку эта логика, похоже, может использоваться в нескольких местах, я добавляю Предикат в перечисление как статическую функцию.
public enum Galaxy { ANDROMEDAE("andromedae"), ANTENNAE("Antennae"), EYE_OF_SAURON("eyeOfSauron"), MEDUSA_MERGER("medusaMerger"); private String key; // constructor, other methods, and getter removed for brevity... public static finaPredicateisInMDC = key -> MDC.get(key.getKey()) != null; }
Теперь, когда у меня есть предикат, я могу использовать его в фильтре.
Galaxy.stream() .filter(Galaxy.isInMDC) .forEach(g -> logStarToGalaxy(foundStar, g));
Итак, мы пошли от этого:
for (final Galaxy g :: Galaxy.values()) { final String gal = MDC.get(g.getKey()) if(gal != null) { logStarToGalaxy(foundStar, gal); } }
к этому:
Galaxy.stream() .filter(Galaxy.isInMDC) .forEach(g -> logStarToGalaxy(foundStar, g))
Этот код понятен и прост для понимания. Этот код является “декларативным” (сообщает компьютеру, что мы хотим, чтобы он делал), и мы удалили оператор if с помощью фильтра потока и предиката. У нас также есть преимущество в том, что мы можем повторно использовать эту функцию предиката везде, где еще нам нужно проверить MDC.
Приведенный выше код является лишь одним из примеров использования функциональных интерфейсов. Функциональные интерфейсы также позволяют нам составлять функции вместе, помогая нам создавать наш код в виде небольших, повторно используемых блоков, которые в конечном итоге являются декларативными по стилю. Приведенная выше информация является лишь кратким описанием функционального интерфейса Java, и есть гораздо больше, в чем вы можете погрузиться с помощью функциональных интерфейсов. Вы можете видеть, что они тесно связаны с использованием лямбд и широко используются в потоках. Я призываю вас изучить эти концепции, пока вы изучаете функциональные интерфейсы. Если у вас есть какие-либо другие полезные советы по функциональному программированию на Java или предложения о том, как улучшить приведенный выше код, оставьте комментарий и присоединяйтесь к разговору!
Оригинал: “https://dev.to/phouchens/exploring-functional-programming-in-java-functional-interfaces-1phe”