Возьмите каждый n-й элемент из потока Java 8
Предположим, у меня есть список вроде этого:
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Можно ли использовать поток Java 8 для переноса каждого второго элемента из этого списка, чтобы получить следующее?
[1, 3, 5, 7, 9]
Или, может быть, даже каждый третий элемент?
[1, 4, 7, 10]
В принципе, я ищу функцию для каждого n-го элемента потока:
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
List<Integer> list2 = list.stream().takenth(3).collect(Collectors.toList());
System.out.println(list2);
// => [1, 4, 7, 10]
Ответы
Ответ 1
Одной из главных побуждений для внедрения потоков Java было разрешение параллельных операций. Это привело к тому, что операции с потоками Java, такие как map
и filter
, не зависят от положения элемента в потоке или элементов вокруг него. Это имеет то преимущество, что позволяет легко разделить потоки для параллельной обработки. Это имеет недостаток, заключающийся в том, чтобы сделать некоторые операции более сложными.
Таким образом, простой ответ заключается в том, что нет простого способа делать такие вещи, как принимать каждый n-й элемент или сопоставлять каждый элемент с суммой всех предыдущих элементов.
Самый простой способ реализовать ваше требование - использовать индекс списка, из которого вы передаете:
List<String> list = ...;
return IntStream.range(0, list.size())
.filter(n -> n % 3 == 0)
.mapToObj(list::get)
.collect(Collectors.toList());
Более сложным решением будет создание пользовательского коллектора, который собирает каждый n-й элемент в список.
class EveryNth<C> {
private final int nth;
private final List<List<C>> lists = new ArrayList<>();
private int next = 0;
private EveryNth(int nth) {
this.nth = nth;
IntStream.range(0, nth).forEach(i -> lists.add(new ArrayList<>()));
}
private void accept(C item) {
lists.get(next++ % nth).add(item);
}
private EveryNth<C> combine(EveryNth<C> other) {
other.lists.forEach(l -> lists.get(next++ % nth).addAll(l));
next += other.next;
return this;
}
private List<C> getResult() {
return lists.get(0);
}
public static Collector<Integer, ?, List<Integer>> collector(int nth) {
return Collector.of(() -> new EveryNth(nth),
EveryNth::accept, EveryNth::combine, EveryNth::getResult));
}
Это можно использовать следующим образом:
List<String> list = Arrays.asList("Anne", "Bill", "Chris", "Dean", "Eve", "Fred", "George");
list.stream().parallel().collect(EveryNth.collector(3)).forEach(System.out::println);
Возвращает результат, который вы ожидаете.
Это очень неэффективный алгоритм даже при параллельной обработке. Он разбивает все элементы, которые он принимает на n списков, а затем просто возвращает первый. К сожалению, он должен хранить все элементы в процессе накопления, потому что он не до тех пор, пока они не будут объединены, что он знает, какой список является n-ым. Учитывая его сложность и неэффективность, я определенно рекомендовал бы придерживаться вышеупомянутого решения, основанного на индексах.
Ответ 2
EDIT - 28 ноября 2017 г.
Как указывает пользователь @Emiel в комментариях, лучший способ сделать это - использовать Stream.itearate
для управления списком через последовательность индексов:
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
int skip = 3;
int size = list.size();
// Limit to carefully avoid IndexOutOfBoundsException
int limit = size / skip + Math.min(size % skip, 1);
List<Integer> result = Stream.iterate(0, i -> i + skip)
.limit(limit)
.map(list::get)
.collect(Collectors.toList());
System.out.println(result); // [1, 4, 7, 10]
Этот подход не имеет недостатков моего предыдущего ответа, который ниже (я решил сохранить его по историческим причинам).
Другим подходом было бы использовать Stream.iterate()
следующим образом:
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
int skip = 3;
int size = list.size();
// Limit to carefully avoid IndexOutOfBoundsException
int limit = size / skip + Math.min(size % skip, 1);
List<Integer> result = Stream.iterate(list, l -> l.subList(skip, l.size()))
.limit(limit)
.map(l -> l.get(0))
.collect(Collectors.toList());
System.out.println(result); // [1, 4, 7, 10]
Идея состоит в создании потока подписок, каждый из которых пропускает первые N
элементы предыдущего (N=3
в примере).
Нам нужно ограничить число итераций, чтобы мы не пытались получить подслост, границы которого вне диапазона.
Затем мы сопоставляем наши подсписки с их первым элементом и собираем наши результаты. Сохранение первого элемента каждого подсписок работает так, как ожидалось, потому что каждый индекс начала подсети сдвигается N
вправо в соответствии с исходным списком.
Это также эффективно, потому что метод List.sublist()
возвращает представление исходного списка, что означает, что он не создает новый List
для каждой итерации.
EDIT:. Через некоторое время я узнал, что гораздо лучше взять либо один из подходов @sprinter, так как subList()
создает обертку вокруг исходного списка. Это означает, что второй список потока будет обертой первого списка, третий список потока будет оберткой второго списка (который уже является оберткой!) И т.д....
Хотя это может работать для небольших и средних списков, следует отметить, что для очень большого списка источников будет создано множество оболочек. И это может оказаться дорогостоящим или даже генерировать StackOverflowError
.
Ответ 3
Если вы хотите использовать стороннюю библиотеку, jOOλ предлагает полезные функции, такие как zipWithIndex()
:
Каждый второй элемент
System.out.println(
Seq.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
.zipWithIndex() // This produces a Tuple2(yourvalue, index)
.filter(t -> t.v2 % 2 == 0) // Filter by the index
.map(t -> t.v1) // Remove the index again
.toList()
);
[1, 3, 5, 7, 9]
Каждый третий элемент
System.out.println(
Seq.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
.zipWithIndex()
.filter(t -> t.v2 % 3 == 0)
.map(t -> t.v1)
.toList()
);
[1, 4, 7, 10]
Отказ от ответственности: я работаю в компании за jOOλ
Ответ 4
Вы также можете использовать flatMap
с пользовательской функцией, которая пропускает элементы:
private <T> Function<T, Stream<T>> everyNth(int n) {
return new Function<T, Stream<T>>() {
int i = 0;
@Override
public Stream<T> apply(T t) {
if (i++ % n == 0) {
return Stream.of(t);
}
return Stream.empty();
}
};
}
@Test
public void everyNth() {
assertEquals(
Arrays.asList(1, 4, 7, 10),
IntStream.rangeClosed(1, 10).boxed()
.flatMap(everyNth(3))
.collect(Collectors.toList())
);
}
У этого есть преимущество работы с неиндексированными потоками. Но не рекомендуется использовать его с параллельными потоками (возможно, переключиться на атомное целое для i
).
Ответ 5
Попробуйте это.
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
int[] n = {0};
List<Integer> result = list.stream()
.filter(x -> n[0]++ % 3 == 0)
.collect(Collectors.toList());
System.out.println(result);
// -> [1, 4, 7, 10]
Ответ 6
Вот код от AbacusUtil
Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
.filter(MutableInt.of(0), (e, idx) -> idx.getAndDecrement() % 2 == 0)
.println();
// output: 1, 3, 5, 7, 9
Или, если требуется индекс:
Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
.indexed().filter(i -> i.index() % 2 == 0).println();
// output: [0]=1, [2]=3, [4]=5, [6]=7, [8]=9
Декларация: я разработчик AbacusUtil.
Ответ 7
Используйте гуаву:
Streams
.mapWithIndex(stream, SimpleImmutableEntry::new)
.filter(entry -> entry.getValue() % 3 == 0)
.map(Entry::getKey)
.collect(Collectors.toList());
Ответ 8
Можете ли вы попробовать это
employees.stream()
.filter(e -> e.getName().charAt(0) == 's')
.skip(n-1)
.findFirst()
Ответ 9
Я пришел сюда из Как избежать переполнения памяти с помощью высокопроизводительного потока ввода-вывода JAVA от разъемов JDBC? что говорит о том, что вы обеспокоены отпечатком стопы.
Поэтому я предлагаю следующее решение, которое должно иметь небольшую скорость сбора мусора
int[] counter = new int[]{0};
list.stream()
.filter(l -> counter[0]++ % n == 0)
Конечно, вы должны убедиться, что ваш поток не параллелен.