Привет, Хабр!
В этой статье я опишу функциональное программирование и расскажу, как оно реализовано в Java. Помимо вопроса «что это?», я постараюсь ответить на вопросы «зачем?», «когда?» и «как?» это используется.
Итак, в программировании есть две популярные парадигмы: императивная и декларативная.
Императивный подход позволяет программисту давать компьютеру инструкции в виде выражений, которые могут изменить состояние данных. В императивной парадигме есть языки программирования, относящиеся к процедурной (C, Fortran) и объектно-ориентированной (C++, C#, Java) категориям. Процедуры содержат выражения (вычисления), которые выполняются последовательно. Объектно-ориентированная модель организована вокруг данных, представленных в виде объектов (классов).
Декларативный подход позволяет программисту декларировать желаемый результат вычислений, но не описывать, как выполнять эти вычисления. В декларативной парадигме есть языки программирования, относящиеся к логической (Prolog) и функциональной (JS, Scala, Groovy) категориям. В функциональной программе вычисления основаны на выполнении цепочки функций.
Всем известный пример декларативного подхода — это SQL.
Язык Java создавался как объектно-ориентированный, но с 8 версии (2014 г.) к объектно-ориентированной модели добавилась функциональная. Таким образом Java совмещает в себе императивную и декларативную парадигмы.
Функциональное программирование (ФП) предлагает такие понятия, как:
Иммутабельность данных. Состояние данных не изменяется, что позволяет сделать результаты выполнения программы гораздо более предсказуемыми и достичь ссылочной прозрачности (referencial transparancy). В ФП ссылочная прозрачность обычно означает, что выражение в коде может быть заменено результатом выполнения этого выражения, при этом результаты выполнения всего кода не изменятся. Иммутабельность данных позволяет избежать гонки данных (race condition) в многопоточной среде. Если же значение передается в виде аргумента в функцию, то оно останется неизменным, а изменения в функции происходят с его копией.
Чистая функция (pure function). При одних и тех же входных данных функция возвращает один и тот же результат. Понятие «чистая функция» появилось в компьютерной инженерии, в математике такого понятия нет. В математике функция всегда при одних и тех же входных данных возвращает один и тот же результат. Функция, которая может возвращать разные результаты при одних и тех же аргументах, это скорее процедура, а не функция. Чистая функция не имеет побочных эффектов (side effects). Побочные эффекты возникают, когда функция использует для вычислений или изменяет значения за пределами своего тела.
Функции как объект первого класса (first class citizens). Функция может быть передана в виде аргумента в другую функцию и/или возвращена другой функцией как значение. То есть функция в ФП может использоваться как объект или переменная.
Не все фичи ФП достаточно хорошо поддерживаются в Java, например рекурсия. В Java так же непросто поддерживать иммутабельность данных, например, если в классе есть список (List) и этот список, в свою очередь, содержит еще списки.
Поддержка ФП в Java:
Функциональные интерфейсы (интерфейсы, у которых только один абстрактный метод). Функциональные интерфейсы имплементируются в виде анонимных функций (лямбда-выражений).
Лямбда-выражения. До Java 8 для реализации интерфейсов использовались анонимные классы, представляющие собой громоздкую и трудночитаемую конструкцию кода. Лямбда-выражения упрощают имплементацию интерфейсов, у которых только один абстрактный метод, значительно уменьшают размер кода и делают его более читабельным.
Функции высшего порядка (Higher order functions). Функции высшего порядка принимают в виде аргументов и/или возвращают как значения другие функции. Такие функции представлены, например, в Stream API, CompletableFuture, Optional.
Вот несколько простых примеров «декларативное vs. императивное программирование».
Получить минимальный элемент списка.
List<Integer> numbers = List.of(5, 8, 4, -50, 100, 28, 99, -13, 68);
Императивный подход:
if (numbers == null || numbers.isEmpty()) { throw new IllegalArgumentException("List must not be null or empty."); } int min = numbers.get(0); for (int number : numbers) { if (number < min) { min = number; } }
Декларативный подход:
В функцию min() нужно передать компаратор:
Comparator<Integer> comparator = new Comparator<>() { @Override public int compare(Integer n1, Integer n2) { return n1.compareTo(n2); } }; Integer min = numbers.stream().min(comparator) .orElseThrow(() -> new IllegalArgumentException( "List must not be null or empty."));
Компаратор заменяем лямбдой:
Integer min = numbers.stream() .min((n1, n2) -> n1.compareTo(n2)) .orElseThrow(() -> new IllegalArgumentException( "List must not be null or empty."));
Или так:
Integer min = numbers.stream() .min(Comparator.naturalOrder()) .orElseThrow(() -> new IllegalArgumentException( "List must not be null or empty."));
Чтобы получить максимальное число, достаточно просто:
Integer max = numbers.stream() .max(Comparator.naturalOrder()) .orElseThrow(() -> new NoSuchElementException( "List must not be null or empty."));
Нужно из последовательности целых чисел выбирать четные до тех пор, пока не попадется нечетное.
List<Integer> numbers = List.of(2, 4, 6, 8, 10, 3, 12, 14, 16, 18);
Императивный подход:
List<Integer> evenNumbers = new ArrayList<>(); for (Integer number : numbers) { if (number % 2 == 0) { evenNumbers.add(number); } else { break; } }
Декларативный подход:
List<Integer> result = numbers.stream() .takeWhile(n -> n % 2 == 0) .collect(Collectors.toList());

Или нужно отбрасывать четные числа, пока не попадется нечетное.
Императивный подход:
boolean oddFound = false; for (Integer number : numbers) { if (!oddFound && number % 2 == 0) { continue; } else { oddFound = true; result.add(number); } }
Декларативный подход:
List<Integer> result = numbers.stream() .dropWhile(n -> n % 2 == 0) .collect(Collectors.toList());

Являются ли ВСЕ числа четными?
Императивный подход:
boolean allEven = true; for (Integer number : numbers) { if (number % 2 != 0) { allEven = false; break; } else { allEven = true; } }
Декларативный подход:
boolean allEven = numbers.stream() .allMatch(n -> n % 2 == 0);
Есть ли хоть одно нечетное число?
Императивный подход:
boolean hasOdd = true; for (Integer number : numbers) { if (number % 2 != 0) { hasOdd = true; break; } else { hasOdd = false; } }
Декларативный подход:
boolean hasOdd = numbers.stream() .anyMatch(n -> n % 2 != 0);
Функция и метод — это одно и то же?
Нет, функция это не то же самое, что и метод, в ООП. Несмотря на то, что функция может использоваться как метод, а метод может использоваться как функция, метод в ООП привязан к классу или экземпляру класса. Функция же — это независимая единица программы, на которую можно ссылаться без привязки к классу. Для того, чтобы вызвать метод, нужно создать экземпляр класса, либо (статический) метод может быть доступен через сам класс. Функция не зависит от внешних значений (переменных, объектов) кроме собственных параметров, в то время как метод, помимо собственных параметров, может зависеть от других атрибутов экземпляра класса. Методы могут изменять значения своих параметров, а (чистые) функции не могут изменить ни свои аргументы, ни другие внешние значения. То есть чистая функция не имеет побочных эффектов. Функция может только использовать свои параметры для того, чтобы создавать новые значения (объекты). Функция, в отличие от метода, принимает параметр, создает его копию, вносит изменения в эту копию, но оригинальный объект остается неизмененным. Функции также могут быть объединены в цепочку функций независимо от контекста и эту цепочку можно вынести в отдельный объект (функцию).
Перейдем к более практичному и сложному примеру. Есть такая сущность:
@Getter @Setter @RequiredArgsConstructor public class Car { private final String brand; private final String model; private final Integer year; private final String color; private final Double price; }
и список:
List<Car> cars = new ArrayList<>(Arrays.asList( new Car("Toyota", "Corolla", 2020, "Red", 20000.0), new Car("Honda", "Civic", 2019, "Blue", 18000.0), new Car("Lexus", "LS", 2021, "Black", 70000.0), new Car("Audi", "Quattro", 2022, "Indigo", 94000.0), new Car("Kia", "Rio", 2018, "White", 30000.0), new Car("Lamborghini", "Diablo", 2020, "Silver", 99600.0), new Car("Volvo", "XC90", 2021, "Gray", 33000.0), new Car("Bentley", "Azure", 2017, "Khaki", 86000.0), new Car("Hyundai", "Elantra", 2022, "Blue", 22000.0), new Car("Volkswagen", "Jetta", 2019, "Red", 21000.0) ));
Вот пример императивного решения: как найти первые три бренда дороже 50000:
List<String> expensiveCarBrands = new ArrayList<>(); for (Car car : cars) { if (car.getPrice() > 50000) { String brand = car.getBrand(); if (!expensiveCarBrands.contains(brand) && expensiveCarBrands.size() < 3) { expensiveCarBrands.add(brand); } } }
Здесь создается новый объект List<String> expensiveCarBrands, цикл, который последовательно проходит каждую строку в списке. На каждой итерации выполняется проверка нескольких условий, и если условия соблюдены, название бренда добавляется в результирующий список.
Такой подход может быть удобен в написании простых программ, но в многопоточной среде приведет к гонке данных (data race), когда разные потоки получают доступ к списку expensiveCarBrands. Создание мутабельного объекта expensiveCarBrands — это реализация анти-паттерна «Аккумулятор». Этот код очень трудно распараллелить. К тому же реализация происходит на уровне отдельно взятых элементов списка, а не на уровне всей коллекции элементов. Все это приводит к появлению багов.
Для устранения вышеупомянутых проблем в Java и было введено функциональное программирование Stream API:
List<String> expensiveCarBrands = cars.stream() .filter(car -> car.getPrice() > 50000) .map(car -> car.getBrand()) .distinct() .limit(3) .collect(Collectors.toList())
Это как SQL запрос к таблице cars.

Здесь список преобразуется в стрим (поток элементов, над которыми можно выполнять операции). Стрим представляет абстракцию, общий план того, что нужно сделать, а не детали реализации, такие как циклы, условия, ветвления и т.п. Стримы чаще всего используются с коллекциями (или файлами). Затем к стриму применяются операции фильтрации и трансформации. Далее следует терминальная операция collect, которая завершает стрим и трансформирует результат в список, который будет возвращен методом. Терминальная операция — это переход от абстракции к конкретике, в данном случае возвращает список. Таким образом, мы получили цепочку вызовов функций.
Декларативный подход концентрирует внимание на том, что нужно сделать, а не как. Поэтому код получается короче и более читабельным.
Функции filter, map и collect являются функциями высшего порядка.
Функция filter принимает предикатив (Predicate), который может быть представлен в виде метода:
boolean priceOver50000(Car car) { return car.getPrice() > 50000; }
Такой метод нельзя передать в виде аргумента в функцию, поэтому в Java есть эквивалент, который так же принимает значение и возвращает true или false. Это функциональный интерфейс Predicate, у которого единственный абстрактный метод boolean test(T t):

Этот интерфейс можно реализовать в виде анонимного класса:
Predicate<Car> priceOver50000 = new Predicate<>() { @Override public boolean test(Car car) { return car.getPrice() > 50000; } }
Его можно заменить лямбда-выражением:
Predicate<Car> priceOver50000 = car -> car.getPrice() > 50000
Эту реализацию можно передать в функцию:
filter(priceOver50000)
Либо можно вообще не создавать объект priceOver50000, а просто передать лямбду:
filter(car -> car.getPrice() > 50000)
Функция map принимает функцию (Function):

Она принимает Car и возвращает String(бренд):
Function<Car, String> brandName = car -> car.getBrand()
Лямбда передается в функцию:
map(car -> car.getBrand())
Можно ли сделать код еще короче и лаконичнее? Да, лямбду можно заменить ссылкой на метод:
map(Car::getBrand)
Функция collect() принимает Supplier, который не принимает аргументов и, как правило, используется для ленивой генерации значений. В данном случае Supplier генерирует список (Collectors.toList()).
Исходный список cars остается неизмененным!
Чтобы распараллелить обработку списка, достаточно заменить stream() на parallelStream():
List<String> expensiveCarBrands = cars.parallelStream() .filter(car -> car.getPrice() > 50000) .map(car -> car.getBrand()) .distinct() .limit(3) .collect(Collectors.toList())
Этот код легко распараллелить потому, что он не имеет состояния (stateless).
В многопоточной среде, когда множество потоков в один момент времени могут получать доступ на чтение и/или запись к одному объекту, приходится использовать такие инструменты как ключевое слово synchronized, семафоры, замки и т.п. Потом приходится думать, откуда берутся дедлоки, голодание потоков, неожиданные результаты вычислений. С внедрением в Java функционального подхода происходит отход от изменяемых данных (mutability) и попытка решить вышеперечисленные проблемы.
Попробуем распараллелить вычисление факториала от 1000 и сравним по производительности с последовательным вычислением.
Функция последовательного вычисления:
public class SequentialStreamFactorial { public static BigInteger factorial(BigInteger n) { return LongStream .rangeClosed(1, n.longValue()) .mapToObj(BigInteger::valueOf) .reduce(BigInteger.ONE, // Начальное значение. BigInteger::multiply); // Аккумулятор. } }
Функция параллельного вычисления:
public class ParallelStreamFactorial { public static BigInteger factorial(BigInteger n) { return LongStream .rangeClosed(1, n.longValue()) .parallel() .mapToObj(BigInteger::valueOf) .reduce(BigInteger.ONE, // Начальное значение. BigInteger::multiply, // Аккумулятор. BigInteger::multiply); // Комбинатор. } }
Здесь стрим разбивается на подстримы. Добавлен третий аргумент в виде функции, которая комбинирует результаты вычислений подстримов.
В главном классе метод runTest() принимает во втором параметре соответствующую функцию factorial(), выполняет ее с параметром n и замеряет время выполнения для каждого алгоритма:
public class Example { private static final int MAX_ITERATIONS = 20_000; private static final int DEFAULT_N = 1_000; public static void main(String[] args) { final BigInteger n = BigInteger.valueOf(DEFAULT_N); runTest("Последовательное вычисление.", SequentialStreamFactorial::factorial, n); runTest("Параллельное вычисление.", ParallelStreamFactorial::factorial, n); } private static <T> void runTest(String algorithmName, Function<T, T> factorialAlgorithm, T n) { long startTime = System.currentTimeMillis(); T result = factorialAlgorithm.apply(n); long endTime = System.currentTimeMillis(); long duration = endTime - startTime; System.out.println(algorithmName + " Факториал числа " + n + " = \n" + result + "\n" + " (Время: " + duration + " мс)"); } }
Результат выполнения может быть примерно таким:

Можно выполнить каждую функцию по 20 тысяч раз:
private static <T> void runTest(String algorithmName, Function<T, T> factorialAlgorithm, T n) { long startTime = System.currentTimeMillis(); IntStream .range(0, MAX_ITERATIONS) .forEach(i -> factorialAlgorithm.apply(n)); long endTime = System.currentTimeMillis(); long duration = endTime - startTime; System.out.println(algorithmName + " Факториал числа " + n + " = \n" + factorialAlgorithm.apply(n) + " = \n" + " (Время: " + duration + " мс)"); }
Вывод примерно такой:

Как видно из вывода, параллелизм кратно сокращает время выполнения
Еще примеры со списком автомобилей
Получить среднюю цену автомобилей:
double averagePrice = cars.stream() .mapToDouble(car -> car.getPrice()) .average() .orElse(0.0)
SQL-эквивалент:

Сколько автомобилей, произведенных до 2020 года?
long count = cars.stream() .filter(car -> car.getYear() < 2020) .count();
SQL:

Самая низкая цена на автомобиль (здесь не используется Comparator):
double minPrice = cars.stream() .mapToDouble(Car::getPrice) .min() .orElse(0);
SQL:

В пакете java.util.function представлено множество функциональных интерфейсов, включая специализации для примитивов, такие как ToIntFunction, ToLongFunction, ToDoubleFunction, которые конвертируют свой параметр в указанный примитивный тип. Например:
ToIntFunction<Double> doubleToInt = Double::intValue; int result = doubleToInt.applyAsInt(5.3); System.out.println("Double to int: " + result);
IntFunction, LongFunction, DoubleFunction конвертируют указанный примитив в тип параметра. Например:
IntFunction<String> intToString = (value) -> "int to String: " + value; String result = intToString.apply(10); System.out.println(result);
Для Function, Predicate и Consumer, которые принимают один параметр, есть версии с двумя параметрами: BiFunction<T,U,R>, BiPredicate<T,U>, BiConsumer<T,U>. Но что если нам нужен функциональный интерфейс, который принимает три параметра или больше? Тогда придется создать кастомный. Для начала приведу пример, как Supplier может служить в качестве фабрики объектов.
Есть Runnable-класс с конструктором без параметров и конструктором с тремя параметрами, который выдает некое значение:
public class PrintValue implements Runnable { String string; /** Конструктор по умолчанию. / PrintValue() { string = "Ноль"; } /** Конструктор с тремя параметрами. / public PrintValue(String s, Integer i, Long l) { string = s + i + l; } @Override public void run() { System.out.println(string); } }
Используем Supplier как фабрику объектов с вызовом конструктора без параметров:
public class Example { public static void main(String[] args) { zeroParamConstructorRef(); } private static void zeroParamConstructorRef() { System.out.println("zeroParamConstructorRef()"); Supplier factory = PrintValue::new; PrintValue printValue = factory.get(); printValue.run(); } }

Чтобы вызвать конструктор с тремя параметрами, создадим кастомный интерфейс, который принимает аргументы P1, P2, P3 и создает объект типа R. У этого интерфейса один абстрактный метод of():
@FunctionalInterface interface TriFactory<P1, P2, P3, R> { R of(P1 p1, P2 p2, P3 p3); }
Создаем фабрику для конструктора с тремя параметрами:
public static void threeParamConstructorRef() { System.out.println("threeParamConstructorRef()"); TriFactory<String, Integer, Long, PrintValue> factory = PrintValue::new; factory.of("Значение равно ", 4, 2L).run(); }
Выполняем:
public static void main(String[] args) { zeroParamConstructorRef(); threeParamConstructorRef(); }

Аналогия с callback-функциями в Javascript
В Javascript callback-функция — это функция, которая передается в другую функцию в качестве аргумента. Функция hello(), которая принимает имя, фамилию и (callback) функцию и выводит имя и фамилию в консоль. Если фамилия не указана, то выполняется callback-функция, переданная в третьем параметре. Вот результат выполнения функции в консоли браузера:

В качестве аргумента здесь передается function() { console.log(«Фамилия не указана») }.
Если фамилия во втором аргументе указана, то callback не вызывается:

То же самое можно сделать и в Java:

Метод hello() имеет параметр типа Consumer<String>, а при вызове метода передается реализация функционального интерфейса Consumer в виде лямбды:
value -> { System.out.println(String.format("Для %s фамилия не указана", value)); });
Consumer, по сути, это void-функция, которая принимает аргумент, но не возвращает никакого значения.
Цепочки функций
Функции, как переменной, можно присвоить метод как значение и составлять цепочки вызовов. Вот пример кода, который генерирует HTML-теги:
public class Example { static public class HtmlTagMaker { static String addLessThan(String text) { return "<" + text; } static String addGreaterThan(String text) { return text + ">"; } } public static void main(String[] args) { Function<String, String> lessThan = HtmlTagMaker::addLessThan; Function<String, String> tagger = lessThan .andThen(HtmlTagMaker::addGreaterThan); String html = tagger.apply("HTML") + tagger.apply("BODY") + tagger.apply("/BODY") + tagger.apply("/HTML"); System.out.println(html); } }
Вывод:

Если выполняется цепочка функций, то используется ли результат выполнения предыдущей функции как входной аргумент для следующей?
Есть Consumer, который принимает дату, инкрементирует ее на один день и выводит в консоль:
Consumer<LocalDate> incrementAndPrint = date -> System.out.println(date.plusDays(1));
Другой Consumer принимает дату, форматирует ее по заданному шаблону и выводит в консоль:
Consumer<LocalDate> printDate = date -> { DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM/dd/yyyy EEEE"); System.out.println(date.format(formatter)); }
Если построить цепочку вызовов из этих двух функций
incrementAndPrint .andThen(printDate) .accept(LocalDate.of(2023, 12, 31));
то получится, что каждая из них принимает на вход исходный объект LocalDate, а не результат выполнения предыдущей:

Примеры использования функциональных интерфейсов в JDK
Класс java.util.Map.

Map<String, Integer> nameMap = new HashMap<>(); Integer value = nameMap.computeIfAbsent("John", s -> s.length());
Функция выполняется, если по ключу «John» не найдено значение. Значению присваивается результат выполнения функции.
Map<String, Integer> iqMap = new HashMap<>(); iqMap.put("Bill", 160); iqMap.put("Warren", 150); iqMap.put("Steve", 140);
iqMap.replaceAll((k, v) -> v + 10);
BiFunction инкрементирует каждое значение мапы на 10
Класс java.util.List.

List<String> names = Arrays.asList("bill", "warren", "steve"); names.replaceAll(String::toUpperCase);
Унарный оператор заменяет все буквы на заглавные.
Класс Optional
Чтобы исключить NullPointerException, в Java 8 появился класс Optional. Объект Optional можно представить как контейнер, который может содержать в себе значение, а может быть пустым, но не null. В классе Optional есть методы, которые явно обрабатывают отсутствие или наличие значения. Эти методы принимают в виде аргументов функциональные интерфейсы.
Например метод orElseThrow() принимает Supplier, который генерирует исключение:
Object value = Optional.ofNullable(null) .orElseThrow(() -> new IllegalStateException("Exception"))
Supplier может быть извлечен в переменную:
Supplier<IllegalStateException> exception = () -> new IllegalStateException("Exception"); Object value = Optional.ofNullable(null) .orElseThrow(exception);
Метод orElseGet() принимает Supplier, который предоставляет строковое значение по умолчанию:
Object value = Optional.ofNullable(null) .orElseGet(() -> "Default value"); System.out.println(value);

Метод ifPresent() использует Consumer:
Optional.ofNullable("john@mail.com") .ifPresent(email -> System.out.println( String.format("Отправлено сообщение на %s", email)));

Если передать null:
Optional.ofNullable(null) .ifPresent(email -> System.out.println( String.format("Отправлено сообщение на %s", email)));
то не будет выведено ничего.
Метод ifPresentOrElse() использует Consumer и Runnable:


Ни один метод Stream API не может вернуть null. Такие методы, как reduce(), min(), max(), findFirst(), findAny(), возвращают Optional, и разработчику не нужно выполнять проверки на null такого вида:
if (value != null) { ... }
Также разработчик не может забыть предусмотреть потенциальную возможность отсутствия значения и при использовании Optional ему придется реализовать логику на случай, если вычисление даст пустое значение.
В заключении приведу пример шаблона «Комбинатор», который взял отсюда
https://www.youtube.com/watch?v=VRpHdSFWGPs&t=5583s
Итак, есть сущность Customer (клиент, заказчик) с полями (имя, эл. почта, телефон и дата рождения):
@Getter @Setter @RequiredArgsConstructor public class Customer { private final String name; private final String email; private final String phone; private final LocalDate dob; }
Есть сервис валидации введенных данных о заказчике:
public class CustomerValidationService { private boolean isEmailValid(String email) { return email.contains("@"); } private boolean isPhoneValid(String phone) { return phone.startsWith("+7"); } private boolean isAdult(LocalDate dob) { return Period.between(dob, LocalDate.now()).getYears() > 16; } public boolean isValid(Customer customer) { return isEmailValid(customer.getEmail()) && isPhoneValid(customer.getPhone()) && isAdult(customer.getDob()); } }
Здесь реализация валидации эл. почты и телефона упрощена, т.к. не является темой статьи.
В данной реализации сервиса, если валидация не прошла (isValid вернул false), неясно, какое поле не прошло валидацию (имя, эл. почта, телефон или дата рождения).
public class Main { public static void main(String[] args) { Customer customer = new Customer( "Helen", "+7234765999", "helen@mail.com", LocalDate.of(2000, 1, 1)); System.out.println( new CustomerValidationService().isValid(customer) ); } }
Чтобы протестировать невалидный ввод, можно убрать «@» из эл. почты или «+» из номера телефона. В коде выше я просто поменял местами аргументы в вызове конструктора, и, соответственно, эл. почта и телефон не проходят валидацию.
Вывод

Создадим перечисление:
public enum ValidationResult { SUCCESS, EMAIL_IS_NOT_VALID, PHONE_IS_NOT_VALID, IS_NOT_ADULT; }
И интерфейс:
public interface CustomerRegistrationValidator extends Function<Customer, ValidationResult> { static CustomerRegistrationValidator isEmailValid() { return customer -> customer.getEmail().contains("@") ? SUCCESS : EMAIL_IS_NOT_VALID; } static CustomerRegistrationValidator isPhoneValid() { return customer -> customer.getPhone().startsWith("+7") ? SUCCESS : PHONE_IS_NOT_VALID; } static CustomerRegistrationValidator isAdult() { return customer -> Period.between(customer.getDob(), LocalDate.now()).getYears() > 16 ? SUCCESS : IS_NOT_ADULT; } default CustomerRegistrationValidator and( CustomerRegistrationValidator other) { return customer -> { ValidationResult result = this.apply(customer); return result.equals(SUCCESS) ? other.apply(customer) : result; }; } }
Эта реализация функционального интерфейса Function имеет входной параметр типа Customer и возвращает значение типа ValidationResult.
В главном классе используем default-метод and для построения цепочки вызовов:
import static com.combinator.CustomerRegistrationValidator.*; import static com.combinator.ValidationResult.SUCCESS; public class Main { public static void main(String[] args) { Customer customer = new Customer( "Helen", "+7234765999", "helen@mail.com", LocalDate.of(2000, 1, 1)); ValidationResult result = isEmailValid() .and(isPhoneValid()) .and(isAdult()) .apply(customer); if(!result.equals(SUCCESS)) { throw new IllegalStateException(result.name()); } } }
Пример вывода:

Заключение
При использовании Stream API разработчик может сократить количество строчек кода в 2-3 раза. Использование функциональных интерфейсов делает код гораздо чище и читабельнее. В целом, хорошее знание функционального подхода в Java существенно повысит ваш уровень как разработчика.
ссылка на оригинал статьи https://habr.com/ru/articles/892242/
Добавить комментарий