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