Найефективніший спосіб отримати останній елемент потоку


76

Потік не має last()методу:

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

Який найелегантніший та / або найефективніший спосіб отримати останній елемент (або нульовий для порожнього потоку)?


4
Якщо вам потрібно знайти останній елемент a Stream, ви можете переглянути свій дизайн, і якщо ви дійсно хочете використовувати a Stream. Streams не обов'язково впорядковані або скінченні. Якщо ваш Streamневпорядкований, нескінченний або обидва, останній елемент не має значення. На мою думку, сенс a Streamполягає у забезпеченні рівня абстракції між даними та способом їх обробки. Таким чином, Streamсамому собі не потрібно нічого знати про відносне впорядкування його елементів. Знаходження останнього елемента в a Stream- це O (n). Якщо у вас інша структура даних, це може бути O (1).
Джеффрі,

1
@jeff потреба була реальною: ситуація грубо додавала товари в кошик для покупок, кожне додавання повертало інформацію про помилку (певні комбінації елементів були невірними), але лише інформацію про помилку останнього додавання (коли всі елементи були додані та справедливий оцінка кошика може бути зроблена) була потрібна інформація. (Так, API, який ми використовуємо, не працює і його неможливо виправити).
Чеська

14
@BrianGoetz: Нескінченні потоки теж не мають чітко визначеного count(), але Stream все ще має count()метод. Дійсно, цей аргумент застосовується до будь-якої операції терміналу, що не замикається на нескінченні потоки.
Джеффрі Босбум,

@BrianGoetz Я думаю, що потоки повинні мати last()метод. 1 квітня може бути проведено опитування про те, як це слід визначити для нескінченних потоків. Я б запропонував: "Він ніколи не повертається, і він використовує принаймні одне ядро ​​процесора на 100%. У паралельних потоках потрібно використовувати всі ядра на 100%".
Войта

Якщо список містить об'єкти з природним порядком або які можна замовити, ви можете використовувати max()метод, як у stream()...max(Comparator...).
Ерк

Відповіді:


123

Зробіть зменшення, яке просто повертає поточне значення:

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

2
Ви б сказали, що це було елегантно, ефективно або і те, і інше?
Дункан Джонс,

1
@Duncan Я думаю, що це і те, і інше, але я ще не пістолет у Java 8, і ця потреба з’явилася на роботі днями - молодший підштовхнув потік на стек, а потім вибив його, і я подумав, що це виглядає краще, але там може бути щось ще простіше.
Чеська

19
Для простоти та елегантності ця відповідь виграє. І це досить ефективно в загальному випадку; це буде досить добре паралельно. Для деяких джерел потоків, які знають їх розмір, існує швидший спосіб, але в більшості випадків не варто зайвого коду, щоб зберегти ці кілька ітерацій.
Brian Goetz

1
@BrianGoetz, як це буде добре паралельно? останнє значення буде непередбачуваним із використанням паралельного потоку
Benez

2
@BrianGoetz: це все одно O(n), навіть якщо ділити його на кількість ядер процесора. Оскільки потік не знає, що робить функція скорочення, він все одно повинен оцінити його для кожного елемента.
Holger

37

Це сильно залежить від природи 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 елементами, а лише над останнім.


1
У чому сенс hasCharacteristics()блоку? Яке значення додає воно, що ще не охоплене recursive()методом? Останній вже переходить до останньої точки розколу. Крім того, recursive()ніколи не може повернутися, nullтому ви можете зняти it != nullчек.
Гілі

1
Рекурсивна операція може обробляти кожен випадок, але є лише запасним варіантом, оскільки вона має гірший випадок глибини рекурсії, що відповідає кількості (нефільтрованих!) Елементів. Ідеальний випадок - SUBSIZEDпотік, який може гарантувати непусті розділені тайми, тому нам ніколи не потрібно повертатися до лівої сторони. Зверніть увагу, що в цьому випадку recursiveнасправді не повториться, оскільки trySplitвже доведено, що повертається null.
Холгер

2
Звичайно, код міг бути написаний інакше, і він був; Я вважаю, що null-check походить від попередньої версії, але потім я виявив, що для непотоків SUBSIZEDвам доводиться мати справу з можливими порожніми розділеними частинами, тобто вам доведеться перебирати, щоб дізнатись, чи має він значення, тому я перемістив Spliterators.iterator(…)виклик у recursiveметод щоб мати можливість зробити резервну копію на лівий бік, якщо правий бік порожній. Цикл все ще є найкращою операцією.
Холгер

2
Цікаве рішення. Зверніть увагу, що відповідно до поточної реалізації Stream API ваш потік повинен бути паралельним або безпосередньо підключеним до вихідного сплітератора. В іншому випадку він з якихось причин відмовиться розділяти, навіть якщо основний сплітератор джерела розколюється. З іншого боку, ви не можете використовувати наосліп, parallel()оскільки це може фактично виконувати деякі операції (наприклад, сортування), паралельно несподівано витрачаючи більше ядер процесора.
Тагір Валєєв

2
@Tagir Valeev: правильно, в прикладі коду використовується .parallel(), але справді, це може вплинути на sorted()або distinct(). Я не думаю, що це повинно мати ефект для будь-якої іншої проміжної операції ...
Холгер

6

Це лише рефакторинг відповіді Холгера , оскільки, хоча він і фантастичний, його трохи важко прочитати / зрозуміти, особливо для людей, які не були програмістами до 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;      
    }   
}


1

Ось ще одне рішення (не таке ефективне):

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

Цікаво ... Ви тестували це? Оскільки substreamметоду немає , і навіть якби він був, це не спрацювало б, оскільки countце термінальна операція. То яка історія за цим?
Lii

Дивно, я не знаю, який jdk у мене є, але він має підпотік. Я подивився офіційний javadoc ( docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html ), і ви маєте рацію, він тут не відображається.
панагду

6
Звичайно, вам доведеться перевірити, чи count==0спочатку Stream.skipне подобається -1як введення. Крім того, питання не говорило, що ви можете придбати Streamдвічі. Також не сказано, що придбання Streamподвійного гарантовано отримує однакову кількість елементів.
Holger

1

Паралельні нерозмірні потоки з методами '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());
}

Модульне тестування за допомогою сполучення 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);
}

Зверніть увагу, що модульні тести використовують бібліотеку assertj для кращої плавності.
Тет

2
Проблема в тому, що ви робите StreamSupport.stream(((Iterable<Long>) stream::iterator).spliterator(), stream.isParallel()), Iterableпроїжджаючи об’їзд, який взагалі не має характеристик, іншими словами створює невпорядкований потік. Таким чином, результат не має нічого спільного з паралельним використанням чи використанням skip, а лише з тим фактом, що “останній” не має значення для невпорядкованого потоку, тому будь-який елемент є дійсним результатом.
Holger

1

Нам потрібен lastбув потік у виробництві - я все ще не впевнений, що насправді, але різні члени команди в моїй команді сказали, що ми це зробили з різних "причин". У підсумку я написав щось подібне:

 private static class Holder<T> implements Consumer<T> {

    T t = null;
    // needed to null elements that could be valid
    boolean set = false;

    @Override
    public void accept(T t) {
        this.t = t;
        set = true;
    }
}

/**
 * when a Stream is SUBSIZED, it means that all children (direct or not) are also SIZED and SUBSIZED;
 * meaning we know their size "always" no matter how many splits are there from the initial one.
 * <p>
 * when a Stream is SIZED, it means that we know it's current size, but nothing about it's "children",
 * a Set for example.
 */
private static <T> Optional<Optional<T>> last(Stream<T> stream) {

    Spliterator<T> suffix = stream.spliterator();
    // nothing left to do here
    if (suffix.getExactSizeIfKnown() == 0) {
        return Optional.empty();
    }

    return Optional.of(Optional.ofNullable(compute(suffix, new Holder())));
}


private static <T> T compute(Spliterator<T> sp, Holder holder) {

    Spliterator<T> s;
    while (true) {
        Spliterator<T> prefix = sp.trySplit();
        // we can't split any further
        // BUT don't look at: prefix.getExactSizeIfKnown() == 0 because this
        // does not mean that suffix can't be split even more further down
        if (prefix == null) {
            s = sp;
            break;
        }

        // if prefix is known to have no elements, just drop it and continue with suffix
        if (prefix.getExactSizeIfKnown() == 0) {
            continue;
        }

        // if suffix has no elements, try to split prefix further
        if (sp.getExactSizeIfKnown() == 0) {
            sp = prefix;
        }

        // after a split, a stream that is not SUBSIZED can give birth to a spliterator that is
        if (sp.hasCharacteristics(Spliterator.SUBSIZED)) {
            return compute(sp, holder);
        } else {
            // if we don't know the known size of suffix or prefix, just try walk them individually
            // starting from suffix and see if we find our "last" there
            T suffixResult = compute(sp, holder);
            if (!holder.set) {
                return compute(prefix, holder);
            }
            return suffixResult;
        }


    }

    s.forEachRemaining(holder::accept);
    // we control this, so that Holder::t is only T
    return (T) holder.t;

}

І деякі способи його використання:

    Stream<Integer> st = Stream.concat(Stream.of(1, 2), Stream.empty());
    System.out.println(2 == last(st).get().get());

    st = Stream.concat(Stream.empty(), Stream.of(1, 2));
    System.out.println(2 == last(st).get().get());

    st = Stream.concat(Stream.iterate(0, i -> i + 1), Stream.of(1, 2, 3));
    System.out.println(3 == last(st).get().get());

    st = Stream.concat(Stream.iterate(0, i -> i + 1).limit(0), Stream.iterate(5, i -> i + 1).limit(3));
    System.out.println(7 == last(st).get().get());

    st = Stream.concat(Stream.iterate(5, i -> i + 1).limit(3), Stream.iterate(0, i -> i + 1).limit(0));
    System.out.println(7 == last(st).get().get());

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

    System.out.println(s.equalsIgnoreCase("9999999"));

    st = Stream.empty();
    System.out.println(last(st).isEmpty());

    st = Stream.of(1, 2, 3, 4, null);
    System.out.println(last(st).get().isEmpty());

    st = Stream.of((Integer) null);
    System.out.println(last(st).isPresent());

    IntStream is = IntStream.range(0, 4).filter(i -> i != 3);
    System.out.println(last(is.boxed()));

По-перше, це тип повернення Optional<Optional<T>>- це дивно , я погоджуюсь. Якщо перший Optionalпорожній, це означає, що в потоці немає елементів; якщо другий Необов'язковий порожній, це означає, що елемент, який був останнім, насправді був null, тобто: Stream.of(1, 2, 3, null)(на відміну від guava'', Streams::findLastщо видає виняток у такому випадку).

Я визнаю, що мене надихнула головним чином відповідь Холгера на подібне моє питання та відповідь гуави Streams::findLast.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.