44

I am wondering is there an alternative to

List<X> lastN = all.subList(Math.max(0, all.size() - n), all.size());

with stream usage?

GEOCHET
  • 21,119
  • 15
  • 74
  • 98
ytterrr
  • 3,036
  • 6
  • 23
  • 32
  • 1
    I don't think this is generally possible with streams, as a stream's size may not be known a priori, or it may even be infinite. And if you create the stream from a list, just use sublist, as you did. – tobias_k May 27 '15 at 08:04
  • 1
    @tobias_k the OP seems to have a finite list however... – Puce May 27 '15 at 08:06
  • 1
    If you already have a list, then `subList` is the way to go. You can then copy it, stream it, whatever else you want. – Brian Goetz May 27 '15 at 16:28

4 Answers4

42

Use Stream.skip()

Returns a stream consisting of the remaining elements of this stream after discarding the first n elements of the stream. If this stream contains fewer than n elements then an empty stream will be returned.

all.stream().skip(Math.max(0, all.size() - n)).forEach(doSomething);
ZhekaKozlov
  • 36,558
  • 20
  • 126
  • 155
Jordi Castilla
  • 26,609
  • 8
  • 70
  • 109
  • 1
    This doesn't return the last n elements. It skips the first n. not what OP asked for. – Bohemian May 27 '15 at 08:00
  • 4
    what if we do not know about stream size (assume worst case)? – ytterrr May 27 '15 at 08:11
  • 3
    This throws `IllegalArgumentException` if `all.size() < n`. The `Math.max(0, all.size() - n)` from the original question will be necessary here as well. – Jan Zyka May 27 '15 at 08:20
29

A custom collector can be written like this:

public static <T> Collector<T, ?, List<T>> lastN(int n) {
    return Collector.<T, Deque<T>, List<T>>of(ArrayDeque::new, (acc, t) -> {
        if(acc.size() == n)
            acc.pollFirst();
        acc.add(t);
    }, (acc1, acc2) -> {
        while(acc2.size() < n && !acc1.isEmpty()) {
            acc2.addFirst(acc1.pollLast());
        }
        return acc2;
    }, ArrayList::new);
}

And use it like this:

List<String> lastTen = input.stream().collect(lastN(10));
Marci-man
  • 2,113
  • 3
  • 28
  • 76
Tagir Valeev
  • 97,161
  • 19
  • 222
  • 334
  • 2
    You don’t need to write `ArrayList::new`, just `ArrayList::new` is enough as method references always use type inference rather than raw types (like if the “diamond operator” was always present when using `::new`). You already use it with `ArrayDeque::new`. Btw. I’d prefer `removeFirst`/`removeLast` over `pollFirst`/`pollLast` here… – Holger May 27 '15 at 13:05
  • 2
    @Holger, first I wrote `ArrayList::new`, but Eclipse displayed a warning about unchecked constructor. Well, probably that's an ECJ-specific problem. – Tagir Valeev May 27 '15 at 13:10
  • Interestingly, the `ArrayDeque::new` would benefit from a type witness, as using `ArrayDeque::new` would make the type witness at the `Collector.of` call obsolete (and `` is simpler than `, List>`) whereas the type witness at `ArrayList::new` is not necessary as its type can be inferred from the target type. – Holger Oct 20 '16 at 08:27
4

In case the stream has unknown size, there's probably no way around consuming the entire stream and buffering the last n elements encountered so far. You can do this using some kind of deque, or a specialized ring-buffer automatically maintaining its maximum size (see this related question for some implementations).

public static <T> List<T> lastN(Stream<T> stream, int n) {
    Deque<T> result = new ArrayDeque<>(n);
    stream.forEachOrdered(x -> {
        if (result.size() == n) {
            result.pop();
        }
        result.add(x);
    });
    return new ArrayList<>(result);
}

All of those operations (size, pop, add) should have complexity of O(1), so the overall complexity for a stream with (unknown) length n would be O(n).

Community
  • 1
  • 1
tobias_k
  • 81,265
  • 12
  • 120
  • 179
3

Sometimes I need a "oneliner" (in this case a three liner) as creating a collector is just too much fuss.

If the stream is small then it is possible to reverse, limit and reverse again without much sacrificing performance. This will result the last n elements.

It is useful if filtering is required as in that case it is not possible to specify the size.

Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
  .filter(i -> i % 2 == 0)
  .sorted(Comparator.reverseOrder())
  .limit(2)
  .sorted(Comparator.naturalOrder())
  .forEach(System.out::println); // prints 6 8
Gebezs
  • 696
  • 3
  • 9