Java Stream: есть ли способ повторить итерацию двух элементов вместо одного?
Скажем, у нас есть этот поток
Stream.of("a", "b", "err1", "c", "d", "err2", "e", "f", "g", "h", "err3", "i", "j");
и я хочу сохранить на карте пары соседних строк, в которых первая начинается с "err".
То, что я думал, это что-то вроде этого
Map<String, String> map = new HashMap<>();
Stream.of("a", "b", "err1", "c", "d", "err2", "e", "f", "g", "h", "err3", "i", "j")
.reduce((acc, next) -> {
if (acc.startsWith("err"))
map.put(acc,next);
if (next.startsWith("err"))
return next;
else
return "";
});
Но я не полностью удовлетворен этим по двум основным причинам.
- Я использую функцию "неправильное использование"
reduce
. В Stream API каждая функция имеет четкую и четко определенную цель: max
предполагается исчислять максимальное значение, filter
должен фильтровать на основе условия, reduce
должен генерировать добавочно накопленное значение и так далее.
- Это не позволяет мне использовать мощные механизмы Streams: что, если бы я хотел ограничить мой поиск первыми двумя результатами?
Здесь я использовал reduce
, потому что (насколько мне известно) это единственная функция, которая позволяет вам сравнить пару значений, которые вы можете каким-то образом привести к чему-то, аналогичному понятиям "текущее значение" и "следующее значение".
Есть ли более простой способ? Что-то, что позволяет вам перебирать поток с учетом более чем одного значения для каждой итерации?
ИЗМЕНИТЬ
То, о чем я думаю, это какой-то механизм, который, учитывая текущий элемент, позволяет вам определить "окно элементов" для каждой итерации.
Что-то вроде
<R> Stream<R> mapMoreThanOne(
int elementsBeforeCurrent,
int elementsAfterCurrent,
Function<List<? super T>, ? extends R> mapper);
вместо
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
Это будет мощное "обновление" для текущего API.
EDIT2
Я ценю усилия людей, предлагающих их решение, но проблема заключается не в самом алгоритме. Существуют разные способы достижения моей цели, объединяя потоки, индексы, временные переменные для хранения предыдущих значений... но мне было интересно, существует ли какой-либо метод в Stream API, который был разработан для задачи обработки элементов, отличных от текущего не нарушая "русскую парадигму". Что-то вроде этого
List<String> list =
Stream.of("a", "b", "err1", "c", "d", "err2", "e", "f", "g", "h", "err3", "i", "j")
.filterFunctionImWonderingIfExist(/*filters couples of elements*/)
.limit(2)
.collect(Collectors.toList());
Учитывая ответы, я думаю, что нет "ясного и быстрого" решения, если не использовать библиотеку StreamEx
Ответы
Ответ 1
Вы можете создать сборщик для этой задачи.
Map<String, String> map =
Stream.of("a", "b", "err1", "c", "d", "err2", "e", "f", "g", "h", "err3", "i", "j")
.collect(MappingErrors.collector());
с:
private static final class MappingErrors {
private Map<String, String> map = new HashMap<>();
private String first, second;
public void accept(String str) {
first = second;
second = str;
if (first != null && first.startsWith("err")) {
map.put(first, second);
}
}
public MappingErrors combine(MappingErrors other) {
throw new UnsupportedOperationException("Parallel Stream not supported");
}
public Map<String, String> finish() {
return map;
}
public static Collector<String, ?, Map<String, String>> collector() {
return Collector.of(MappingErrors::new, MappingErrors::accept, MappingErrors::combine, MappingErrors::finish);
}
}
В этом сборнике сохраняются два работающих элемента. Каждый раз, когда a String
принимается, они обновляются и если первый начинается с "err"
, эти два элемента добавляются к карте.
Другим решением является использование библиотеки StreamEx, которая предоставляет pairMap
, который применяет данную функцию к каждой смежной паре элементов этого потока. В следующем коде операция возвращает массив String, состоящий из первого и второго элементов пары, если первый элемент начинается с "err"
, null
в противном случае. Элементы null
затем отфильтровываются, и поток собирается в карту.
Map<String, String> map =
StreamEx.of("a", "b", "err1", "c", "d", "err2", "e", "f", "g", "h", "err3", "i", "j")
.pairMap((s1, s2) -> s1.startsWith("err") ? new String[] { s1, s2 } : null)
.nonNull()
.toMap(a -> a[0], a -> a[1]);
System.out.println(map);
Ответ 2
Вы можете написать пользовательский коллекционер или использовать гораздо более простой способ потоковой передачи по индексам списка:
Map<String, String> result = IntStream.range(0, data.size() - 1)
.filter(i -> data.get(i).startsWith("err"))
.boxed()
.collect(toMap(data::get, i -> data.get(i+1)));
Это предполагает, что ваши данные находятся в списке с произвольным доступом или что вы можете временно выгрузить его в один.
Если вы не можете случайно получить доступ к данным или загрузить их в список или массив для обработки, вы всегда можете создать собственный сборщик pairing
, чтобы вы могли писать
Map<String, String> result = data.stream()
.collect(pairing(
(a, b) -> a.startsWith("err"),
AbstractMap.SimpleImmutableEntry::new,
toMap(Map.Entry::getKey, Map.Entry::getValue)
));
Здесь источник для коллектора. Он совместим с параллелью и может пригодиться в других ситуациях:
public static <T, V, A, R> Collector<T, ?, R> pairing(BiPredicate<T, T> filter, BiFunction<T, T, V> map, Collector<? super V, A, R> downstream) {
class Pairing {
T left, right;
A middle = downstream.supplier().get();
boolean empty = true;
void add(T t) {
if (empty) {
left = t;
empty = false;
} else if (filter.test(right, t)) {
downstream.accumulator().accept(middle, map.apply(right, t));
}
right = t;
}
Pairing combine(Pairing other) {
if (!other.empty) {
this.add(other.left);
this.middle = downstream.combiner().apply(this.middle, other.middle);
this.right = other.right;
}
return this;
}
R finish() {
return downstream.finisher().apply(middle);
}
}
return Collector.of(Pairing::new, Pairing::add, Pairing::combine, Pairing::finish);
}
Ответ 3
Все будет проще, если ваш вход находится в списке произвольного доступа. Таким образом, вы можете использовать старый добрый метод List.subList
следующим образом:
List<String> list = Arrays.asList("a", "b", "err1", "c", "d", "err2", "e",
"f", "g", "h", "err3", "i", "j");
Map<String, String> map = IntStream.range(0, list.size()-1)
.mapToObj(i -> list.subList(i, i+2))
.filter(l -> l.get(0).startsWith("err"))
.collect(Collectors.toMap(l -> l.get(0), l -> l.get(1)));
То же самое можно было бы сделать с уже упомянутой библиотекой StreamEx (написанной мной) немного короче:
List<String> list = Arrays.asList("a", "b", "err1", "c", "d", "err2", "e",
"f", "g", "h", "err3", "i", "j");
Map<String, String> map = StreamEx.ofSubLists(list, 2, 1)
.mapToEntry(l -> l.get(0), l -> l.get(1))
.filterKeys(key -> key.startsWith("err"))
.toMap();
Хотя, если вы не хотите зависимостей сторонних производителей, плохое решение Stream API выглядит также не очень плохо.
Ответ 4
Вот простой один вкладыш с использованием готового коллектора:
Stream<String> stream = Stream.of("a", "b", "err1", "c", "d", "err2", "e", "f", "g", "h", "err3", "i", "j");
Map<String, String> map = Arrays.stream(stream
.collect(Collectors.joining(",")).split(",(?=(([^,]*,){2})*[^,]*$)"))
.filter(s -> s.startsWith("err"))
.map(s -> s.split(","))
.collect(Collectors.toMap(a -> a[0], a -> a[1]));
"Трюк" здесь состоит в том, чтобы сначала соединить все члены вместе в одну строку, а затем разбить на строки пар, например "a,b"
, "err1,c"
и т.д. После того, как у вас есть поток пар, обработка проста.