Как использовать потоки Java 8, чтобы найти все значения, предшествующие большему значению?

Случай использования

Через некоторое кодирование, опубликованное Катасом на работе, я наткнулся на эту проблему, которую я не знаю, как решить.

Использование Java 8 Streams, учитывая список положительных целых чисел, создает список целых чисел, где целое число предшествует большему значению.

[10, 1, 15, 30, 2, 6]

Приведенный выше ввод даст:

[1, 15, 2]

поскольку 1 предшествует 15, 15 предшествует 30, а 2 предшествует 6.

Не потоковое решение

public List<Integer> findSmallPrecedingValues(final List<Integer> values) {

    List<Integer> result = new ArrayList<Integer>();
    for (int i = 0; i < values.size(); i++) {
        Integer next = (i + 1 < values.size() ? values.get(i + 1) : -1);
        Integer current = values.get(i);
        if (current < next) {
            result.push(current);
        }
    }
    return result;
}

Что я пробовал

Проблема у меня в том, что я не могу понять, как получить доступ к следующей в лямбде.

return values.stream().filter(v -> v < next).collect(Collectors.toList());

Вопрос

  • Можно ли получить следующее значение в потоке?
  • Должен ли я использовать map и отображение на Pair для того, чтобы получить доступ к следующему?

7 ответов

Решение

С помощью IntStream.range:

static List<Integer> findSmallPrecedingValues(List<Integer> values) {
    return IntStream.range(0, values.size() - 1)
        .filter(i -> values.get(i) < values.get(i + 1))
        .mapToObj(values::get)
        .collect(Collectors.toList());
}

Это, безусловно, лучше, чем императивное решение с большим циклом, но все же немного сложнее, чем цель "использовать поток" идиоматическим способом.

Можно ли получить следующее значение в потоке?

Нет, не совсем. Лучшее, что я знаю о том, что в java.util.stream описание пакета:

Элементы потока посещаются только один раз в течение жизни потока. Как Iteratorдолжен быть создан новый поток, чтобы вернуться к тем же элементам источника.

(Извлечение элементов помимо текущего элемента, с которым ведется работа, подразумевает, что они могут быть посещены более одного раза.)

Мы также могли бы технически сделать это несколькими другими способами:

  • С состоянием (очень хорошо).
  • Использование потока iterator технически все еще использует поток.

Это не чистый Java8, но недавно я опубликовал небольшую библиотеку StreamEx, в которой есть метод именно для этой задачи:

// Find all numbers where the integer preceded a larger value.
Collection<Integer> numbers = Arrays.asList(10, 1, 15, 30, 2, 6);
List<Integer> res = StreamEx.of(numbers).pairMap((a, b) -> a < b ? a : null)
    .nonNull().toList();
assertEquals(Arrays.asList(1, 15, 2), res);

Операция pairMap реализована внутренне с использованием специального сплитератора. В результате у вас есть довольно чистый код, который не зависит от того, является ли источник List или что-нибудь еще. Конечно, он работает и с параллельным потоком.

Передал тестовый сценарий для этой задачи.

Это не однострочная (это двухслойная), но это работает:

List<Integer> result = new ArrayList<>();
values.stream().reduce((a,b) -> {if (a < b) result.add(a); return b;});

Вместо того, чтобы решать это "глядя на следующий элемент", это решает его, "глядя на предыдущий элемент, который reduce() дать вам бесплатно. Я согнул его предполагаемое использование, введя фрагмент кода, который заполняет список на основе сравнения предыдущих и текущих элементов, а затем возвращает текущий, чтобы следующая итерация увидела его как свой предыдущий элемент.


Некоторый тестовый код:

List<Integer> result = new ArrayList<>();
IntStream.of(10, 1, 15, 30, 2, 6).reduce((a,b) -> {if (a < b) result.add(a); return b;});
System.out.println(result);

Выход:

[1, 15, 2]

Принятый ответ работает нормально, если поток является последовательным или параллельным, но может пострадать, если основной List не случайный доступ, из-за нескольких вызовов get,

Если ваш поток последовательный, вы можете запустить этот коллектор:

public static Collector<Integer, ?, List<Integer>> collectPrecedingValues() {
    int[] holder = {Integer.MAX_VALUE};
    return Collector.of(ArrayList::new,
            (l, elem) -> {
                if (holder[0] < elem) l.add(holder[0]);
                holder[0] = elem;
            },
            (l1, l2) -> {
                throw new UnsupportedOperationException("Don't run in parallel");
            });
}

и использование:

List<Integer> precedingValues = list.stream().collect(collectPrecedingValues());

Тем не менее, вы также можете реализовать коллектор, чтобы он работал для последовательных и параллельных потоков. Единственное, что вам нужно применить окончательное преобразование, но здесь вы можете контролировать List реализация, так что вы не будете страдать от get спектакль.

Идея состоит в том, чтобы сначала создать список пар (представленных int[] массив размера 2), который содержит значения в потоке, разрезанные окном размера два с разрывом в один. Когда нам нужно объединить два списка, мы проверяем пустоту и объединяем пробел последнего элемента первого списка с первым элементом второго списка. Затем мы применяем окончательное преобразование, чтобы отфильтровать только нужные значения и отобразить их, чтобы получить желаемый результат.

Это может быть не так просто, как принятый ответ, но вполне может быть альтернативным решением.

public static Collector<Integer, ?, List<Integer>> collectPrecedingValues() {
    return Collectors.collectingAndThen(
            Collector.of(() -> new ArrayList<int[]>(),
                    (l, elem) -> {
                        if (l.isEmpty()) l.add(new int[]{Integer.MAX_VALUE, elem});
                        else l.add(new int[]{l.get(l.size() - 1)[1], elem});
                    },
                    (l1, l2) -> {
                        if (l1.isEmpty()) return l2;
                        if (l2.isEmpty()) return l1;
                        l2.get(0)[0] = l1.get(l1.size() - 1)[1];
                        l1.addAll(l2);
                        return l1;
                    }), l -> l.stream().filter(arr -> arr[0] < arr[1]).map(arr -> arr[0]).collect(Collectors.toList()));
}

Затем вы можете обернуть эти два коллектора в метод служебного коллектора, проверить, параллелен ли поток isParallel затем решите, какой сборщик вернуть.

Если вы готовы использовать стороннюю библиотеку и не нуждаетесь в параллелизме, тогда jOOλ предлагает функции окна в стиле SQL следующим образом

System.out.println(
Seq.of(10, 1, 15, 30, 2, 6)
   .window()
   .filter(w -> w.lead().isPresent() && w.value() < w.lead().get())
   .map(w -> w.value())
   .toList()
);

Уступая

[1, 15, 2]

lead() Функция обращается к следующему значению в порядке обхода из окна.

Отказ от ответственности: я работаю на компанию за JOOλ

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

Приведенный ниже пример сначала определяет экземпляр класса BoundedQueue, который будет хранить элементы, проходящие через поток (если вам не нравится идея расширения LinkedList, обратитесь к упомянутой выше ссылке для альтернативного и более общего подхода). Позже вы просто исследуете два последующих элемента - благодаря вспомогательному классу:

public class Kata {
  public static void main(String[] args) {
    List<Integer> input = new ArrayList<Integer>(asList(10, 1, 15, 30, 2, 6));

    class BoundedQueue<T> extends LinkedList<T> {
      public BoundedQueue<T> save(T curElem) {
        if (size() == 2) { // we need to know only two subsequent elements
          pollLast(); // remove last to keep only requested number of elements
        }

        offerFirst(curElem);
        return this;
      }

      public T getPrevious() {
        return (size() < 2) ? null : getLast();
      }

      public T getCurrent() {
        return (size() == 0) ? null : getFirst();
      }
    }

    BoundedQueue<Integer> streamHistory = new BoundedQueue<Integer>();

    final List<Integer> answer = input.stream()
      .map(i -> streamHistory.save(i))
      .filter(e -> e.getPrevious() != null)
      .filter(e -> e.getCurrent() > e.getPrevious())
      .map(e -> e.getPrevious())
      .collect(Collectors.toList());

    answer.forEach(System.out::println);
  }
}
      List<Integer> listOne= List.of(12,13,67,56,90,62);

listOne.stream().filter(val->val.intValue()>50).collect(Collectors.toList()).forEach(System.out::println);
Другие вопросы по тегам