Занятие № 5

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

 Павел 

 Брычев 

Обо мне

Образование: ИЕИГН СФУ, Институт математики, 2010г.

Работаю в IT 10 лет, из них в СИС - 8,5 лет.

Старший разработчик программного обеспечения.     

Технологии:

  • java
  • spring
  • postgresql
  • angular
  • typescript
  • javascript, html, css
  • intersystems cache
  • actionscript
  • cpp
  • и даже немного 1С

Увлечения:

  • программирование
  • настольные игры
  • чтение
  • туризм
  • бег
  • спортивное ориентирование
  • ...

Roadmap

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

Программист должен описать, как нужно решать задачу.

Императивный стиль

  • происходит от англ. imperative, означающего "повелительный", "властный", "обязывающий";
  • программа представляет собой набор последовательных инструкций;
  • управление состоянием памяти;
  • управление состоянием (переменные) программы;
  • оператор присваивания;
  • условный оператор;
  • циклы.

Программист должен описать, что представляет собой проблема и ожидаемый результат.

Декларативный стиль

  • от англ. declarative, означающего "описание", "соглашение", "декларация";
  • программа - совокупность утверждений, описывающих проблему, и результат, а не методы его достижения;
  • нет управления состоянием памяти;
  • нет состояния программы;
  • нет условного оператора, хотя возможность задать ветвление есть;
  • нет циклов.

Алан Тьюринг

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

Алонзо Чёрч

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

Откуда пошло функциональное программирование

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

// 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;
}

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

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;
}

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

Предикат

Предика́т — это функция, которая для каждого набора переданных в нее аргументов возвращает одно из следующих двух значений:

  • 0 ("истина", "да", "true") 
  • 1 ("ложь", "нет", "false") 

    Примеры:

        P(x) - x это простое число.

        F(d, l) - d является отцом l.

        Q(p) - p является продуктом типа "сладкое".

        Q(p, t) - p является продуктом типа t.

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);
}

Немного магии...

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

Лямбда-выражение (λ)

Лямбда-выражение — специальный синтаксис для определения анонимных функций.

  • Используя лямбда-выражения, можно объявлять функции в любом месте кода.
  • При объявлении, λ замыкает в себе значения переменных из блока кода, в котором объявляется. В λ можно использовать только final или effectively final переменные.
// Однострочная лямбда-функция
Predicate predicate = 
   product -> product.getType() == type;

Параметры

Разделитель

Тело

// Обычная функция
boolean match(Product product) {
   return product.getType() == this.type;
}

Тип возвращаемого значения

?

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

Predicate predicate = (Product item, double maxPrice) -> {
   if (item.getPrice() > maxPrice) {
      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));
}

... стало!

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;
}

Постойте-ка! А как же filter?

+ еще 10 строк кода!!!

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

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

Ответ – StreamAPI!

"Это не те дроиды стримы,

что вы ищете..."

  • InputStream, OutputStream
  • FileInputStream, FileOutputStream
  • BufferedInputStream, BufferedOutputStream
  • ...

Streams

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

А что тогда такое Stream?

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

Примеры:

  • filter
  • map
  • flatMap
  • skip
  • limit

Нетерминальные методы Stream

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

Примеры:​

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

Терминальные методы Stream

Оператор map

  • принимает на вход функцию трансформации элемента коллекции
  • применяет ее ко всем элементам
  • возвращает преобразованный поток элементов

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

List<Producer> getUniqueProducers(List<Product> products) {

   return products.stream()
                  .map(Product::getProducer)
                  .distinct()
                  .collect(Collectors.toList());
}

Оператор sorted

  • принимает на вход функцию сравнения (компаратор) двух элементов
  • возвращает отсортированный поток элементов
  • можно выстроить сортировку по нескольким полям (или выражениям), используя метод thenComparing интерфейса Comparable

Сортировка элементов

List<Producer> getUniqueSortedProducers(List<Product> products) {

   return products
      .stream()
      .map(Product::getProducer)
      .distinct()
      .sorted(Comparator.comparing(Producer::getName))
      .collect(Collectors.toList());
}
List<Product> filterByType(
    List<Product> products, ProductType type) {
    
    return products.stream()
                   .filter(product -> item.getType() == type)
                   .collect(Collectors.toList());
}

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

Оператор filter

  • аналог написанного ранее (слайд 15) метода фильтрации коллекции
  • принимает на вход функцию-предикат
  • возвращает отфильтрованный поток элементов
List<Product> filterByType(
    List<Product> products, ProductType type) {
    return products.stream()
                   .filter(product -> item.getType() == type)
                   .collect(Collectors.toList());
}

Оператор collect

  • ​преобразовывает поток в конечный элемент, тип которого зависит от переданного коллектора

Получение результата работы stream

String getProductsAsString(
    List<Product> products) {
    return products.stream()
                   .collect(Collectors.joining());
}
BigDecimal getTotalTax(List<Product> products) {
   return products
    .stream()
    .map(Product::getPrice)
    .map(price -> price.divide(BigDecimal.valueOf(120),
                               RoundingMode.HALF_EVEN)
                       .multiply(BigDecimal.valueOf(20)))
    .reduce(BigDecimal.ZERO, BigDecimal::add);
 }

Получение результата работы stream

Оператор reduce

  • базовый оператор, используя его можно реализовать другие операторы
  • трансформирует поток к конкретному результату (зависит от реализации)
  • в одной из форм - принимает на вход бинарную функцию, которая будет обрабатывать накопленный элемент ("аккумулятор") и текущий элемент
  • возвращает накопленный элемент

Декларативность

   - мы описываем, что мы хотим получить

 

Ленивость

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

 

Лаконичность

   - пишем меньше кода, допускаем меньше низкоуровневых ошибок

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

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

Optional<T> findFirst();

Optional?

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

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

String extractProductProducerName(Product product) {
   return product.getProducer()
                 .getName();
}

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

String getProductProducerNameByIdUnsafe(
   List<Product> products, long id) {
   
   Product product = findProductByIdUnsafe(products, id);
   return extractProductProducerName(product);
}

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

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

Optional<Product> findProductById(
   List<Product> products, long id) {
      return products
         .stream()
         .filter(product -> product.getId() == id)
         .findFirst();
}

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

Optional<Product> вместо Product

String getProductProducerNameById(
   List<Product> products, long id) {
   
   Product product = findProductById(products, id);
   return extractProductProducerName(product);
}

Ошибка на этапе компиляции, а не NullPointerException в рантайме

Применим написанную функцию

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

String getProductProducerNameById(
   List<Product> products, long id) {
   Optional<Product> productOptional = 
      findProductById(products, id);
    
   if (productOptional.isPresent()) {
       return extractProductProducerName(
          productOptional.get());
   } else {
       throw new RuntimeException(
          String.format(
             "Товар с идентификатором '%s' не найден", id));
   }
}

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

String getProductProducerNameById(
   List<Product> products, long id) {
   Optional<String> nameOptional = 
      findProductById(products, id)
         .map(ProductsService::extractProductProducerName);
    
   if (nameOptional.isPresent()) {
      return nameOptional.get();
   } else {
      throw new RuntimeException(
         String.format(
            "Товар с идентификатором '%s' не найден", id));
   }
}

Метод map

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

String getProductProducerNameById(
   List<Product> products, long id) {
   return 
      findProductById(products, id)
         .map(ProductsService::extractProductProducerName);
         .orElseThrow(() -> new RuntimeException(
            String.format(
               "Товар с идентификатором '%s' не найден", id)
            )
         );
}

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

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

Вместо исключения - вернется строка "Неизвестный производитель"

String getProductProducerNameByIdSoft(
   List<Product> products, long id) {
   return 
      findProductById(products, id)
         .map(ProductsService::extractProductProducerName);
         .orElse("Неизвестный производитель");
}

Как создать / получить Optional

  • Некоторые функции StreamAPI возвращают Optional:
    • ​findFirst, findAny
    • reduce
    • min, max​
  • Optional.of и ​Optional.ofNullable
    • создают новый Optional для поданного в метод объекта
  • ​​Optional Полученный любым способом можно использовать:
    • для защиты от NullPointerException
    • для организации цепочки вычислений
    • для упрощения громоздкого и/или избыточного императивного кода​​

Optional.of не может быть пустым. Если подать в него null, то произойдет исключение в рантайме!

Что почитать

Предметная область:

 - грузовые перевозки

Данные:

 - есть несколько видов транспорта,

   у каждого транспорта своя

   стоимость доставки груза и вместимость.

 - есть груз, который нужно доставить,

   груз имеет список доступных путей

   (ж/д, морской, воздушный, ...)

Задание:

 - для заданного груза рассчитать оптимальный по затратам способ доставки (наименьшая цена)

  • в классе PathFinder реализовать метод getOptimalTransport
  • для проверки использовать тесты класса TestPathFinder
  • для решения задачи приветствуется использование StreamAPI и Optional

Задание #5.1 ~ "Оптимальный транспорт"

Обязательное

Предметная область и данные: см. задачу #5.1

Дополнительные данные и условия:

 - есть несколько задач по доставке груза

 - есть несколько видов транспорта

 - есть диапазон потраченных денег

 - мы знаем точно, что была решена одна задача,

   но не знаем какая именно

 - не факт что все деньги потрачены на задачу :-)

Задание:

  • для заданного набора данных определить список возможных решений задач доставки, которые подходят под все перечисленные условия, т.е. формально мы должны понять какие именно задачи могли быть решены
  • выдать этот список в отсортированном виде по 2-ум полям:
    • стоимость решения - по убыванию, наименование груза - по возрастанию
  • в классе InversePathFinder реализовать метод getAllSolutions
  • для проверки использовать тесты класса TestInversePathFinder

Задание #5.2* ~ "Нельзя просто взять и вспомнить"

Необязательное

Исходный код:

https://github.com/SiberianIntegrationSystems/JavaSIS-3.20/tree/master/unit5

Требования:

  • включенный процессор аннотаций в IDEA (enable annotation processor)
  • установленный плагин Lombok                                                                                                                                   

Возможные проблемы:

  • Некорректное отображение русских символов в консоли или некорректная сортировка русских слов по алфавиту
    • Кодировка проекта IDEA должна быть UTF-8 (Settings -> Editor -> File Encodings - настройки GlobalEncoding и ProjectEncoding)
    • Кодировка консоли тоже должна быть UTF-8 (Help -> Edit Custom VM Options... - проверить наличие следующих строки и, если их нет, то добавить:
      • -Dfile.encoding=UTF-8
      • ​-Dconsole.encoding=UTF-8
    • Если все настроено так, как описано выше, то попробуйте
      • либо заново импортировать проект в IDEA
      • либо перевести все файлы сначала в другую кодировку (например, windows-1251), а потом вернуть в UTF-8

Дополнительная информация по заданиям

Сущности:

  • RouteType - перечисление для типа маршрута (ж/д, авиа, море, ...)
  • Route - маршрут, определенного типа с конкретной протяженностью
  • Transport - конкретная единица транспорта (самолет, автомобиль и т.п.)
    • ​содержит наименование, допустимые объем и тип маршрута, а также стоимость перевозки груза на единицу длины маршрута
  • DeliveryTask - задача по доставке груза
    • ​содержит название груза, объем груза и список доступных типов маршрутов
  • Solution - решение задачи по доставке груза (не факт что оптимальное)
    • ​содержит решаемую задачу, транспорт, выбранный для решения и стоимость решения​
  • InverseDeliveryTask - обратная задача по доставке груза
    • ​содержит список задач по доставке груза, которые могли быть решены
    • ​содержит список доступного транспорта, который мог быть использован для решения
    • ​содержит диапазон (BigDecimalRange) цены решенной задачи (может быть частично открытым / закрытым / строгим / нестрогим )

Дополнительная информация по заданиям

Занятие 5

By pavelsilent

Занятие 5

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

  • 334