Get last element of Stream/List in a one-liner
Asked Answered
C

10

164

How can I get the last element of a stream or list in the following code?

Where data.careas is a 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?

As you can see getting the first element, with a certain filter, is not hard.

However getting the last element in a one-liner is a real pain:

  • It seems I cannot obtain it directly from a Stream. (It would only make sense for finite streams)
  • It also seems that you cannot get things like first() and last() from the List interface, which is really a pain.

I do not see any argument for not providing a first() and last() method in the List interface, as the elements in there, are ordered, and moreover the size is known.

But as per the original answer: How to get the last element of a finite Stream?

Personally, this is the closest I could get:

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

However it does involve, using an indexOf on every element, which is most likely not you generally want as it can impair performance.

Clerc answered 29/1, 2014 at 9:25 Comment(3)
Guava provides Iterables.getLast which takes Iterable but is optimized to work with List. A pet peeve is that it doesn't have getFirst. The Stream API in general is horribly anal, omitting lots of convenience methods. C#'s LINQ, by constrast, is happy to provide .Last() and even .Last(Func<T,Boolean> predicate), even though it supports infinite Enumerables too.Cognizance
@AleksandrDubinsky upvoted, but one note for readers. Stream API is not fully comparable to LINQ since both done in a very different paradigm. It is not worse or better it is just different. And definitely some methods are absent not because oracle devs are incompetent or mean :)Padova
For a true one-liner, this thread may be of use.Cosignatory
S
259

It is possible to get the last element with the method Stream::reduce. The following listing contains a minimal example for the general case:

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

This implementations works for all ordered streams (including streams created from Lists). For unordered streams it is for obvious reasons unspecified which element will be returned.

The implementation works for both sequential and parallel streams. That might be surprising at first glance, and unfortunately the documentation doesn't state it explicitly. However, it is an important feature of streams, and I try to clarify it:

  • The Javadoc for the method Stream::reduce states, that it "is not constrained to execute sequentially".
  • The Javadoc also requires that the "accumulator function must be an associative, non-interfering, stateless function for combining two values", which is obviously the case for the lambda expression (first, second) -> second.
  • The Javadoc for reduction operations states: "The streams classes have multiple forms of general reduction operations, called reduce() and collect() [..]" and "a properly constructed reduce operation is inherently parallelizable, so long as the function(s) used to process the elements are associative and stateless."

The documentation for the closely related Collectors is even more explicit: "To ensure that sequential and parallel executions produce equivalent results, the collector functions must satisfy an identity and an associativity constraints."


Back to the original question: The following code stores a reference to the last element in the variable last and throws an exception if the stream is empty. The complexity is linear in the length of the stream.

CArea last = data.careas
                 .stream()
                 .filter(c -> c.bbox.orientationHorizontal)
                 .reduce((first, second) -> second).get();
Surrejoinder answered 29/1, 2014 at 20:13 Comment(20)
Nice one, thanks! Do you by the way know if it is possibly to omit a name (perhaps by using a _ or similar) in cases where you do not need a parameter? So would be: .reduce((_, current) -> current) if only that aws valid syntax.Clerc
@Clerc you can use any legal variable name, for example: .reduce(($, current) -> current) or .reduce((__, current) -> current) (double underscore).Bowlds
I doubt this will work for parallel streams, even if they're ordered.Cognizance
@AleksandrDubinsky: Why shouldn't it work for parallel streams? Take a look at the documentation: Reduction Operations.Surrejoinder
Technically, it may not work for any streams. The documentation that you point to, as well as for Stream.reduce(BinaryOperator<T>) makes no mention if reduce obeys encounter order, and a terminal operation is free to ignore encounter order even if the stream is ordered. As an aside, the word "commutative" doesn't appear in the Stream javadocs, so its absence does not tell us much.Cognizance
@AleksandrDubinsky: Exactly, the documentation doesn't mention commutative, because it is not relevant for the reduce operation. The important part is: "[..] A properly constructed reduce operation is inherently parallelizable, so long as the function(s) used to process the elements are associative [..]."Surrejoinder
The documentation doesn't mention "commutative" anywhere, even though it's relevant to many functions. The terminology that it uses is "encounter order", and the reduce documentation makes no mention of it one way or the other. Therefore, we must assume that reduce does not obey ordering, and that a non-commutative function will produce undefined results.Cognizance
That said, the current implementation (from looking at java.util.stream.ReduceOps) seems to obey encounter order.Cognizance
The class-comment of java.util.Stream says "Note that if it is important that the elements for a given key appear in the order they appear in the source, then we cannot use a concurrent reduction, as ordering is one of the casualties of concurrent insertion. We would then be constrained to implement either a sequential reduction or a merge-based parallel reduction."Bestraddle
I added an answer trying to address the concerns in these comments.Henceforward
@Aleksandr Dubinsky: there is no relevance of commutativity to any function in the Stream API. If you want to claim otherwise, name at least one for which it is.Mosa
@Mosa Define "relevance." Anyway, to reiterate my point, which noone except @Henceforward seems to have appreciated, is that "orderness" of a Stream means nothing to a terminal operation, which is free to not declaring itself as preserving "encounter order." Search for this term in the javadocs. reduce happily leaves this aspect undefined. We must conclude, therefore, that conformant implementations are free to produce somewhat undefined results if the lambda is not commutative. Stream API sucks. All this drama over a trifle.Cognizance
@Aleksandr Dubinsky: I like how you interpret the absence of a statement as “the implementor is free to do something undocumented”. That contradicts the fact that every method which is allowed to ignore the encounter order has an explicit statement about it, but never mind. Note that the documentation regarding Associativity clearly explains how that property relates to concurrent execution strategies and reduce((a,b)->b) is an acknowledged solutionMosa
@Aleksandr Dubinsky: don’t get me wrong, I have to admit that the documentation has room for improvements, similar to this issue, but if someone like the Java Language Architect involved in the development of that API acknowledges that it is the intended way it should work, I’ll consider the deficiencies of the documentation as exactly that, documentation flaws, rather than room for contradicting implementations.Mosa
@Mosa Whether reduce does or does not respect encounter order is not just a theoretical question of the spec. It's hugely important, because when a terminal operation does not respect encounter order, the whole pipeline becomes unordered. For example, the operation skip goes crazy on unordered parallel streams and can skip the last element for a short stream. This answer is possibly/likely wrong in the general case. The fact this isn't defined in the javadoc or the spec is criminal. Stream API sucks.Cognizance
@Aleksandr Dubinsky: of course, it’s not a “theoretical question of the spec”. It makes the difference between reduce((a,b)->b) being a correct solution for getting the last element (of an ordered stream,of course) or not. The statement of Brian Goetz makes a point, further the API documentation states that reduce("", String::concat) is an inefficient but correct solution for string concatenation, which implies maintenance of the encounter order.The intention is well-known,the documentation has to catch up.Mosa
What is the efficiency of this approach? Linear complexity is pretty bad. It seems to be going over the list reducing till it reaches the last element. What if the list is really big? Wouldn't it be faster if it was hashed and there was an inbuilt method to just get the last elementLashondra
@Lashondra Streams are lazy constructs, you cannot assume that you have all elements in the stream in memory at all times so you can't do better than O(n) in general.Thoer
@Thoer Yes I agree, but this feels like a mapreduce to get the last element while all it needs is to access the last element by memory. Looks like the job of an array but arrays are inherently imperative instead of declarative. Fits the criteria for an inbuilt method called last()?Lashondra
For readers for which it is not immediately clear where get() comes from: reduce(BinaryOperator) returns an Optional containing the element, or an empty optional if the stream is empty. get() will in such case throw a NoSuchElementException.Mercurial
C
55

If you have a Collection (or more general an Iterable) you can use Google Guava's

Iterables.getLast(myIterable)

as handy oneliner.

Carmel answered 7/10, 2015 at 7:34 Comment(1)
And you can easily convert a stream to an iterable: Iterables.getLast(() -> data.careas.stream().filter(c -> c.bbox.orientationHorizontal).iterator())Jugendstil
G
20

One liner (no need for stream;):

Object lastElement = list.isEmpty() ? null : list.get(list.size()-1);
Girosol answered 30/8, 2017 at 7:48 Comment(0)
B
15

Guava has dedicated method for this case:

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

It's equivalent to stream.reduce((a, b) -> b) but creators claim it has much better performance.

From documentation:

This method's runtime will be between O(log n) and O(n), performing better on efficiently splittable streams.

It's worth to mention that if stream is unordered this method behaves like findAny().

Brouwer answered 7/5, 2019 at 18:24 Comment(2)
This should be an accepted answerBulbul
@Bulbul sort of... Holger's showed some flaws with it hereNavel
F
6
list.stream().sorted(Comparator.comparing(obj::getSequence).reversed()).findFirst().get();

reverse the order and get the first element from the list. here object has sequence number, Comparator provides multiple functionalities can be used as per logic.

Franciscka answered 8/7, 2021 at 3:49 Comment(1)
You can use max() like this : list.stream().max(Comparator.comparing(obj::getSequence)).get();Scab
E
1

Another way to get the last element is by using sort.

    Optional<CArea> num=data.careas.stream().sorted((a,b)->-1).findFirst();
Engineman answered 4/11, 2020 at 3:56 Comment(0)
C
1

Java 21 added the getLast() method to the List interface, which returns the last element of the list.

CArea last = data.careas.stream()
                 .filter(c -> c.bbox.orientationHorizontal)
                 .collect(Collectors.toList())
                 .getLast();

Also added was the getFirst() method, which addresses your point about not having this functionality on the List interface being a pain.

Chrisom answered 14/8, 2023 at 5:18 Comment(1)
As it's Java 21, then collect(Collectors.toList()) can be replaced with just toList(). But also note that it will throw NoSuchElementException if the list will be empty.Hooker
A
0

You can also use skip() function as below...

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

it's super simple to use.

Alcoholicity answered 3/7, 2018 at 10:50 Comment(3)
Note: you shouldn't rely on stream's "skip" when dealing with huge collections (millions of entries), because "skip" is implemented by iterating through all elements until the Nth number is reached. Tried it. Was very disappointed by the performance, compared to a simple get-by-index operation.Tao
also if list is empty, it will throw ArrayIndexOutOfBoundsExceptionHenbit
What if its not a list ? Get wont be available , I had a requirement to take a set and stream it and then the above solution was working. There is no option in linkedhashset to get last or nth element from back without iterating thru elements. I upvoted this to save the author from drowning :DHeterotrophic
S
0

One more approach. Pair will have first and last elements:

    List<Object> pair = new ArrayList<>();
    dataStream.ForEach(o -> {
        if (pair.size() == 0) {
            pair.add(o);
            pair.add(o);
        }
        pair.set(1, o);
    });
Schellens answered 4/6, 2022 at 14:28 Comment(0)
N
-1

If you need to get the last N number of elements. Closure can be used. The below code maintains an external queue of fixed size until, the stream reaches the end.

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

Another option could be to use reduce operation using identity as a Queue.

    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);
Niphablepsia answered 5/5, 2020 at 16:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.