Как использовать потоки 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);