Получить последний элемент Stream / List в однострочном формате

119

Как я могу получить последний элемент потока или списка в следующем коде?

Где data.careasнаходится List<CArea>:

CArea first = data.careas.stream()
                  .filter(c -> c.bbox.orientationHorizontal).findFirst().get();

CArea last = data.careas.stream()
                 .filter(c -> c.bbox.orientationHorizontal)
                 .collect(Collectors.toList()).; //how to?

Как видите, получить первый элемент filterне сложно.

Однако получение последнего элемента в однострочнике - настоящая боль:

  • Кажется, я не могу получить его напрямую от Stream. (Это имело бы смысл только для конечных потоков)
  • Также кажется, что вы не можете получить что-то вроде first()и last()из Listинтерфейса, что действительно неприятно.

Я не вижу никаких аргументов для не обеспечивая first()и last()метод в Listинтерфейсе, как элементы в наличии, упорядочены, и , кроме того размер известен.

Но согласно первоначальному ответу: как получить последний элемент конечного Stream?

Лично мне это ближе всего:

int lastIndex = data.careas.stream()
        .filter(c -> c.bbox.orientationHorizontal)
        .mapToInt(c -> data.careas.indexOf(c)).max().getAsInt();
CArea last = data.careas.get(lastIndex);

Однако это подразумевает использование для indexOfкаждого элемента, что, скорее всего, вам обычно не нужно, поскольку это может снизить производительность.

skiwi
источник
10
Guava предоставляет, Iterables.getLastкоторый использует Iterable, но оптимизирован для работы List. Любимая мозоль в том, что этого нет getFirst. StreamAPI вообще ужасно анальный, опуская множество удобных методов. LINQ C #, напротив, рад предоставить .Last()и даже .Last(Func<T,Boolean> predicate), хотя он также поддерживает бесконечные перечисления.
Александр Дубинский
@AleksandrDubinsky проголосовал за, но одно замечание для читателей. StreamAPI нельзя полностью сопоставить с ним, LINQпоскольку оба выполнены в совершенно другой парадигме. Это не хуже и не лучше, это просто другое. И определенно некоторые методы отсутствуют не потому, что разработчики оракула некомпетентны или
злы
1
Для истинной однострочности эта нить может пригодиться.
квант

Ответы:

186

Получить последний элемент можно методом Stream :: reduce . Следующий листинг содержит минимальный пример для общего случая:

Stream<T> stream = ...; // sequential or parallel stream
Optional<T> last = stream.reduce((first, second) -> second);

Эта реализация работает для всех упорядоченных потоков (включая потоки, созданные из списков ). Для неупорядоченных потоков по очевидным причинам не указано, какой элемент будет возвращен.

Реализация работает как для последовательных, так и для параллельных потоков . На первый взгляд это может показаться неожиданным, и, к сожалению, в документации об этом прямо не говорится. Однако это важная особенность потоков, и я пытаюсь ее прояснить:

  • В Javadoc для метода Stream :: reduce говорится, что он « не ограничен для последовательного выполнения » .
  • Javadoc также требует, чтобы «функция аккумулятора должна представлять собой ассоциативный , без мешающего , без сохранения функции для объединения двух значений» , которое, очевидно , имеет место для лямбда - выражения(first, second) -> second .
  • В Javadoc для операций сокращения говорится: «Классы потоков имеют несколько форм общих операций сокращения, называемых reduce () и collect () [..]», и «правильно построенная операция сокращения по своей сути является распараллеливаемой , если функция (s ), используемые для обработки элементов, являются ассоциативными и не имеют состояния ".

Документация для тесно связанных сборщиков еще более ясна: «Чтобы гарантировать, что последовательное и параллельное выполнение дает эквивалентные результаты , функции сборщика должны удовлетворять ограничениям идентичности и ассоциативности ».


Вернемся к исходному вопросу: следующий код сохраняет ссылку на последний элемент переменной lastи выдает исключение, если поток пуст. Сложность линейна по длине потока.

CArea last = data.careas
                 .stream()
                 .filter(c -> c.bbox.orientationHorizontal)
                 .reduce((first, second) -> second).get();
nosid
источник
Хороший, спасибо! Кстати, знаете ли вы, можно ли пропустить имя (возможно, используя _или аналогичный) в тех случаях, когда вам не нужен параметр? Так было бы: .reduce((_, current) -> current)если бы только этот правильный синтаксис.
skiwi
2
@skiwi вы можете использовать любое допустимое имя переменной, например: .reduce(($, current) -> current)или .reduce((__, current) -> current)(двойное подчеркивание).
assylias
2
Технически это может не работать ни с одним потоком. Документация, на которую вы указываете, а также для Stream.reduce(BinaryOperator<T>)не упоминает, reduceподчиняется ли порядок встреч, а операция терминала может игнорировать порядок встреч, даже если поток упорядочен. Кстати, слово «коммутативный» не появляется в javadocs Stream, поэтому его отсутствие мало что говорит нам.
Александр Дубинский
2
@AleksandrDubinsky: Точно, в документации не упоминается коммутативность , потому что она не имеет отношения к операции сокращения . Важная часть: «[..] Правильно построенная операция сокращения по своей природе распараллеливается, если функция (и), используемые для обработки элементов, являются ассоциативными [..]».
nosid 05
2
@ Александр Дубинский: конечно, это не «теоретический вопрос спецификации». От этого зависит, reduce((a,b)->b)является ли правильное решение для получения последнего элемента (разумеется, в упорядоченном потоке) или нет. В заявлении Брайана Гетца говорится о том , что далее в документации API указано, что reduce("", String::concat)это неэффективное, но правильное решение для конкатенации строк, которое подразумевает поддержание порядка встреч. Намерение хорошо известно, документация должна наверстать упущенное.
Хольгер
42

Если у вас есть коллекция (или в более общем плане Iterable), вы можете использовать Google Guava

Iterables.getLast(myIterable)

как удобный oneliner.

Пети
источник
1
И вы можете легко преобразовать поток в повторяющийся:Iterables.getLast(() -> data.careas.stream().filter(c -> c.bbox.orientationHorizontal).iterator())
shmosel
10

Один лайнер (поток не нужен;):

Object lastElement = list.get(list.size()-1);
nimo23
источник
30
Если список пуст, этот код выбросит ArrayIndexOutOfBoundsException.
Dragon
8

У Guava есть специальный метод для этого случая:

Stream<T> stream = ...;
Optional<T> lastItem = Streams.findLast(stream);

Это эквивалентно stream.reduce((a, b) -> b) но создатели утверждают, что у него намного лучшая производительность.

Из документации :

Время выполнения этого метода будет между O (log n) и O (n), что позволит лучше работать с эффективно разделяемыми потоками.

Стоит отметить, что если поток неупорядочен, этот метод ведет себя как findAny().

k13i
источник
1
@ZhekaKozlov рода ... Holger показал некоторые недостатки с ним здесь
EUGENE
0

Если вам нужно получить последние N элементов. Можно использовать закрытие. Приведенный ниже код поддерживает внешнюю очередь фиксированного размера до тех пор, пока поток не достигнет конца.

    final Queue<Integer> queue = new LinkedList<>();
    final int N=5;
    list.stream().peek((z) -> {
        queue.offer(z);
        if (queue.size() > N)
            queue.poll();
    }).count();

Другой вариант - использовать операцию сокращения с использованием идентификатора в качестве очереди.

    final int lastN=3;
    Queue<Integer> reduce1 = list.stream()
    .reduce( 
        (Queue<Integer>)new LinkedList<Integer>(), 
        (m, n) -> {
            m.offer(n);
            if (m.size() > lastN)
               m.poll();
            return m;
    }, (m, n) -> m);

    System.out.println("reduce1 = " + reduce1);
Химаншу Ахире
источник
-1

Вы также можете использовать функцию skip (), как показано ниже ...

long count = data.careas.count();
CArea last = data.careas.stream().skip(count - 1).findFirst().get();

это очень просто в использовании.

Параг Вайдья
источник
Примечание: вы не должны полагаться на «пропуск» потока при работе с огромными коллекциями (миллионами записей), потому что «пропуск» реализуется путем повторения всех элементов до тех пор, пока не будет достигнуто N-е число. Пробовал. Был очень разочарован производительностью по сравнению с простой операцией получения по индексу.
java.is.for.desktop 01
1
также, если список пуст, он выбрасываетArrayIndexOutOfBoundsException
Jindra Vysocký