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

Введение в Протонпак

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

1. Обзор

В этом руководстве мы рассмотрим основные функции Protonpack , библиотеки, которая расширяет стандартный Stream API , добавляя некоторые дополнительные функции.

Обратитесь к этой статье, чтобы узнать об основах Java Stream API.

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

Чтобы использовать библиотеку Protonpack, нам нужно добавить зависимость в наш файл pom.xml :

<dependency>
<groupId>com.codepoetics</groupId>
<artifactId>protonpack</artifactId>
<version>1.15</version>
</dependency>

Проверьте наличие последней версии на Maven Central .

3. StreamUtils

Это основной класс, который расширяет стандартный Stream API Java.

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

3.1. TakeWhile() и TakeUntil()

takeWhile() берет значения из исходного потока , если они соответствуют поставленному условию :

Stream<Integer> streamOfInt = Stream
.iterate(1, i -> i + 1);
List<Integer> result = StreamUtils
.takeWhile(streamOfInt, i -> i < 5)
.collect(Collectors.toList());
assertThat(result).contains(1, 2, 3, 4);

И наоборот, takeUntil() принимает значения до тех пор, пока значение не удовлетворяет заданному условию , а затем останавливается:

Stream<Integer> streamOfInt = Stream
.iterate(1, i -> i + 1);
List<Integer> result = StreamUtils
.takeUntil(streamOfInt, i -> i >= 5)
.collect(Collectors.toList());
assertThat(result).containsExactly(1, 2, 3, 4);

В Java 9 и более поздних версиях takeWhile() является частью стандартного Stream API .

3.2. zip()

zip() принимает два или три потока в качестве входных данных и функцию объединения. Метод берет значение из одной и той же позиции каждого потока и передает его объединителю .

Так происходит до тех пор, пока в одном из потоков не закончатся значения:

String[] clubs = {"Juventus", "Barcelona", "Liverpool", "PSG"};
String[] players = {"Ronaldo", "Messi", "Salah"};
Set<String> zippedFrom2Sources = StreamUtils
.zip(stream(clubs), stream(players), (club, player) -> club + " " + player)
.collect(Collectors.toSet());

assertThat(zippedFrom2Sources)
.contains("Juventus Ronaldo", "Barcelona Messi", "Liverpool Salah");

Точно так же перегруженный zip() , который принимает поток из трех источников:

String[] leagues = { "Serie A", "La Liga", "Premier League" };
Set<String> zippedFrom3Sources = StreamUtils
.zip(stream(clubs), stream(players), stream(leagues),
(club, player, league) -> club + " " + player + " " + league)
.collect(Collectors.toSet());

assertThat(zippedFrom3Sources).contains(
"Juventus Ronaldo Serie A",
"Barcelona Messi La Liga",
"Liverpool Salah Premier League");

3.3. zipWithIndex()

zipWithIndex() принимает значения и сжимает каждое значение с его индексом, чтобы создать поток индексированных значений:

Stream<String> streamOfClubs = Stream
.of("Juventus", "Barcelona", "Liverpool");
Set<Indexed<String>> zipsWithIndex = StreamUtils
.zipWithIndex(streamOfClubs)
.collect(Collectors.toSet());
assertThat(zipsWithIndex)
.contains(Indexed.index(0, "Juventus"), Indexed.index(1, "Barcelona"),
Indexed.index(2, "Liverpool"));

3.4. объединить()

merge() работает с несколькими исходными потоками и объединителем. Он берет значение одной и той же позиции индекса из каждого исходного потока и передает его объединителю .

Метод работает, последовательно беря 1 значение из одного и того же индекса из каждого потока, начиная с начального значения.

Затем значение передается объединителю, а полученное комбинированное значение возвращается обратно в объединитель для создания следующего значения:

Stream<String> streamOfClubs = Stream
.of("Juventus", "Barcelona", "Liverpool", "PSG");
Stream<String> streamOfPlayers = Stream
.of("Ronaldo", "Messi", "Salah");
Stream<String> streamOfLeagues = Stream
.of("Serie A", "La Liga", "Premier League");

Set<String> merged = StreamUtils.merge(
() -> "",
(valOne, valTwo) -> valOne + " " + valTwo,
streamOfClubs,
streamOfPlayers,
streamOfLeagues)
.collect(Collectors.toSet());

assertThat(merged)
.contains("Juventus Ronaldo Serie A", "Barcelona Messi La Liga",
"Liverpool Salah Premier League", "PSG");

3.5. слияние со списком()

mergeToList() принимает несколько потоков в качестве входных данных. Он объединяет значение одного и того же индекса из каждого потока в список :

Stream<String> streamOfClubs = Stream
.of("Juventus", "Barcelona", "PSG");
Stream<String> streamOfPlayers = Stream
.of("Ronaldo", "Messi");

Stream<List<String>> mergedStreamOfList = StreamUtils
.mergeToList(streamOfClubs, streamOfPlayers);
List<List<String>> mergedListOfList = mergedStreamOfList
.collect(Collectors.toList());

assertThat(mergedListOfList.get(0))
.containsExactly("Juventus", "Ronaldo");
assertThat(mergedListOfList.get(1))
.containsExactly("Barcelona", "Messi");
assertThat(mergedListOfList.get(2))
.containsExactly("PSG");

3.6. чередование ()

interleave() создает альтернативные значения, взятые из нескольких потоков, используя селектор .

Метод отдает селектору набор, содержащий по одному значению из каждого потока , и селектор выберет одно значение. ``

Затем выбранное значение будет удалено из набора и заменено следующим значением, из которого произошло выбранное значение. Эта итерация продолжается до тех пор, пока во всех источниках не закончатся значения.

В следующем примере используется чередование() для создания чередующихся значений с помощью стратегии циклического перебора :

Stream<String> streamOfClubs = Stream
.of("Juventus", "Barcelona", "Liverpool");
Stream<String> streamOfPlayers = Stream
.of("Ronaldo", "Messi");
Stream<String> streamOfLeagues = Stream
.of("Serie A", "La Liga");

List<String> interleavedList = StreamUtils
.interleave(Selectors.roundRobin(), streamOfClubs, streamOfPlayers, streamOfLeagues)
.collect(Collectors.toList());

assertThat(interleavedList)
.hasSize(7)
.containsExactly("Juventus", "Ronaldo", "Serie A", "Barcelona", "Messi", "La Liga", "Liverpool");

Имейте в виду, что приведенный выше код предназначен для учебных целей, поскольку селектор циклического перебора предоставляется библиотекой как Selectors.roundRobin() .

3.7. skipUntil() и skipWhile()

skipUntil() пропускает значения до тех пор, пока значение не будет соответствовать условию :

Integer[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
List skippedUntilGreaterThan5 = StreamUtils
.skipUntil(stream(numbers), i -> i > 5)
.collect(Collectors.toList());

assertThat(skippedUntilGreaterThan5).containsExactly(6, 7, 8, 9, 10);

Напротив, skipWhile() пропускает значения, пока значения соответствуют условию :

Integer[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
List skippedWhileLessThanEquals5 = StreamUtils
.skipWhile(stream(numbers), i -> i <= 5 || )
.collect(Collectors.toList());

assertThat(skippedWhileLessThanEquals5).containsExactly(6, 7, 8, 9, 10);

Одна важная особенность skipWhile() заключается в том, что она продолжит потоковую передачу после того, как найдет первое значение, не удовлетворяющее условию:

List skippedWhileGreaterThan5 = StreamUtils
.skipWhile(stream(numbers), i -> i > 5)
.collect(Collectors.toList());
assertThat(skippedWhileGreaterThan5).containsExactly(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

В Java 9 и более поздних версиях dropWhile () в стандартном Stream API предоставляет те же функции, что и skipWhile() .

3.8. разворачиваться ()

unfold() генерирует потенциально бесконечный поток, применяя пользовательский генератор к начальному значению, а затем к каждому сгенерированному значению — поток можно прервать, вернув Optional.empty():

Stream<Integer> unfolded = StreamUtils
.unfold(2, i -> (i < 100)
? Optional.of(i * i) : Optional.empty());

assertThat(unfolded.collect(Collectors.toList()))
.containsExactly(2, 4, 16, 256);

3.9. оконный()

windowed() создает несколько подмножеств исходного потока как поток List . Метод принимает исходный поток, размер окна и значение пропуска в качестве параметра.

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

Integer[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8 };

List<List> windowedWithSkip1 = StreamUtils
.windowed(stream(numbers), 3, 1)
.collect(Collectors.toList());
assertThat(windowedWithSkip1)
.containsExactly(asList(1, 2, 3), asList(2, 3, 4), asList(3, 4, 5), asList(4, 5, 6), asList(5, 6, 7));

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

List<List> windowedWithSkip2 = StreamUtils.windowed(stream(numbers), 3, 2).collect(Collectors.toList());
assertThat(windowedWithSkip2).containsExactly(asList(1, 2, 3), asList(3, 4, 5), asList(5, 6, 7));

3.10. агрегат()

Есть два метода агрегата() , которые работают совершенно по-разному.

Первый агрегат() группирует элементы с одинаковым значением в соответствии с заданным предикатом :

Integer[] numbers = { 1, 2, 2, 3, 4, 4, 4, 5 };
List<List> aggregated = StreamUtils
.aggregate(Arrays.stream(numbers), (int1, int2) -> int1.compareTo(int2) == 0)
.collect(Collectors.toList());
assertThat(aggregated).containsExactly(asList(1), asList(2, 2), asList(3), asList(4, 4, 4), asList(5));

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

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

List<List> aggregatedFixSize = StreamUtils
.aggregate(stream(numbers), 5)
.collect(Collectors.toList());
assertThat(aggregatedFixSize).containsExactly(asList(1, 2, 2, 3, 4), asList(4, 4, 5));

3.11. агрегатное состояние списка ()

aggregateOnListCondition() группирует значения на основе предиката и текущей активной группы . Предикату присваивается текущая активная группа в виде списка и следующее значение. Затем он должен определить, должна ли группа продолжать работу или начать новую группу.

В следующем примере выполняется требование группировать смежные целые значения в группу, где сумма значений в каждой группе не должна превышать 5:

Integer[] numbers = { 1, 1, 2, 3, 4, 4, 5 };
Stream<List<Integer>> aggregated = StreamUtils
.aggregateOnListCondition(stream(numbers),
(currentList, nextInt) -> currentList.stream().mapToInt(Integer::intValue).sum() + nextInt <= 5);
assertThat(aggregated)
.containsExactly(asList(1, 1, 2), asList(3), asList(4), asList(4), asList(5));

4. Потоковый<T>

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

Streamable<String> s = Streamable.of("a", "b", "c", "d");
List<String> collected1 = s.collect(Collectors.toList());
List<String> collected2 = s.collect(Collectors.toList());
assertThat(collected1).hasSize(4);
assertThat(collected2).hasSize(4);

5. Сборщик утилит

CollectorUtils дополняет стандартные коллекторы , добавляя несколько полезных методов сборщиков.

5.1. maxBy() и minBy()

maxBy() находит максимальное значение в потоке, используя предоставленную логику проекции :

Stream<String> clubs = Stream.of("Juventus", "Barcelona", "PSG");
Optional<String> longestName = clubs.collect(CollectorUtils.maxBy(String::length));
assertThat(longestName).contains("Barcelona");

Напротив, minBy() находит минимальное значение, используя предоставленную логику проекции .

5.2. уникальный()

Сборщик unique() делает очень простую вещь: он возвращает единственное значение, если данный поток имеет ровно 1 элемент:

Stream<Integer> singleElement = Stream.of(1);
Optional<Integer> unique = singleElement.collect(CollectorUtils.unique());
assertThat(unique).contains(1);

В противном случае, unique() вызовет исключение:

Stream multipleElement = Stream.of(1, 2, 3);
assertThatExceptionOfType(NonUniqueValueException.class).isThrownBy(() -> {
multipleElement.collect(CollectorUtils.unique());
});

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

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

Начиная с Java 9, некоторые функции, предоставляемые Protonpack, будут доступны в стандартном Stream API.

Как обычно, код можно найти на Github .