Самый эффективный способ получить последний элемент потока

В потоке нет метода last():

Stream<T> stream;
T last = stream.last(); // No such method

Какой самый элегантный и/или эффективный способ получить последний элемент (или null для пустого потока)?

Ответ 1

Сделайте сокращение, которое просто возвращает текущее значение:

Stream<T> stream;
T last = stream.reduce((a, b) -> b).orElse(null);

Ответ 2

Это сильно зависит от характера Stream. Имейте в виду, что "простой" не обязательно означает "эффективный". Если вы подозреваете, что поток очень велик, но с тяжелыми операциями или с источником, который знает размер заранее, следующее может быть значительно более эффективным, чем простое решение:

static <T> T getLast(Stream<T> stream) {
    Spliterator<T> sp=stream.spliterator();
    if(sp.hasCharacteristics(Spliterator.SIZED|Spliterator.SUBSIZED)) {
        for(;;) {
            Spliterator<T> part=sp.trySplit();
            if(part==null) break;
            if(sp.getExactSizeIfKnown()==0) {
                sp=part;
                break;
            }
        }
    }
    T value=null;
    for(Iterator<T> it=recursive(sp); it.hasNext(); )
        value=it.next();
    return value;
}

private static <T> Iterator<T> recursive(Spliterator<T> sp) {
    Spliterator<T> prev=sp.trySplit();
    if(prev==null) return Spliterators.iterator(sp);
    Iterator<T> it=recursive(sp);
    if(it!=null && it.hasNext()) return it;
    return recursive(prev);
}

Вы можете проиллюстрировать разницу в следующем примере:

String s=getLast(
    IntStream.range(0, 10_000_000).mapToObj(i-> {
        System.out.println("potential heavy operation on "+i);
        return String.valueOf(i);
    }).parallel()
);
System.out.println(s);

Он будет печатать:

potential heavy operation on 9999999
9999999
Другими словами, он не выполнял операции над первыми элементами 9999999, а только на последнем.

Ответ 3

Это всего лишь рефакторинг Holger, потому что код, в то время как фантастический, немного трудно читать/понимать, особенно для людей, которые не были C программистов до Java. Надеюсь, мой обновленный примерный класс немного легче будет отслеживать для тех, кто не знаком с разделителями, что они делают или как они работают.

public class LastElementFinderExample {
    public static void main(String[] args){
        String s = getLast(
            LongStream.range(0, 10_000_000_000L).mapToObj(i-> {
                System.out.println("potential heavy operation on "+i);
                return String.valueOf(i);
            }).parallel()
        );
        System.out.println(s);
    }

    public static <T> T getLast(Stream<T> stream){
        Spliterator<T> sp = stream.spliterator();
        if(isSized(sp)) {
            sp = getLastSplit(sp);
        }
        return getIteratorLastValue(getLastIterator(sp));
    }

    private static boolean isSized(Spliterator<?> sp){
        return sp.hasCharacteristics(Spliterator.SIZED|Spliterator.SUBSIZED);
    }

    private static <T> Spliterator<T> getLastSplit(Spliterator<T> sp){
        return splitUntil(sp, s->s.getExactSizeIfKnown() == 0);
    }

    private static <T> Iterator<T> getLastIterator(Spliterator<T> sp) {
        return Spliterators.iterator(splitUntil(sp, null));
    }

    private static <T> T getIteratorLastValue(Iterator<T> it){
        T result = null;
        while (it.hasNext()){
            result = it.next();
        }
        return result;
    }

    private static <T> Spliterator<T> splitUntil(Spliterator<T> sp, Predicate<Spliterator<T>> condition){
        Spliterator<T> result = sp;
        for (Spliterator<T> part = sp.trySplit(); part != null; part = result.trySplit()){
            if (condition == null || condition.test(result)){
                result = part;
            }
        }
        return result;      
    }   
}

Ответ 4

Я считаю, что это решение более эффективно и доступно, чем решение Holger:

import java.util.Spliterator;
import static java.util.Spliterator.ORDERED;
import java.util.stream.Stream;

/**
 * @param <T>    the type of elements in the stream
 * @param stream a stream
 * @return the last element in the stream
 * @throws AssertionError if the stream is unordered
 */
public static <T> Optional<T> getLastElement(Stream<T> stream)
{
    Spliterator<T> spliterator = stream.spliterator();
    assert (spliterator.hasCharacteristics(ORDERED)): "Operation makes no sense on unordered streams";

    // First we skip as many elements as possible
    Consumer<T> noOp = input -> {};
    while (true) {
        // trySplit() moves the first spliterator forward by the size of the second spliterator
        Spliterator<T> second = spliterator.trySplit();
        if (second == null)
            break;
        if (!spliterator.tryAdvance(noOp)) {
            // If the first spliterator is empty, continue splitting the second spliterator
            spliterator = second;
        }
    }

    // Then we consume the last element(s)
    LastElementConsumer<T> consumer = new LastElementConsumer<>();
    spliterator.forEachRemaining(consumer);
    return consumer.get();
}

[...]

import java.util.Optional;
import java.util.function.Consumer;

/**
 * A consumer that returns the last value that was consumed.
 * <p>
 * @param <T> the type of elements to consume
 * @author Gili Tzabari
 */
public final class LastElementConsumer<T> implements Consumer<T>
{
    private Optional<T> result = Optional.empty();

    @Override
    public void accept(T t)
    {
        result = Optional.of(t);
    }

    /**
     * @return the last value that was consumed
     */
    public Optional<T> get()
    {
        return result;
    }
}

Если вы запустите:

String s = getLastElement(IntStream.range(0, 10_000_000).mapToObj(i->
  {
    System.out.println("Potential heavy operation on " + i);
    return String.valueOf(i);
  }).parallel()
);
System.out.println(s);

он напечатает тот же результат, что и решение Holger:

Potential heavy operation on 9999999
9999999
Другими словами, он не выполнял операции над первыми элементами 9999999, а только на последнем.

Ответ 5

Вот еще одно решение (не так эффективно):

List<String> list = Arrays.asList("abc","ab","cc");
long count = list.stream().count();
list.stream().skip(count-1).findFirst().ifPresent(System.out::println);

Ответ 6

Параллельные необработанные потоки с методами "skip" сложны, и реализация @Holger дает неправильный ответ. Кроме того, реализация @Holger немного медленнее, поскольку использует итераторы.

Оптимизация ответа @Holger:

public static <T> Optional<T> last(Stream<? extends T> stream) {
    Objects.requireNonNull(stream, "stream");

    Spliterator<? extends T> spliterator = stream.spliterator();
    Spliterator<? extends T> lastSpliterator = spliterator;

    // Note that this method does not work very well with:
    // unsized parallel streams when used with skip methods.
    // on that cases it will answer Optional.empty.

    // Find the last spliterator with estimate size
    // Meaningfull only on unsized parallel streams
    if(spliterator.estimateSize() == Long.MAX_VALUE) {
        for (Spliterator<? extends T> prev = spliterator.trySplit(); prev != null; prev = spliterator.trySplit()) {
            lastSpliterator = prev;
        }
    }

    // Find the last spliterator on sized streams
    // Meaningfull only on parallel streams (note that unsized was transformed in sized)
    for (Spliterator<? extends T> prev = lastSpliterator.trySplit(); prev != null; prev = lastSpliterator.trySplit()) {
        if (lastSpliterator.estimateSize() == 0) {
            lastSpliterator = prev;
            break;
        }
    }

    // Find the last element of the last spliterator
    // Parallel streams only performs operation on one element
    AtomicReference<T> last = new AtomicReference<>();
    lastSpliterator.forEachRemaining(last::set);

    return Optional.ofNullable(last.get());
}

Тестирование модулей с помощью junit 5:

@Test
@DisplayName("last sequential sized")
void last_sequential_sized() throws Exception {
    long expected = 10_000_000L;
    AtomicLong count = new AtomicLong();
    Stream<Long> stream = LongStream.rangeClosed(1, expected).boxed();
    stream = stream.skip(50_000).peek(num -> count.getAndIncrement());

    assertThat(Streams.last(stream)).hasValue(expected);
    assertThat(count).hasValue(9_950_000L);
}

@Test
@DisplayName("last sequential unsized")
void last_sequential_unsized() throws Exception {
    long expected = 10_000_000L;
    AtomicLong count = new AtomicLong();
    Stream<Long> stream = LongStream.rangeClosed(1, expected).boxed();
    stream = StreamSupport.stream(((Iterable<Long>) stream::iterator).spliterator(), stream.isParallel());
    stream = stream.skip(50_000).peek(num -> count.getAndIncrement());

    assertThat(Streams.last(stream)).hasValue(expected);
    assertThat(count).hasValue(9_950_000L);
}

@Test
@DisplayName("last parallel sized")
void last_parallel_sized() throws Exception {
    long expected = 10_000_000L;
    AtomicLong count = new AtomicLong();
    Stream<Long> stream = LongStream.rangeClosed(1, expected).boxed().parallel();
    stream = stream.skip(50_000).peek(num -> count.getAndIncrement());

    assertThat(Streams.last(stream)).hasValue(expected);
    assertThat(count).hasValue(1);
}

@Test
@DisplayName("getLast parallel unsized")
void last_parallel_unsized() throws Exception {
    long expected = 10_000_000L;
    AtomicLong count = new AtomicLong();
    Stream<Long> stream = LongStream.rangeClosed(1, expected).boxed().parallel();
    stream = StreamSupport.stream(((Iterable<Long>) stream::iterator).spliterator(), stream.isParallel());
    stream = stream.peek(num -> count.getAndIncrement());

    assertThat(Streams.last(stream)).hasValue(expected);
    assertThat(count).hasValue(1);
}

@Test
@DisplayName("last parallel unsized with skip")
void last_parallel_unsized_with_skip() throws Exception {
    long expected = 10_000_000L;
    AtomicLong count = new AtomicLong();
    Stream<Long> stream = LongStream.rangeClosed(1, expected).boxed().parallel();
    stream = StreamSupport.stream(((Iterable<Long>) stream::iterator).spliterator(), stream.isParallel());
    stream = stream.skip(50_000).peek(num -> count.getAndIncrement());

    // Unfortunately unsized parallel streams does not work very well with skip
    //assertThat(Streams.last(stream)).hasValue(expected);
    //assertThat(count).hasValue(1);

    // @Holger implementation gives wrong answer!!
    //assertThat(Streams.getLast(stream)).hasValue(9_950_000L); //!!!
    //assertThat(count).hasValue(1);

    // This is also not a very good answer better
    assertThat(Streams.last(stream)).isEmpty();
    assertThat(count).hasValue(0);
}

Единственное решение, поддерживающее оба сценария, заключается в том, чтобы избежать обнаружения последнего разделителя на нестандартных параллельных потоках. Следствием этого является то, что решение будет выполнять операции над всеми элементами, но оно всегда даст правильный ответ.

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

public static <T> Optional<T> last(Stream<? extends T> stream) {
    Objects.requireNonNull(stream, "stream");

    Spliterator<? extends T> spliterator = stream.spliterator();

    // Find the last spliterator with estimate size (sized parallel streams)
    if(spliterator.hasCharacteristics(Spliterator.SIZED|Spliterator.SUBSIZED)) {
        // Find the last spliterator on sized streams (parallel streams)
        for (Spliterator<? extends T> prev = spliterator.trySplit(); prev != null; prev = spliterator.trySplit()) {
            if (spliterator.getExactSizeIfKnown() == 0) {
                spliterator = prev;
                break;
            }
        }
    }

    // Find the last element of the spliterator
    //AtomicReference<T> last = new AtomicReference<>();
    //spliterator.forEachRemaining(last::set);

    //return Optional.ofNullable(last.get());

    // A better one that supports native parallel streams
    return (Optional<T>) StreamSupport.stream(spliterator, stream.isParallel())
            .reduce((a, b) -> b);
}

Что касается модульного тестирования для этой реализации, первые три теста одинаковы (последовательный и размерный параллели). Тесты для несимметричной параллели здесь:

@Test
@DisplayName("last parallel unsized")
void last_parallel_unsized() throws Exception {
    long expected = 10_000_000L;
    AtomicLong count = new AtomicLong();
    Stream<Long> stream = LongStream.rangeClosed(1, expected).boxed().parallel();
    stream = StreamSupport.stream(((Iterable<Long>) stream::iterator).spliterator(), stream.isParallel());
    stream = stream.peek(num -> count.getAndIncrement());

    assertThat(Streams.last(stream)).hasValue(expected);
    assertThat(count).hasValue(10_000_000L);
}

@Test
@DisplayName("last parallel unsized with skip")
void last_parallel_unsized_with_skip() throws Exception {
    long expected = 10_000_000L;
    AtomicLong count = new AtomicLong();
    Stream<Long> stream = LongStream.rangeClosed(1, expected).boxed().parallel();
    stream = StreamSupport.stream(((Iterable<Long>) stream::iterator).spliterator(), stream.isParallel());
    stream = stream.skip(50_000).peek(num -> count.getAndIncrement());

    assertThat(Streams.last(stream)).hasValue(expected);
    assertThat(count).hasValue(9_950_000L);
}

Ответ 7

Guava имеет Streams.findLast:

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