Tại sao filter () sau khi flatMap () "không hoàn toàn" lười biếng trong các luồng Java?


75

Tôi có mã mẫu sau:

System.out.println(
       "Result: " +
        Stream.of(1, 2, 3)
                .filter(i -> {
                    System.out.println(i);
                    return true;
                })
                .findFirst()
                .get()
);
System.out.println("-----------");
System.out.println(
       "Result: " +
        Stream.of(1, 2, 3)
                .flatMap(i -> Stream.of(i - 1, i, i + 1))
                .flatMap(i -> Stream.of(i - 1, i, i + 1))
                .filter(i -> {
                    System.out.println(i);
                    return true;
                })
                .findFirst()
                .get()
);

Kết quả như sau:

1
Result: 1
-----------
-1
0
1
0
1
2
1
2
3
Result: -1

Từ đây, tôi thấy rằng trong trường hợp đầu tiên streamthực sự hoạt động một cách lười biếng - chúng tôi sử dụng findFirst()vì vậy khi chúng tôi có phần tử đầu tiên lambda lọc của chúng tôi không được gọi. Tuy nhiên, trong trường hợp thứ hai sử dụng flatMaps, chúng ta thấy rằng mặc dù phần tử đầu tiên đáp ứng điều kiện lọc được tìm thấy (nó chỉ là bất kỳ phần tử đầu tiên nào vì lambda luôn trả về true) các nội dung khác của luồng vẫn được cung cấp thông qua chức năng lọc.

Tôi đang cố gắng hiểu tại sao nó hoạt động như vậy thay vì từ bỏ sau khi phần tử đầu tiên được tính như trong trường hợp đầu tiên. Bất kỳ thông tin hữu ích sẽ được đánh giá cao.


11
@PhilippSander: Bởi vì nếu nó hoạt động một cách lười biếng - như trong trường hợp đầu tiên - nó sẽ chỉ đánh giá bộ lọc một lần.
Jon Skeet

4
Lưu ý rằng bạn cũng có thể sử dụng peek: Stream.of(1, 2, 3).peek(System.out::println).filter(i -> true)...
Alexis C.

4
Lưu ý rằng tôi đã tạo một cách giải quyết
Holger,

9
Một lỗi OpenJDK đã được đưa ra cho vấn đề này vào ngày câu hỏi này được hỏi: bug.openjdk.java.net/browse/JDK-8075939 . Nó đã được chỉ định, nhưng vẫn chưa được sửa, gần một năm sau :(
MikeFHay 29/02/16

5
@MikeFHay JDK-8075939 được nhắm mục tiêu cho Java 10. Cf. mail.openjdk.java.net/pipermail/core-libs-dev/2017-December/… cho chuỗi đánh giá core-libs-dev và một liên kết đến webrev đầu tiên.
Stefan Zobel

Câu trả lời:


65

TL; DR, điều này đã được giải quyết trong JDK-8075939 và được sửa trong Java 10 (và được hỗ trợ lại cho Java 8 trong JDK-8225328 ).

Khi xem xét triển khai ( ReferencePipeline.java), chúng tôi thấy phương thức [ link ]

@Override
final void forEachWithCancel(Spliterator<P_OUT> spliterator, Sink<P_OUT> sink) {
    do { } while (!sink.cancellationRequested() && spliterator.tryAdvance(sink));
}

sẽ được gọi cho findFirsthoạt động. Điều đặc biệt cần quan tâm là sink.cancellationRequested()cho phép kết thúc vòng lặp ở trận đấu đầu tiên. So sánh với [ liên kết ]

@Override
public final <R> Stream<R> flatMap(Function<? super P_OUT, ? extends Stream<? extends R>> mapper) {
    Objects.requireNonNull(mapper);
    // We can do better than this, by polling cancellationRequested when stream is infinite
    return new StatelessOp<P_OUT, R>(this, StreamShape.REFERENCE,
                                 StreamOpFlag.NOT_SORTED | StreamOpFlag.NOT_DISTINCT | StreamOpFlag.NOT_SIZED) {
        @Override
        Sink<P_OUT> opWrapSink(int flags, Sink<R> sink) {
            return new Sink.ChainedReference<P_OUT, R>(sink) {
                @Override
                public void begin(long size) {
                    downstream.begin(-1);
                }

                @Override
                public void accept(P_OUT u) {
                    try (Stream<? extends R> result = mapper.apply(u)) {
                        // We can do better that this too; optimize for depth=0 case and just grab spliterator and forEach it
                        if (result != null)
                            result.sequential().forEach(downstream);
                    }
                }
            };
        }
    };
}

Phương thức tiến một mục sẽ kết thúc cuộc gọi forEachtrên luồng phụ mà không có bất kỳ khả năng kết thúc sớm hơn và chú thích ở đầu flatMapphương thức thậm chí còn cho biết về tính năng vắng mặt này.

Vì đây không chỉ là một thứ tối ưu hóa vì nó ngụ ý rằng mã chỉ đơn giản bị hỏng khi luồng phụ là vô hạn, tôi hy vọng rằng các nhà phát triển sớm chứng minh rằng họ “có thể làm tốt hơn điều này”…


Để minh họa các hàm ý, trong khi Stream.iterate(0, i->i+1).findFirst()hoạt động như mong đợi, Stream.of("").flatMap(x->Stream.iterate(0, i->i+1)).findFirst()sẽ kết thúc trong một vòng lặp vô hạn.

Về đặc điểm kỹ thuật, hầu hết nó có thể được tìm thấy trong

chương “Hoạt động dòng và đường ống” của đặc điểm kỹ thuật gói :

Các hoạt động trung gian trả về một luồng mới. Họ luôn lười biếng ;

… Sự lười biếng cũng cho phép tránh kiểm tra tất cả dữ liệu khi không cần thiết; đối với các hoạt động như "tìm chuỗi đầu tiên dài hơn 1000 ký tự", chỉ cần kiểm tra vừa đủ các chuỗi để tìm một chuỗi có các đặc điểm mong muốn mà không cần kiểm tra tất cả các chuỗi có sẵn từ nguồn. (Hành vi này thậm chí còn trở nên quan trọng hơn khi luồng đầu vào là vô hạn và không chỉ lớn.)

Hơn nữa, một số hoạt động được coi là hoạt động đoản mạch . Một hoạt động trung gian là ngắn mạch nếu, khi được trình bày với đầu vào vô hạn, kết quả là nó có thể tạo ra một dòng hữu hạn. Hoạt động của thiết bị đầu cuối bị chập mạch nếu, khi xuất hiện với đầu vào vô hạn, nó có thể kết thúc trong thời gian hữu hạn. Việc xảy ra hiện tượng đoản mạch trong đường ống là điều kiện cần nhưng chưa đủ để quá trình xử lý dòng vô hạn kết thúc bình thường trong thời gian hữu hạn.

Rõ ràng là hoạt động đoản mạch không đảm bảo chấm dứt thời gian hữu hạn, ví dụ: khi bộ lọc không khớp với bất kỳ mục nào, quá trình xử lý không thể hoàn thành, nhưng việc triển khai không hỗ trợ bất kỳ chấm dứt nào trong thời gian hữu hạn bằng cách bỏ qua bản chất đoản mạch của một hoạt động khác xa với thông số kỹ thuật.


27
Đây là một lỗi. Mặc dù có thể đúng là thông số kỹ thuật hỗ trợ hành vi này, nhưng không ai mong đợi rằng việc nhận phần tử đầu tiên của một luồng vô hạn sẽ tạo ra lỗi StackOverflowError hoặc sẽ kết thúc trong một vòng lặp vô hạn, bất kể nó đến trực tiếp từ nguồn của đường dẫn hay từ một luồng lồng nhau thông qua một chức năng ánh xạ. Điều này sẽ được báo cáo là một lỗi.
fps

5
@Vadym S. Khondar: gửi báo cáo lỗi là một ý kiến ​​hay. Về lý do tại sao ai đó không phát hiện ra điều này trước đây, tôi đã thấy rất nhiều lỗi "không thể tin được rằng tôi là người đầu tiên nhận thấy loại lỗi này" trước đây. Trừ khi có liên quan đến các luồng vô hạn, lỗi này chỉ có tác động đến hiệu suất mà có thể không được chú ý trong nhiều trường hợp sử dụng.
Holger

7
@Marko Topolnik: thuộc tính “không bắt đầu cho đến khi hoạt động đầu cuối của đường ống được thực thi” không phủ nhận các thuộc tính khác của các hoạt động lười biếng. Tôi biết rằng không có tuyên bố một câu nào về tài sản được thảo luận, nếu không tôi đã trích dẫn nó. Trong các Streamdoc API người ta nói rằng “Streams là lười biếng; tính toán trên dữ liệu nguồn chỉ được thực hiện khi hoạt động đầu cuối được bắt đầu và các phần tử nguồn chỉ được sử dụng khi cần thiết . "
Holger

6
Bạn có thể thắc mắc một lần nữa khi điều này ngụ ý một sự đảm bảo thực thi lười biếng liên quan đến đoản mạch, tuy nhiên, tôi có xu hướng nhìn nhận nó theo cách khác: không có thời điểm nào người ta nói rằng các triển khai miễn phí để thực hiện hành vi không lười biếng theo cách chúng ta thấy ở đây. Và đặc điểm kỹ thuật rất đầy đủ về những gì được phép và những gì không.
Holger

5
JDK-8075939 hiện đang có tiến bộ. Xem mail.openjdk.java.net/pipermail/core-libs-dev/2017-December/… để biết chuỗi đánh giá core-libs-dev và liên kết đến webrev đầu tiên. Nó xuất hiện, chúng ta sẽ thấy nó trong Java 10.
Stefan Zobel

17

Từng phần tử của luồng đầu vào được tiêu thụ một cách lười biếng. Phần tử đầu tiên 1, được chuyển đổi bởi hai flatMaps thành luồng -1, 0, 1, 0, 1, 2, 1, 2, 3, do đó toàn bộ luồng chỉ tương ứng với phần tử đầu vào đầu tiên. Các dòng lồng nhau được háo hức thực hiện bằng đường ống, sau đó được làm phẳng, sau đó được cung cấp cho filtersân khấu. Điều này giải thích đầu ra của bạn.

Những điều trên không xuất phát từ một hạn chế cơ bản, nhưng nó có thể sẽ khiến mọi thứ trở nên phức tạp hơn nhiều khi có được sự lười biếng hoàn toàn cho các luồng lồng nhau. Tôi nghĩ rằng nó sẽ là một thách thức lớn hơn để làm cho nó hoạt động hiệu quả.

Để so sánh, các seq lười biếng của Clojure nhận được một lớp bao bọc khác cho mỗi cấp độ lồng như vậy. Do thiết kế này, các hoạt động thậm chí có thể thất bại vớiStackOverflowError khi thực hiện lồng ghép ở mức quá cao.


2
@MarkoTopolnik, cảm ơn bạn đã trả lời. Thực ra, mối quan tâm của Holger thực sự là lý do khiến tôi ngạc nhiên. Trường hợp thứ hai có nghĩa là tôi không thể sử dụng flatMap cho các luồng vô hạn?
Vadym S. Khondar

Vâng, tôi cá rằng luồng lồng nhau không thể là vô hạn.
Marko Topolnik

8

Liên quan đến sự cố với các luồng phụ vô hạn, hành vi của flatMap vẫn trở nên đáng ngạc nhiên hơn khi một người ném vào hoạt động đoản mạch trung gian (trái ngược với thiết bị đầu cuối).

Trong khi phần sau hoạt động như mong đợi, in ra chuỗi số nguyên vô hạn

Stream.of("x").flatMap(_x -> Stream.iterate(1, i -> i + 1)).forEach(System.out::println);

mã sau chỉ in ra "1", nhưng vẫn không kết thúc:

Stream.of("x").flatMap(_x -> Stream.iterate(1, i -> i + 1)).limit(1).forEach(System.out::println);

Tôi không thể tưởng tượng việc đọc các thông số kỹ thuật mà không phải là một lỗi.


6

Trong thư viện StreamEx miễn phí của mình, tôi đã giới thiệu các bộ thu gom chập mạch. Khi thu thập dòng tuần tự với bộ thu ngắn mạch (như MoreCollectors.first()) chính xác một phần tử được tiêu thụ từ nguồn. Bên trong nó được triển khai theo một cách khá bẩn: sử dụng một ngoại lệ tùy chỉnh để phá vỡ luồng điều khiển. Sử dụng thư viện của tôi, mẫu của bạn có thể được viết lại theo cách này:

System.out.println(
        "Result: " +
                StreamEx.of(1, 2, 3)
                .flatMap(i -> Stream.of(i - 1, i, i + 1))
                .flatMap(i -> Stream.of(i - 1, i, i + 1))
                .filter(i -> {
                    System.out.println(i);
                    return true;
                })
                .collect(MoreCollectors.first())
                .get()
        );

Kết quả là như sau:

-1
Result: -1


0

Tôi đồng ý với những người khác, đây là một lỗi được mở tại JDK-8075939 . Và vì nó vẫn chưa được sửa sau hơn một năm. Tôi muốn giới thiệu cho bạn: AbacusUtil

N.println("Result: " + Stream.of(1, 2, 3).peek(N::println).first().get());

N.println("-----------");

N.println("Result: " + Stream.of(1, 2, 3)
                        .flatMap(i -> Stream.of(i - 1, i, i + 1))
                        .flatMap(i -> Stream.of(i - 1, i, i + 1))
                        .peek(N::println).first().get());

// output:
// 1
// Result: 1
// -----------
// -1
// Result: -1

Tiết lộ : Tôi là nhà phát triển của AbacusUtil.


0

Hôm nay tôi cũng tình cờ gặp lỗi này. Hành vi không quá căng thẳng, vì trường hợp đơn giản, như bên dưới, đang hoạt động tốt, nhưng mã sản xuất tương tự không hoạt động.

 stream(spliterator).map(o -> o).flatMap(Stream::of).flatMap(Stream::of).findAny()

Đối với những người không thể đợi vài năm nữa để chuyển sang JDK-10, có một luồng lười thực sự thay thế. Nó không hỗ trợ song song. Nó được dành riêng cho bản dịch JavaScript, nhưng nó hoạt động với tôi, vì giao diện giống nhau.

StreamHelper dựa trên bộ sưu tập, nhưng nó rất dễ dàng để điều chỉnh Spliterator.

https://github.com/yaitskov/j4ts/blob/stream/src/main/java/javaemul/internal/stream/StreamHelper.java

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.