Подтвердить что ты не робот

Возьмите каждый 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]
4b9b3361

Ответ 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)

Конечно, вы должны убедиться, что ваш поток не параллелен.