BambooleanLogic
BambooleanLogic

Reputation: 8161

Skip last x elements in Stream<T>

If I have a Stream<T>, I can easily use skip(long) to skip the first few elements of a stream. However, there seems to be no equivalent for skipping a given number of elements at the end of the stream.

The most obvious solution is to use limit(originalLength - elementsToRemoveAtEnd), but that requires knowing the initial length beforehand, which isn't always the case.

Is there a way to remove the last few elements of a stream of unknown length without having to collect it into a Collection, count the elements and stream it again?

Upvotes: 22

Views: 20301

Answers (3)

M. Justin
M. Justin

Reputation: 21114

Here's a solution using the Stream Gatherers feature in the upcoming Java 24:

List<T> result = myStream.gather(skipLast(5)).toList()
private static <T> Gatherer<T, ?, T> skipLast(int n) {
    return Gatherer.<T, Queue<T>, T>ofSequential(
            LinkedList::new,
            Gatherer.Integrator.ofGreedy((state, element, downstream) -> {
                state.add(element);

                if (state.size() == n + 1) {
                    return downstream.push(state.remove());
                } else {
                    return true;
                }
            }));
}

This gatherer uses a queue as its state, keeping the most recent n elements in it. Whenever an additional element would be added once n elements have been kept, the first element is removed from the queue and emitted to the stream.

Once the entire stream has been sent to the gatherer, the queue will contain the last n elements of the stream, none of which will have been pushed downstream. The net result is that all but the last n elements are included (in order) in the resulting stream.

Upvotes: 1

Nathan
Nathan

Reputation: 8940

The following code uses ArrayDeque to buffer n elements where n is the number of elements to skip at the end. The trick is to use skip(n). This causes the first n elements to be added to the ArrayDeque. Then, once n elements have been buffered, the stream continues processing elements but pops elements from ArrayDeque. When the end of the stream is reached, the last n elements are stuck in the ArrayDeque and discarded.

ArrayDeque does not allow null elements. The code below maps null into NULL_VALUE before adding to ArrayDeque and then maps NULL_VALUE back to null after popping from ArrayDeque.

private static final Object NULL_VALUE = new Object();

public static <T> Stream<T> skipLast(Stream<T> input, int n)                   
{
   ArrayDeque<T> queue;

   if (n <= 0)
      return(input);

   queue = new ArrayDeque<>(n + 1);

   input = input.
      map(item -> item != null ? item : NULL_VALUE).
      peek(queue::add).
      skip(n).
      map(item -> queue.pop()).
      map(item -> item != NULL_VALUE ? item : null);

   return(input);
}

Upvotes: 1

Holger
Holger

Reputation: 298153

There is no general storage-free solution for Streams that may have an unknown length. However, you don’t need to collect the entire stream, you only need a storage as large as the number of elements you want to skip:

static <T> Stream<T> skipLastElements(Stream<T> s, int count) {
    if(count<=0) {
      if(count==0) return s;
      throw new IllegalArgumentException(count+" < 0");
    }
    ArrayDeque<T> pending=new ArrayDeque<T>(count+1);
    Spliterator<T> src=s.spliterator();
    return StreamSupport.stream(new Spliterator<T>() {
        public boolean tryAdvance(Consumer<? super T> action) {
            while(pending.size()<=count && src.tryAdvance(pending::add));
            if(pending.size()>count) {
              action.accept(pending.remove());
              return true;
            }
          return false;
        }
        public Spliterator<T> trySplit() {
            return null;
        }
        public long estimateSize() {
            return src.estimateSize()-count;
        }
        public int characteristics() {
            return src.characteristics();
        }
    }, false);
}
public static void main(String[] args) {
    skipLastElements(Stream.of("foo", "bar", "baz", "hello", "world"), 2)
    .forEach(System.out::println);
}

Upvotes: 11

Related Questions