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" и т.д. После того, как у вас есть поток пар, обработка проста.