Перейти к основному содержимому

Введение в JOOL

· 7 мин. чтения

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 с помощью метода 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 добавляет методы более высокого уровня в Stream , в то время как часто использует его методы ниже.

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 есть много полезных методов для управления последовательностями элементов. Давайте посмотрим на некоторые из них.

Мы можем использовать метод 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, которая добавляет полезные дополнительные методы к стандартному Stream API Java.

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