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

123,426

Solution 1

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();

Solution 2

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

Iterables.getLast(myIterable)

as handy oneliner.

Solution 3

One liner (no need for stream;):

Object lastElement = list.isEmpty() ? null : list.get(list.size()-1);

Solution 4

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().

Solution 5

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.

Share:
123,426
skiwi
Author by

skiwi

Studied Computer Science at the Eindhoven University of Technology. I used to be quite experienced in working with Java and have experience in PHP/MySQL/Javascript/jQuery/CSS/HTML aswell. Nowadays I work more often with C# and have some side projects in other languages. Feel free to check what I am working at on Github or to contact me at LinkedIn.

Updated on July 08, 2021

Comments

  • skiwi
    skiwi almost 3 years

    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.

    • Aleksandr Dubinsky
      Aleksandr Dubinsky over 10 years
      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.
    • fasth
      fasth about 9 years
      @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 :)
    • quantum
      quantum almost 9 years
      For a true one-liner, this thread may be of use.
  • skiwi
    skiwi over 10 years
    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.
  • assylias
    assylias over 10 years
    @skiwi you can use any legal variable name, for example: .reduce(($, current) -> current) or .reduce((__, current) -> current) (double underscore).
  • Aleksandr Dubinsky
    Aleksandr Dubinsky over 10 years
    I doubt this will work for parallel streams, even if they're ordered.
  • nosid
    nosid over 10 years
    @AleksandrDubinsky: Why shouldn't it work for parallel streams? Take a look at the documentation: Reduction Operations.
  • Aleksandr Dubinsky
    Aleksandr Dubinsky over 10 years
    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.
  • nosid
    nosid over 10 years
    @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 [..]."
  • Aleksandr Dubinsky
    Aleksandr Dubinsky over 10 years
    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.
  • Aleksandr Dubinsky
    Aleksandr Dubinsky over 10 years
    That said, the current implementation (from looking at java.util.stream.ReduceOps) seems to obey encounter order.
  • René
    René over 9 years
    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."
  • FBB
    FBB over 8 years
    I added an answer trying to address the concerns in these comments.
  • Holger
    Holger about 8 years
    @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.
  • Aleksandr Dubinsky
    Aleksandr Dubinsky about 8 years
    @Holger Define "relevance." Anyway, to reiterate my point, which noone except @FBB 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.
  • Holger
    Holger about 8 years
    @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 solution
  • Holger
    Holger about 8 years
    @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.
  • Aleksandr Dubinsky
    Aleksandr Dubinsky about 8 years
    @Holger 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.
  • Holger
    Holger about 8 years
    @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.
  • devssh
    devssh about 6 years
    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 element
  • fernandohur
    fernandohur almost 6 years
    @devssh 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.
  • devssh
    devssh almost 6 years
    @fernandohur 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()?
  • java.is.for.desktop
    java.is.for.desktop over 5 years
    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.
  • Eugene
    Eugene almost 5 years
    @ZhekaKozlov sort of... Holger's showed some flaws with it here
  • Jindra Vysocký
    Jindra Vysocký over 4 years
    also if list is empty, it will throw ArrayIndexOutOfBoundsException
  • shmosel
    shmosel over 4 years
    And you can easily convert a stream to an iterable: Iterables.getLast(() -> data.careas.stream().filter(c -> c.bbox.orientationHorizontal).iterator())
  • MC Emperor
    MC Emperor almost 4 years
    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.
  • Don Woodward
    Don Woodward over 3 years
    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 :D