Функциональное программирование стало неотъемлемой частью экосистемы Java благодаря появлению в Java 8 лямбд, потоков и функциональных интерфейсов Predicate и Function. С этими концепциями пишется более выразительный, модульный и переиспользуемый код. Даже опытными программистами, так называемыми сеньорами, этот новый подход освоен не полностью, в их коде не раскрывается весь его потенциал.

Рассмотрим, как создается динамический класс с Predicate и Function для валидации и преобразования данных. И продемонстрируем, как эти интерфейсы используются со спецификациями JPA, коллекциями и потоками для реальных приложений.

Что такое Predicate и Function?

Predicate

Predicate  —  это функциональный интерфейс, которым вычисляется логическое условие по одному аргументу.

  • Сигнатура: boolean test(T t).
  • Стандартное применение: фильтры, валидации, удаление элементов из коллекций.

Function

Function  —  это функциональный интерфейс, которым вводимое значение преобразуется выводимое.

  • Сигнатура: R apply(T t).
  • Стандартное применение: преобразование данных, сопоставление в потоках.

Настройка процессов при помощи Predicate и Function

В примере ниже продемонстрируем, как Predicate и Function используются с перечислениями для моделирования реальной бизнес-логики, определим разновидности кредитных карт  —  GOLD, BLACK и PLATINUM  —  и применим правила для:

  1. Проверки с помощью Predicate, принята ли транзакция.
  2. Подсчета с помощью Function баллов лояльности за транзакции.

Перечисление CreditCardType:

import java.math.BigDecimal;
import java.util.function.Function;
import java.util.function.Predicate;

public enum CreditCardType {
GOLD {
@Override
Function<BigDecimal, BigDecimal> formulaPoints() {
return amount -> amount.multiply(new BigDecimal("0.10"));
}

@Override
Predicate<BigDecimal> acceptAmount() {
return amount -> amount.compareTo(new BigDecimal("1000")) <= 0;
}
},
BLACK {
@Override
Function<BigDecimal, BigDecimal> formulaPoints() {
return amount -> amount.multiply(new BigDecimal("0.20"));
}

@Override
Predicate<BigDecimal> acceptAmount() {
return amount -> amount.compareTo(new BigDecimal("5000")) <= 0;
}
},
PLATINUM {
@Override
Function<BigDecimal, BigDecimal> formulaPoints() {
return amount -> amount.multiply(new BigDecimal("0.50"));
}

@Override
Predicate<BigDecimal> acceptAmount() {
return amount -> amount.compareTo(new BigDecimal("10000")) <= 0;
}
};

abstract Function<BigDecimal, BigDecimal> formulaPoints();
abstract Predicate<BigDecimal> acceptAmount();
}

Пример использования: валидация и подсчет баллов лояльности

public class Main {
public static void main(String[] args) {
calculatePoints(CreditCardType.GOLD, new BigDecimal("1000"));
calculatePoints(CreditCardType.BLACK, new BigDecimal("1000"));
calculatePoints(CreditCardType.PLATINUM, new BigDecimal("1000"));

calculatePoints(CreditCardType.GOLD, new BigDecimal("5000"));
calculatePoints(CreditCardType.BLACK, new BigDecimal("5000"));
calculatePoints(CreditCardType.PLATINUM, new BigDecimal("5000"));

calculatePoints(CreditCardType.GOLD, new BigDecimal("10000"));
calculatePoints(CreditCardType.BLACK, new BigDecimal("10000"));
calculatePoints(CreditCardType.PLATINUM, new BigDecimal("10000"));
}

private static void calculatePoints(CreditCardType creditCardType, BigDecimal amountTransaction) {
System.out.println(creditCardType);
if (creditCardType.acceptAmount().test(amountTransaction)) {
System.out.println("Transaction accepted!");
System.out.println("Bonus points: " + creditCardType.formulaPoints().apply(amountTransaction));
} else {
System.out.println("Transaction rejected!");
}
System.out.println("------------------------------");
}
}

Объяснение

  1. Перечисление CreditCardType:
  • Каждой разновидностью кредитной карты определяются собственные правила для:
  • acceptAmount(): это Predicate, которым проверяется соответствие суммы транзакции критериям карты.
  • formulaPoints(): это Function, которым по сумме транзакции подсчитываются баллы лояльности.

2. Валидация с Predicate:

  • Методом acceptAmount() проверяется допустимость транзакции для разновидности карты. Например:
  • GOLD принимаются транзакции суммой до 1000;
  • BLACK  —  до 5000;
  • PLATINUM  —  до 10 000.

3. Преобразование с Function:

  • Методом formulaPoints() подсчитываются баллы лояльности за принятые транзакции. Например:
  • GOLD: 10 % от суммы транзакции;
  • BLACK: 20 % от суммы транзакции;
  • PLATINUM: 50 % от суммы транзакции.

4. Метод calculatePoints:

  • Им при помощи Predicate проверяется допустимость транзакции.
  • Если она допустима, при помощи Function подсчитываются и отображаются баллы лояльности.

Пример вывода

GOLD
Transaction accepted!
Bonus points: 100.0
------------------------------
BLACK
Transaction accepted!
Bonus points: 200.0
------------------------------
PLATINUM
Transaction accepted!
Bonus points: 500.0
------------------------------
GOLD
Transaction rejected!
------------------------------
BLACK
Transaction accepted!
Bonus points: 1000.0
------------------------------
PLATINUM
Transaction accepted!
Bonus points: 2500.0
------------------------------
GOLD
Transaction rejected!
------------------------------
BLACK
Transaction rejected!
------------------------------
PLATINUM
Transaction accepted!
Bonus points: 5000.0
------------------------------

Преимущества Predicate и Function

  1. Модульность:
  • Каждой разновидностью карты инкапсулируется собственная логика валидации и подсчета.
  • Легко добавляются новые разновидности карт с уникальными правилами.

2. Переиспользуемость:

  • Интерфейсами Predicate и Function предоставляется переиспользуемая логика валидации и преобразования.

3. Удобство восприятия:

  • В разновидностях карт используются перечисления, поэтому логика структурирована и легко отслеживается.

4. Расширяемость:

  • Чтобы добавить новые правила для разновидностей карт, достаточно реализовать абстрактные методы.

Реальные сценарии

1. Спецификации JPA

В Spring Data JPA Predicate важен при построении динамических запросов с помощью спецификаций, он применяется интерфейсом Specification для определения фильтров запросов:

import org.springframework.data.jpa.domain.Specification;
import javax.persistence.criteria.*;

public class CustomerSpecification {
public static Specification<Customer> hasName(String name) {
return (Root<Customer> root, CriteriaQuery<?> query, CriteriaBuilder builder) ->
builder.equal(root.get("name"), name);
}

public static Specification<Customer> hasAgeGreaterThan(int age) {
return (Root<Customer> root, CriteriaQuery<?> query, CriteriaBuilder builder) ->
builder.greaterThan(root.get("age"), age);
}
}

Пример использования:

Specification<Customer> spec = CustomerSpecification.hasName("John")
.and(CustomerSpecification.hasAgeGreaterThan(25));

List<Customer> customers = customerRepository.findAll(spec);

Так Predicate-подобным поведением динамически фильтруются запросы к базе данных.

2. Коллекции

Классами коллекций Java Predicate активно используется для фильтрации, а Function  —  для преобразования данных.

  • Фильтрация выполняется с помощью removeIf:
List<String> names = new ArrayList<>(List.of("Alice", "Bob", "Charlie"));
names.removeIf(name -> name.startsWith("B"));
System.out.println(names); // Вывод: [Alice, Charlie]
  • Преобразование  —  с помощью replaceAll:
List<String> names = new ArrayList<>(List.of("Alice", "Bob", "Charlie"));
names.replaceAll(name -> name.toUpperCase());
System.out.println(names); // Вывод: [ALICE, BOB, CHARLIE]

3. Потоки

Для таких операций, как объединение элементов в один результат, фильтрация и сопоставление, потоками активно применяются и Predicate, и Function.

  • Фильтрация с Predicate:
List<String> names = List.of("Alice", "Bob", "Charlie");
List<String> filteredNames = names.stream()
.filter(name -> name.startsWith("A"))
.toList();
System.out.println(filteredNames); // Вывод: [Alice]
  • Сопоставление при помощи Function:
List<String> names = List.of("Alice", "Bob", "Charlie");
List<Integer> nameLengths = names.stream()
.map(String::length)
.toList();
System.out.println(nameLengths); // Вывод: [5, 3, 7]

Этими методами коллекции обрабатываются декларативно, без циклов, а код делается более удобным для восприятия и выразительным.

Заключение

Predicate и Function  —  это фундаментальные инструменты функционального программирования на Java, которыми предоставляются выразительные способы валидации, фильтрации и преобразования данных.

От реальных сценариев вроде спецификаций JPA и потоковой обработки до асинхронных операций с CompletableFuture  —  благодаря этим интерфейсам разработчики пишут более чистый, модульный и переиспользуемый код.

Усовершенствуйте проектирование приложений, включив эти шаблоны в свои проекты. Освойте Predicate и Function и раскройте весь потенциал функционального программирования на Java.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Lucas Fernandes: Master Functional Programming in Java: Using Predicate and Function Elegantly

Предыдущая статьяКомпонентный подход: реализация экранов с помощью библиотеки Decompose. Часть 2
Следующая статьяКонтейнеризация проекта GO с Envoy