Занятие № 5

Функциональное программирование в Java

Петров Андрей

Обо мне

Программирую 8 лет, в СИС – 5 лет.

Руководитель группы разработки.

#angular #typescript #sping #java #postgresql

 

Увлечения:

  • Программирование
  • Настольные игры
  • Лазертаг

Roadmap

  • Основы ФП
  • Функции высшего порядка
  • Лямбда-функции
  • StreamAPI
  • Optional

Откуда пошло ФП

Алан Тьюринг

Машина Тьюринга

Алонзо Чёрч

Лямбда-исчисление

Знакомьтесь: функция

// f(x, y) = x + y * 2

int f(int x, int y) {
    return x + y * 2;
}

Как это нам поможет?

Отфильтруем список товаров по типу

List<Product> filterByType(
    List<Product> products, ProductType type
) {
    List<Product> result = new ArrayList<>();
    for (int i = 0; i < products.size(); i++) {
        Product product = products.get(i);
        if (product.getType() == type) {
            result.add(product);
        }
    }
    return result;
}

Недостатки приведенного кода

  • Много кода
  • Слишком сложно
List<Product> filterByType(
    List<Product> products, ProductType type
) {
    List<Product> result = new ArrayList<>();
    for (int i = 0; i < products.size(); i++) {
        Product product = products.get(i);
        if (product.getType() == type) {
            result.add(product);
        }
    }
    return result;
}
List<Product> filterByPrice(
    List<Product> products, int threshold
) {
    List<Product> result = new ArrayList<>();
    for (int i = 0; i < products.size(); i++) {
        Product product = products.get(i);
        if (product.getPrice() > threshold) {
            result.add(product);
        }
    }
    return result;
}
List<Product> filterByProducer(
    List<Product> products, String producer
) {
    List<Product> result = new ArrayList<>();
    for (int i = 0; i < products.size(); i++) {
        Product product = products.get(i);
        if (product.getProducer().equals(producer)) {
            result.add(product);
        }
    }
    return result;
}

Как можно улучшить?

Вариант цикла с foreach

List<Product> filterByType(
    List<Product> products, ProductType type
) {
    List<Product> result = new ArrayList<>();
    for (Product product : products) {
        if (product.getType() == type) {
            result.add(product);
        }
    }
    return result;
}

Класс-предикат

public interface Predicate {
    boolean match(Product value);
}
public class ProductTypePredicate implements Predicate {
    private ProductType type;

    ProductTypePredicate(ProductType type) {
        this.type = type;
    }

    @Override
    boolean match(Product product) {
        return product.getType() == this.type;
    }
}

Фильтрация с предикатом

// Метод фильтрации товаров
List<Product> filter(List<Product> products, Predicate predicate) {
    List<Product> result = new ArrayList<>();
    for (Product product : products) {
        // Меняем частную бизнес-логику на вызов предиката
        if (predicate.match(product)) {
            result.add(product);
        }
    }
    return result;
}

Функция высшего порядка

Пример фильтрации с предикатом

List<Product> filterProductsByType(
    List<Product> products, ProductType type
) {
    Predicate predicate = new ProductTypePredicate(type);
    return filter(products, predicate);
}

Убираем все лишнее

Вспомним предикат

public class ProductTypePredicate implements Predicate {
    private ProductType type;

    ProductTypePredicate(ProductType type) {
        this.type = type;
    }

    @Override
    boolean match(Product product) {
        return product.getType() == this.type;
    }
}

Идеально

List<Product> filterProductsByType(
    List<Product> products, ProductType type
) {
    return filter(products, item -> item.getType() == type);
}

Лямбда-функция

Predicate predicate = product -> item.getType() == type;
  • Аргументы
  • Разделитель
  • Тело

Однострочная лямбда

Лямбда с блоком

Predicate<Product> predicate = (Product item) -> {
    if (item.getPrice() > 100) {
        return item.getType() == type;
    }
    return false;
};

Было

List<Product> filterByType(
    List<Product> products, ProductType type
) {
    List<Product> result = new ArrayList<>();
    for (int i = 0; i < products.size(); i++) {
        Product product = products.get(i);
        if (product.getType() == type) {
            result.add(product);
        }
    }
    return result;
}
List<Product> filterByPrice(
    List<Product> products, int threshold
) {
    List<Product> result = new ArrayList<>();
    for (int i = 0; i < products.size(); i++) {
        Product product = products.get(i);
        if (product.getPrice() > threshold) {
            result.add(product);
        }
    }
    return result;
}
List<Product> filterByProducer(
    List<Product> products, String producer
) {
    List<Product> result = new ArrayList<>();
    for (int i = 0; i < products.size(); i++) {
        Product product = products.get(i);
        if (product.getProducer().equals(producer)) {
            result.add(product);
        }
    }
    return result;
}

Стало

List<Product> filterByType(
    List<Product> products, ProductType type
) {
    return filter(products, product -> product.getType() == type);
}

List<Product> filterByPrice(List<Product> products, int threshold) {
    return filter(products, product -> product.getPrice() > threshold);
}

List<Product> filterByProducer(List<Product> products, String producer) {
    return filter(products, product -> product.getProducer().equals(producer));
}

Постоянно писать функции

высшего порядка?

Ответ – StreamAPI!

Не те Stream, а эти Stream

Что такое Stream?

Поток, выполняющий операции над элементами коллекции

Трансформация элементов

map: принимает на вход функцию трансформации элемента коллекции, применяет ее ко всем элементам

List<ProductType> getUniqueProductTypes(List<Product> products) {
    return products.stream()
                   .map(product -> product.getType())
                   .distinct()
                   .collect(Collectors.toList());
}

Фильтрация элементов

List<Product> filterProductsByType(
    List<Product> products, ProductType type
) {
    return products.stream()
                   .filter(item -> item.getType() == type)
                   .collect(Collectors.toList());
}

filter: аналог нашего метода фильтрации, принимает предикат

Как получить результат работы потока?

Два типа методов Stream. Нетерминальные.

Добавляют новую операцию обработки потока, возвращают новый поток

Примеры:

  • filter
  • map
  • flatMap
  • skip
  • limit

Два типа методов Stream. Терминальные.

Завершают работу потока, выполняют все операции, формируют и возвращают результат работы потока

Примеры:​

  • findFirst
  • reduce
  • collect
  • forEach
  • toArray
  • allMatch
  • count

Плюсы использования StreamAPI

  • Декларативность: мы описываем, что мы хотим получить
  • Ленивость: нет вычислений, пока нам не понадобится результат
  • Лаконичность: пишем меньше кода, допускаем меньше низкоуровневых ошибок

Рассмотрим метод findFirst

Optional<T> findFirst();

Optional?

Optional – попытка решить проблему null

Напишем пару простых методов

Product findProductById(
    List<Product> products, long id
) {
    for (Product product : products) {
        if (product.getId() == id)
            return product;
    }
    return null;
}

double getPriceWithNDS(Product product) {
    double price = product.getPrice();
    return price * 1.2;
}

Используем их вместе

double getProductPriceWithNDS(
    List<Product> products, long productId
) {
    Product product = findProductById(products, productId);
    return getPriceWithNDS(product);
}

NullPointerException в рантайме, если товар с таким id не найден

Перепишем поиск на использование Stream

double getProductPriceWithNDS(
    List<Product> products, long productId
) {
    Optional<Product> product = products
        .stream()
        .filter(p -> p.getId() == productId)
        .findFirst();
    return getPriceWithNDS(product);
}

Ошибка компиляции

Optional – коробочка со значением

Как получить значение

double getProductPriceWithNDS(
    List<Product> products, long productId
) {
    Optional<Product> product = ...;
    if (product.isPresent()) {
        return getPriceWithNDS(product.get());
    } else {
        throw new RuntimeException("Не найден такой товар");
    }
}

Метод map

double getProductPriceWithNDS(
    List<Product> products, long productId
) {
    Optional<Double> price = products
        ...
        .findFirst()
        .map(this::getPriceWithNDS);
    if (price.isPresent()) {
        return price.get();
    } else {
        throw new RuntimeException("Не найден такой товар");
    }
}

Ссылка на метод: то же, что и product -> this.getPriceWithNDS(product)

Избавляемся от isPresent()

double getProductPriceWithNDS(
    List<Product> products, long productId
) {
    return products
        .stream()
        .filter(p -> p.getId() == productId)
        .findFirst()
        .map(this::getPriceWithNDS)
        .orElseThrow(
            () -> new RuntimeException("Не найден такой товар")
        );
}

А если без исключения?

double getProductPriceWithNDS(
    List<Product> products, long productId
) {
    return products
        .stream()
        .filter(p -> p.getId() == productId)
        .findFirst()
        .map(this::getPriceWithNDS)
        .orElse(0.0);
}

Что почитать

Домашняя работа

Предметная область: грузовые перевозки

Есть несколько видов транспорта, у каждого транспорта своя стоимость доставки груза и вместимость.

Есть груз, который нужно доставить, груз имеет список доступных путей (ж/д, морской, воздушный и т.д.)

 

Задача: для заданного груза рассчитать оптимальный по затратам способ доставки

JavaSIS #2.19 Занятие 5

By walkingdev

JavaSIS #2.19 Занятие 5

Функциональное программирование в Java

  • 411