Cách hiệu quả nhất để lấy phần tử cuối cùng của luồng


76

Luồng không có last()phương thức:

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

Cách thanh lịch và / hoặc hiệu quả nhất để lấy phần tử cuối cùng (hoặc null cho một Luồng trống) là gì?


4
Nếu bạn cần tìm phần tử cuối cùng của a Stream, bạn có thể muốn xem xét lại thiết kế của mình và nếu bạn thực sự muốn sử dụng a Stream. Streams không nhất thiết phải có thứ tự hoặc hữu hạn. Nếu của bạn Streamkhông có thứ tự, vô hạn hoặc cả hai, phần tử cuối cùng không có ý nghĩa. Theo suy nghĩ của tôi, quan điểm của a Streamlà cung cấp một lớp trừu tượng giữa dữ liệu và cách bạn xử lý nó. Như vậy, Streambản thân một không cần biết gì về thứ tự tương đối của các phần tử của nó. Tìm phần tử cuối cùng trong a Streamlà O (n). Nếu bạn có một cấu trúc dữ liệu khác, nó có thể là O (1).
Jeffrey

1
@jeff nhu cầu là có thật: tình huống đại khái là thêm các mặt hàng vào giỏ hàng, mỗi lần bổ sung trả về thông tin lỗi (một số kết hợp mặt hàng nhất định không hợp lệ), nhưng chỉ có thông tin lỗi của lần bổ sung cuối cùng (khi tất cả các mặt hàng đã được thêm vào và hợp lệ đánh giá giỏ hàng có thể được thực hiện) là thông tin cần thiết. (Có, API chúng tôi đang sử dụng bị hỏng và không thể sửa được).
Bohemian

14
@BrianGoetz: Các luồng vô hạn cũng không được xác định rõ ràng count(), nhưng Luồng vẫn có một count()phương thức. Thực sự, đối số đó áp dụng cho bất kỳ hoạt động đầu cuối không chập mạch nào trên các dòng vô hạn.
Jeffrey Bosboom

@BrianGoetz Tôi nghĩ luồng nên có last()phương pháp. Có thể có một cuộc khảo sát vào ngày 1 tháng 4 về cách xác định nó cho các luồng vô hạn. Tôi sẽ đề xuất: "Nó không bao giờ quay trở lại và nó sử dụng ít nhất một lõi bộ xử lý ở mức 100%. Trên các luồng song song, nó được yêu cầu sử dụng tất cả các lõi ở mức 100%."
Vojta

Nếu danh sách chứa các đối tượng có thứ tự tự nhiên hoặc có thể được sắp xếp, bạn có thể sử dụng max()phương thức như trong stream()...max(Comparator...).
Erk

Câu trả lời:


123

Thực hiện giảm chỉ trả về giá trị hiện tại:

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

2
Bạn sẽ nói điều này là thanh lịch, hiệu quả hay cả hai?
Duncan Jones

1
@Duncan Tôi nghĩ đó là cả hai, nhưng tôi chưa phải là một khẩu súng trong java 8 và nhu cầu này đã xuất hiện vào ngày hôm trước - một học sinh đã đẩy luồng lên một ngăn xếp rồi bật nó lên và tôi nghĩ điều này trông tốt hơn, nhưng ở đó có thể là một cái gì đó thậm chí còn đơn giản hơn ngoài kia.
Bohemian

19
Đối với sự đơn giản và thanh lịch, câu trả lời này chiến thắng. Và hiệu quả hợp lý của nó trong trường hợp chung; nó sẽ song song hợp lý tốt. Đối với một số nguồn luồng biết kích thước của chúng, có một cách nhanh hơn, nhưng trong hầu hết các trường hợp, mã bổ sung không đáng để lưu vài lần lặp đó.
Brian Goetz

1
@BrianGoetz làm thế nào điều này sẽ song song tốt? giá trị cuối cùng sẽ không thể đoán trước được bằng cách sử dụng một luồng song song
từ

2
@BrianGoetz: nó vẫn còn O(n), ngay cả khi chia cho số lõi CPU. Vì luồng không biết chức năng giảm thiểu làm gì, nên nó vẫn phải đánh giá nó cho mọi phần tử.
Holger

37

Điều này phụ thuộc nhiều vào bản chất của Stream. Hãy nhớ rằng "đơn giản" không nhất thiết có nghĩa là "hiệu quả". Nếu bạn nghi ngờ luồng rất lớn, thực hiện các thao tác nặng hoặc có một nguồn biết trước kích thước, thì giải pháp sau có thể hiệu quả hơn đáng kể so với giải pháp đơn giản:

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

Bạn có thể minh họa sự khác biệt bằng ví dụ sau:

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

Nó sẽ in:

potential heavy operation on 9999999
9999999

Nói cách khác, nó không thực hiện thao tác trên 9999999 phần tử đầu tiên mà chỉ trên phần tử cuối cùng.


1
Điểm của hasCharacteristics()khối là gì? Giá trị nào mà nó thêm vào mà chưa được recursive()phương thức bao hàm? Cái sau đã điều hướng đến điểm tách cuối cùng. Hơn nữa, recursive()không bao giờ có thể trở lại nullđể bạn có thể xóa it != nullséc.
Gili

1
Op đệ quy có thể xử lý mọi trường hợp nhưng chỉ là dự phòng vì nó có trường hợp xấu hơn về độ sâu đệ quy khớp với số phần tử (chưa được lọc!). Trường hợp lý tưởng là một SUBSIZEDluồng có thể đảm bảo các nửa tách không trống để chúng ta không bao giờ cần quay lại phía bên trái. Lưu ý rằng trong trường hợp đó recursivesẽ không thực sự lặp lại vì trySplitnó đã được chứng minh là trả về null.
Holger

2
Tất nhiên, mã có thể được viết khác, và nó là; Tôi đoán null-check bắt nguồn từ một phiên bản trước đó nhưng sau đó tôi phát hiện ra rằng đối với các SUBSIZEDluồng không phải luồng, bạn phải xử lý các phần tách trống có thể xảy ra, tức là bạn phải lặp lại để tìm xem nó có các giá trị hay không, do đó tôi đã chuyển lệnh Spliterators.iterator(…)gọi thành một recursivephương thức để có thể sao lưu sang phía bên trái nếu phía bên phải trống. Vòng lặp vẫn là hoạt động được ưu tiên.
Holger

2
Giải pháp thú vị. Lưu ý rằng theo triển khai Stream API hiện tại, luồng của bạn phải song song hoặc được kết nối trực tiếp với bộ tách nguồn. Nếu không, vì một số lý do nào đó, nó sẽ từ chối phân chia ngay cả khi bộ tách nguồn cơ bản phân chia. Mặt khác, bạn không thể sử dụng một cách mù quáng parallel()vì điều này thực sự có thể thực hiện một số hoạt động (như sắp xếp) song song tiêu tốn nhiều lõi CPU hơn một cách bất ngờ.
Tagir Valeev

2
@Tagir Valeev: đúng, mã ví dụ sử dụng .parallel(), nhưng thực sự, nó có thể ảnh hưởng đến sorted()hoặc distinct(). Tôi không nghĩ rằng sẽ có tác động đối với bất kỳ hoạt động trung gian nào khác…
Holger

6

Đây chỉ là một bản tái cấu trúc câu trả lời của Holger bởi vì mã, mặc dù tuyệt vời, nhưng hơi khó đọc / hiểu, đặc biệt là đối với những người không phải là lập trình viên C trước Java. Hy vọng rằng lớp mẫu được tái cấu trúc của tôi sẽ dễ làm theo hơn một chút đối với những người không quen thuộc với trình phân tách, những gì chúng làm hoặc cách chúng hoạt động.

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

Đây là một giải pháp khác (không hiệu quả lắm):

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

Thật thú vị ... Bạn đã chạy thử cái này chưa? Bởi vì không có substreamphương pháp, và ngay cả khi có điều này cũng không hoạt động vì đây countlà một hoạt động đầu cuối. Vậy câu chuyện đằng sau điều này là gì?
Lii

Thật lạ, tôi không biết tôi có jdk nhưng nó có một dòng con. Tôi đã xem javadoc của tư pháp ( docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html ) và bạn nói đúng, nó không xuất hiện ở đây.
panagdu

6
Tất nhiên, bạn sẽ phải kiểm tra xem count==0đầu tiên Stream.skipkhông giống -1như đầu vào. Bên cạnh đó, câu hỏi không nói rằng bạn có thể đạt được Streamhai lần. Nó cũng không nói rằng có được Streamhai lần được đảm bảo nhận được cùng một số phần tử.
Holger

1

Các luồng không có kích thước song song với phương pháp 'bỏ qua' rất phức tạp và việc triển khai @ Holger đưa ra câu trả lời sai. Ngoài ra, việc triển khai của @ Holger chậm hơn một chút vì nó sử dụng trình vòng lặp.

Tối ưu hóa câu trả lời @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());
}

Kiểm tra đơn vị bằng cách sử dụng 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);
}

Giải pháp duy nhất để hỗ trợ các kịch bản của cả hai là tránh phát hiện trình phân tách cuối cùng trên các luồng song song không có kích thước. Hệ quả là giải pháp sẽ thực hiện các phép toán trên tất cả các phần tử nhưng nó sẽ luôn đưa ra câu trả lời đúng.

Lưu ý rằng trong các luồng tuần tự, nó vẫn sẽ thực hiện các hoạt động trên tất cả các phần tử.

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

Đối với thử nghiệm đơn vị cho việc triển khai đó, ba thử nghiệm đầu tiên hoàn toàn giống nhau (song song tuần tự và có kích thước). Các bài kiểm tra cho song song không kích thước là ở đây:

@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);
}

Lưu ý rằng các bài kiểm tra đơn vị đang sử dụng thư viện khẳng định để lưu loát hơn.
Tết

2
Vấn đề là bạn đang làm StreamSupport.stream(((Iterable<Long>) stream::iterator).spliterator(), stream.isParallel()), đi qua một con Iterableđường vòng không có đặc điểm nào cả, hay nói cách khác là tạo ra một luồng không có thứ tự . Vì vậy, kết quả không liên quan gì đến song song hoặc sử dụng skip, mà chỉ với thực tế là “cuối cùng” không có ý nghĩa đối với một luồng không có thứ tự, vì vậy bất kỳ phần tử nào cũng là một kết quả hợp lệ.
Holger

1

Chúng tôi cần lastmột Luồng trong quá trình sản xuất - tôi vẫn không chắc chúng tôi thực sự đã làm như vậy, nhưng các thành viên khác nhau trong nhóm của tôi cho biết chúng tôi đã làm vì nhiều "lý do" khác nhau. Tôi đã viết một cái gì đó như thế này:

 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;

}

Và một số cách sử dụng của nó:

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

Đầu tiên là kiểu trả về Optional<Optional<T>>- nó trông kỳ lạ , tôi đồng ý. Nếu đầu tiên Optionaltrống, điều đó có nghĩa là không có phần tử nào trong Luồng; nếu Tùy chọn thứ hai trống, điều đó có nghĩa là phần tử cuối cùng, thực sự nulllà: Stream.of(1, 2, 3, null)(không giống như guava's Streams::findLastném Ngoại lệ trong trường hợp như vậy).

Tôi thừa nhận rằng tôi chủ yếu lấy cảm hứng từ câu trả lời của Holger về câu hỏi tương tự của tôi và của ổi Streams::findLast.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.