«1. Обзор

В этой статье мы рассмотрим библиотеку jOOL — еще один продукт от jOOQ.

2. Зависимость от Maven

Давайте начнем с добавления зависимости от Maven в ваш pom.xml:

<dependency>
    <groupId>org.jooq</groupId>
    <artifactId>jool</artifactId>
    <version>0.9.12</version>
</dependency>

Вы можете найти последнюю версию здесь.

3. Функциональные интерфейсы

В Java 8 функциональные интерфейсы весьма ограничены. Они принимают максимум два параметра и не имеют многих дополнительных функций.

jOOL исправляет это, доказывая набор новых функциональных интерфейсов, которые могут принимать даже 16 параметров (от Function1 до Function16) и обогащены дополнительными удобными методами.

Например, чтобы создать функцию, которая принимает три аргумента, мы можем использовать Function3:

Function3<String, String, String, Integer> lengthSum
  = (v1, v2, v3) -> v1.length() + v2.length() + v3.length();

В чистой Java вам нужно будет реализовать ее самостоятельно. Кроме того, функциональные интерфейсы из jOOL имеют метод applyPartially(), который позволяет нам легко выполнять частичное приложение:

Function2<Integer, Integer, Integer> addTwoNumbers = (v1, v2) -> v1 + v2;
Function1<Integer, Integer> addToTwo = addTwoNumbers.applyPartially(2);

Integer result = addToTwo.apply(5);

assertEquals(result, (Integer) 7);

Когда у нас есть метод типа Function2, мы можем легко преобразовать его в стандартный Java BiFunction с помощью метода toBiFunction():

BiFunction biFunc = addTwoNumbers.toBiFunction();

Точно так же существует метод toFunction() типа Function1.

4. Кортежи

Кортеж — очень важная конструкция в мире функционального программирования. Это типизированный контейнер для значений, где каждое значение может иметь свой тип. Кортежи часто используются в качестве аргументов функций.

Они также очень полезны при преобразовании потока событий. В jOOL у нас есть кортежи, которые могут обернуть от одного до шестнадцати значений, предоставляемых Tuple1 до типов Tuple16:

tuple(2, 2)

И для четырех значений:

tuple(1,2,3,4);

Давайте рассмотрим пример, когда у нас есть последовательность кортежей, содержащих 3 значения:

Seq<Tuple3<String, String, Integer>> personDetails = Seq.of(
  tuple("michael", "similar", 49),
  tuple("jodie", "variable", 43));
Tuple2<String, String> tuple = tuple("winter", "summer");

List<Tuple4<String, String, String, String>> result = personDetails
  .map(t -> t.limit2().concat(tuple)).toList();

assertEquals(
  result,
  Arrays.asList(tuple("michael", "similar", "winter", "summer"), tuple("jodie", "variable", "winter", "summer"))
);

Мы можем использовать различные виды преобразований кортежей. Во-первых, мы вызываем метод limit2() для получения только двух значений из Tuple3. Затем мы вызываем метод concat() для объединения двух кортежей.

В результате мы получаем значения типа Tuple4.

5. Seq

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

5.1. Содержит операции

Мы можем найти пару вариантов методов, проверяющих наличие элементов в Seq. Некоторые из этих методов используют метод anyMatch() из класса Stream:

assertTrue(Seq.of(1, 2, 3, 4).contains(2));

assertTrue(Seq.of(1, 2, 3, 4).containsAll(2, 3));

assertTrue(Seq.of(1, 2, 3, 4).containsAny(2, 5));

5.2. Операции соединения

Когда у нас есть два потока и мы хотим их соединить (аналогично операции соединения двух наборов данных в SQL), использование стандартного класса Stream — не очень элегантный способ сделать это:

Stream<Integer> left = Stream.of(1, 2, 4);
Stream<Integer> right = Stream.of(1, 2, 3);

List<Integer> rightCollected = right.collect(Collectors.toList());
List<Integer> collect = left
  .filter(rightCollected::contains)
  .collect(Collectors.toList());

assertEquals(collect, Arrays.asList(1, 2));

Мы необходимо собрать правильный поток в список, чтобы предотвратить java.lang.IllegalStateException: поток уже обработан или закрыт. Затем нам нужно выполнить побочную операцию, получив доступ к списку rightCollected из метода фильтра. Это подверженный ошибкам и не элегантный способ соединения двух наборов данных.

К счастью, в Seq есть полезные методы для внутренних, левых и правых соединений наборов данных. Эти методы скрывают его реализацию, раскрывая элегантный API.

Мы можем выполнить внутреннее соединение, используя метод innerJoin():

assertEquals(
  Seq.of(1, 2, 4).innerJoin(Seq.of(1, 2, 3), (a, b) -> a == b).toList(),
  Arrays.asList(tuple(1, 1), tuple(2, 2))
);

Мы можем выполнить правое и левое соединения соответственно:

assertEquals(
  Seq.of(1, 2, 4).leftOuterJoin(Seq.of(1, 2, 3), (a, b) -> a == b).toList(),
  Arrays.asList(tuple(1, 1), tuple(2, 2), tuple(4, null))
);

assertEquals(
  Seq.of(1, 2, 4).rightOuterJoin(Seq.of(1, 2, 3), (a, b) -> a == b).toList(),
  Arrays.asList(tuple(1, 1), tuple(2, 2), tuple(null, 3))
);

Существует даже метод crossJoin(), который позволяет выполнить декартово соединение двух наборов данных:

assertEquals(
  Seq.of(1, 2).crossJoin(Seq.of("A", "B")).toList(),
  Arrays.asList(tuple(1, "A"), tuple(1, "B"), tuple(2, "A"), tuple(2, "B"))
);

5.3. Манипуляции с Seq

В Seq есть много полезных методов для работы с последовательностями элементов. Давайте посмотрим на некоторые из них.

Мы можем использовать метод cycle() для многократного извлечения элементов из исходной последовательности. Это создаст бесконечный поток, поэтому нам нужно быть осторожными при сборе результатов в список, поэтому нам нужно использовать метод limit() для преобразования бесконечной последовательности в конечную:

assertEquals(
  Seq.of(1, 2, 3).cycle().limit(9).toList(),
  Arrays.asList(1, 2, 3, 1, 2, 3, 1, 2, 3)
);

Предположим, что мы хотим продублировать все элементы из одной последовательности во вторую последовательность. Метод дубликат() делает именно это:

assertEquals(
  Seq.of(1, 2, 3).duplicate().map((first, second) -> tuple(first.toList(), second.toList())),
  tuple(Arrays.asList(1, 2, 3), Arrays.asList(1, 2, 3))
);

Возвращаемый тип метода дубликата() — это кортеж из двух последовательностей.

Допустим, у нас есть последовательность целых чисел, и мы хотим разделить эту последовательность на две последовательности, используя некоторый предикат. Мы можем использовать метод partition():

assertEquals(
  Seq.of(1, 2, 3, 4).partition(i -> i > 2)
    .map((first, second) -> tuple(first.toList(), second.toList())),
  tuple(Arrays.asList(3, 4), Arrays.asList(1, 2))
);

5.4. Группировка элементов

«Группировка элементов по ключу с помощью Stream API громоздка и не интуитивно понятна, потому что нам нужно использовать метод collect() с коллектором Collectors.groupingBy.

Seq скрывает этот код за методом groupBy(), который возвращает Map, поэтому нет необходимости явно использовать метод collect():

Map<Integer, List<Integer>> expectedAfterGroupBy = new HashMap<>();
expectedAfterGroupBy.put(1, Arrays.asList(1, 3));
expectedAfterGroupBy.put(0, Arrays.asList(2, 4));

assertEquals(
  Seq.of(1, 2, 3, 4).groupBy(i -> i % 2),
  expectedAfterGroupBy
);

5.5. Пропуск элементов

Допустим, у нас есть последовательность элементов, и мы хотим пропустить элементы, пока предикат не соответствует. Когда предикат удовлетворен, элементы должны попасть в результирующую последовательность.

Для этого мы можем использовать метод skipWhile():

assertEquals(
  Seq.of(1, 2, 3, 4, 5).skipWhile(i -> i < 3).toList(),
  Arrays.asList(3, 4, 5)
);

Мы можем добиться того же результата, используя метод skipUntil():

assertEquals(
  Seq.of(1, 2, 3, 4, 5).skipUntil(i -> i == 3).toList(),
  Arrays.asList(3, 4, 5)
);

5.6. Сжатие последовательностей

Когда мы обрабатываем последовательности элементов, часто возникает необходимость сжать их в одну последовательность.

API zip(), который можно использовать для объединения двух последовательностей в одну:

assertEquals(
  Seq.of(1, 2, 3).zip(Seq.of("a", "b", "c")).toList(),
  Arrays.asList(tuple(1, "a"), tuple(2, "b"), tuple(3, "c"))
);

Результирующая последовательность содержит кортежи из двух элементов.

Когда мы заархивируем две последовательности, но хотим заархивировать их определенным образом, мы можем передать BiFunction методу zip(), который определяет способ заархивирования элементов:

assertEquals(
  Seq.of(1, 2, 3).zip(Seq.of("a", "b", "c"), (x, y) -> x + ":" + y).toList(),
  Arrays.asList("1:a", "2:b", "3:c")
);

Иногда это полезно заархивировать последовательность с индексом элементов в этой последовательности через API zipWithIndex():

assertEquals(
  Seq.of("a", "b", "c").zipWithIndex().toList(),
  Arrays.asList(tuple("a", 0L), tuple("b", 1L), tuple("c", 2L))
);

6. Преобразование проверенных исключений в непроверенные

Допустим, у нас есть метод, который принимает строку и может генерировать проверенное исключение:

public Integer methodThatThrowsChecked(String arg) throws Exception {
    return arg.length();
}

Затем мы хотим отобразить элементы потока, применяя этот метод к каждому элементу. Нет способа обработать это исключение выше, поэтому нам нужно обработать это исключение в методе map():

List<Integer> collect = Stream.of("a", "b", "c").map(elem -> {
    try {
        return methodThatThrowsChecked(elem);
    } catch (Exception e) {
        e.printStackTrace();
        throw new RuntimeException(e);
    }
}).collect(Collectors.toList());

assertEquals(
    collect,
    Arrays.asList(1, 1, 1)
);

Мы мало что можем сделать с этим исключением из-за дизайна функциональных интерфейсов в Java, поэтому в предложение catch, мы преобразуем проверенное исключение в непроверенное.

К счастью, в jOOL есть класс Unchecked, в котором есть методы, которые могут преобразовывать проверенные исключения в непроверенные:

List<Integer> collect = Stream.of("a", "b", "c")
  .map(Unchecked.function(elem -> methodThatThrowsChecked(elem)))
  .collect(Collectors.toList());

assertEquals(
  collect,
  Arrays.asList(1, 1, 1)
);

Мы оборачиваем вызов метода ThatThrowsChecked() в метод Unchecked.function(), который обрабатывает преобразование исключений ниже.

7. Заключение

В этой статье показано, как использовать библиотеку jOOL, которая добавляет полезные дополнительные методы к стандартному Java Stream API.

Реализацию всех этих примеров и фрагментов кода можно найти в проекте GitHub — это проект Maven, поэтому его должно быть легко импортировать и запускать как есть.