Tại sao Luồng Java lại một lần?


239

Không giống như C # IEnumerable, trong đó một đường ống thực thi có thể được thực thi nhiều lần như chúng ta muốn, trong Java, một luồng có thể được 'lặp lại' chỉ một lần.

Bất kỳ cuộc gọi đến một hoạt động đầu cuối sẽ đóng luồng, khiến nó không thể sử dụng được. 'Tính năng' này lấy đi rất nhiều năng lượng.

Tôi tưởng tượng lý do cho việc này không phải là kỹ thuật. Những cân nhắc thiết kế đằng sau hạn chế kỳ lạ này là gì?

Chỉnh sửa: để thể hiện những gì tôi đang nói, hãy xem xét việc triển khai Sắp xếp nhanh trong C # sau đây:

IEnumerable<int> QuickSort(IEnumerable<int> ints)
{
  if (!ints.Any()) {
    return Enumerable.Empty<int>();
  }

  int pivot = ints.First();

  IEnumerable<int> lt = ints.Where(i => i < pivot);
  IEnumerable<int> gt = ints.Where(i => i > pivot);

  return QuickSort(lt).Concat(new int[] { pivot }).Concat(QuickSort(gt));
}

Bây giờ để chắc chắn, tôi không ủng hộ rằng đây là một triển khai tốt của sắp xếp nhanh chóng! Tuy nhiên, đây là ví dụ tuyệt vời về sức mạnh biểu cảm của biểu thức lambda kết hợp với hoạt động truyền phát.

Và nó không thể được thực hiện bằng Java! Tôi thậm chí không thể hỏi một luồng xem nó có trống không mà không hiển thị nó không sử dụng được.


4
Bạn có thể đưa ra một ví dụ cụ thể khi đóng luồng "lấy đi năng lượng" không?
Rogério

23
Nếu bạn muốn sử dụng dữ liệu từ một luồng nhiều lần, bạn sẽ phải chuyển dữ liệu đó vào một bộ sưu tập. Đây là khá nhiều như thế nào nó làm việc: hoặc là bạn phải thực hiện lại việc tính toán để tạo ra các dòng, hoặc bạn có để lưu trữ các kết quả trung gian.
Louis Wasserman

5
Ok, nhưng làm lại cùng một tính toán trên cùng một luồng nghe có vẻ sai. Một luồng được tạo từ một nguồn nhất định trước khi tính toán được thực hiện, giống như các vòng lặp được tạo cho mỗi lần lặp. Tôi vẫn muốn xem một ví dụ cụ thể thực tế; cuối cùng, tôi cá rằng có một cách rõ ràng để giải quyết từng vấn đề với các luồng sử dụng một lần, giả sử có một cách tương ứng tồn tại với các liệt kê của C #.
Rogério

2
Điều này lúc đầu thật khó hiểu với tôi, vì tôi nghĩ câu hỏi này sẽ liên quan đến C # s IEnumerablevới các luồng củajava.io.*
SpaceTrucker

9
Lưu ý rằng việc sử dụng IEnumerable nhiều lần trong C # là một mẫu dễ vỡ, do đó tiền đề của câu hỏi có thể hơi thiếu sót. Nhiều triển khai của IEnumerable cho phép nhưng một số thì không! Các công cụ phân tích mã có xu hướng cảnh báo bạn không làm điều đó.
Sander

Câu trả lời:


368

Tôi có một số hồi ức từ thiết kế ban đầu của API Streams có thể làm sáng tỏ cơ sở thiết kế.

Quay trở lại năm 2012, chúng tôi đã thêm lambdas vào ngôn ngữ và chúng tôi muốn có một bộ hoạt động theo định hướng bộ sưu tập hoặc "dữ liệu số lượng lớn", được lập trình bằng lambdas, điều đó sẽ tạo thuận lợi cho việc song song. Ý tưởng về các hoạt động xâu chuỗi lười biếng cùng nhau đã được thiết lập tốt vào thời điểm này. Chúng tôi cũng không muốn các hoạt động trung gian lưu trữ kết quả.

Vấn đề chính mà chúng tôi cần quyết định là các đối tượng trong chuỗi trông như thế nào trong API và cách chúng nối với các nguồn dữ liệu. Các nguồn thường là các bộ sưu tập, nhưng chúng tôi cũng muốn hỗ trợ dữ liệu đến từ tệp hoặc mạng hoặc dữ liệu được tạo khi đang di chuyển, ví dụ: từ trình tạo số ngẫu nhiên.

Có nhiều ảnh hưởng của công việc hiện tại lên thiết kế. Trong số những người có ảnh hưởng lớn hơn có thư viện Guava của Google và thư viện bộ sưu tập Scala. (Nếu có ai ngạc nhiên về ảnh hưởng từ Guava, hãy lưu ý rằng Kevin Bourrillion , nhà phát triển chính của Guava, thuộc nhóm chuyên gia JSR-335 Lambda .) Trên các bộ sưu tập của Scala, chúng tôi thấy bài nói chuyện này của Martin Oderky được đặc biệt quan tâm: Tương lai- Chứng minh Bộ sưu tập Scala: từ Mutable đến Persistent đến Parallel . (Stanford EE380, ngày 1 tháng 6 năm 2011)

Thiết kế nguyên mẫu của chúng tôi tại thời điểm đó được dựa trên xung quanh Iterable. Các hoạt động quen thuộc filter, mapv.v. là các phương thức mở rộng (mặc định) trên Iterable. Gọi một đã thêm một hoạt động cho chuỗi và trả lại một hoạt động khác Iterable. Một hoạt động đầu cuối như countsẽ gọi iterator()chuỗi đến nguồn và các hoạt động được thực hiện trong Iterator của từng giai đoạn.

Vì đây là Iterables, bạn có thể gọi iterator()phương thức nhiều lần. Điều gì sẽ xảy ra sau đó?

Nếu nguồn là một bộ sưu tập, điều này chủ yếu hoạt động tốt. Các bộ sưu tập là Iterable và mỗi lệnh gọi để iterator()tạo một cá thể Iterator riêng biệt độc lập với bất kỳ phiên bản hoạt động nào khác và mỗi lần di chuyển qua bộ sưu tập một cách độc lập. Tuyệt quá.

Bây giờ nếu nguồn là một lần, như đọc các dòng từ tệp thì sao? Có thể Iterator đầu tiên sẽ nhận được tất cả các giá trị nhưng các giá trị thứ hai và tiếp theo sẽ trống. Có lẽ các giá trị nên được xen kẽ giữa các Iterators. Hoặc có thể mỗi Iterator sẽ nhận được tất cả các giá trị giống nhau. Sau đó, điều gì sẽ xảy ra nếu bạn có hai vòng lặp và một cái đi xa hơn cái kia? Ai đó sẽ phải đệm các giá trị trong Iterator thứ hai cho đến khi chúng được đọc. Tệ hơn, nếu bạn nhận được một Iterator và đọc tất cả các giá trị, và chỉ sau đó nhận được một Iterator thứ hai. Các giá trị đến từ đâu bây giờ? Có một yêu cầu cho tất cả chúng được đệm lên chỉ trong trường hợp ai đó muốn một Iterator thứ hai?

Rõ ràng, việc cho phép nhiều Iterator trên một nguồn một lần đặt ra rất nhiều câu hỏi. Chúng tôi không có câu trả lời tốt cho họ. Chúng tôi muốn hành vi nhất quán, có thể dự đoán được cho những gì xảy ra nếu bạn gọi iterator()hai lần. Điều này đẩy chúng tôi về phía không cho phép nhiều đường ngang, làm cho các đường ống một lần.

Chúng tôi cũng quan sát những người khác va vào những vấn đề này. Trong JDK, hầu hết các Iterables là các bộ sưu tập hoặc các đối tượng giống như bộ sưu tập, cho phép truyền tải nhiều lần. Nó không được chỉ định ở bất cứ đâu, nhưng dường như có một kỳ vọng bất thành văn rằng Iterables cho phép nhiều lần truyền tải. Một ngoại lệ đáng chú ý là giao diện NIO DirectoryStream . Đặc điểm kỹ thuật của nó bao gồm cảnh báo thú vị này:

Trong khi DirectoryStream mở rộng Iterable, nó không phải là Iterable có mục đích chung vì nó chỉ hỗ trợ một Iterator duy nhất; gọi phương thức iterator để có được một iterator thứ hai hoặc tiếp theo ném IllegalStateException.

[in đậm trong bản gốc]

Điều này có vẻ bất thường và khó chịu đến mức chúng tôi không muốn tạo ra một loạt các Iterables mới có thể chỉ có một lần. Điều này đã đẩy chúng tôi ra khỏi việc sử dụng Iterable.

Vào thời điểm này, một bài báo của Bruce Eckel đã xuất hiện mô tả một điểm rắc rối mà anh gặp phải với Scala. Ông đã viết mã này:

// Scala
val lines = fromString(data).getLines
val registrants = lines.map(Registrant)
registrants.foreach(println)
registrants.foreach(println)

Nó khá đơn giản. Nó phân tích các dòng văn bản thành Registrantcác đối tượng và in chúng ra hai lần. Ngoại trừ việc nó thực sự chỉ in chúng ra một lần. Hóa ra anh ta nghĩ rằng đó registrantslà một bộ sưu tập, trong khi thực tế nó là một trình vòng lặp. Cuộc gọi thứ hai để foreachgặp một trình vòng lặp trống, từ đó tất cả các giá trị đã hết, vì vậy nó không in gì cả.

Loại kinh nghiệm này đã thuyết phục chúng tôi rằng điều rất quan trọng là có kết quả có thể dự đoán rõ ràng nếu cố gắng vượt qua nhiều lần. Nó cũng nhấn mạnh tầm quan trọng của việc phân biệt giữa các cấu trúc giống như đường ống lười biếng với các bộ sưu tập thực tế lưu trữ dữ liệu. Chính điều này đã dẫn đến việc tách các hoạt động đường ống lười biếng sang giao diện Stream mới và chỉ giữ các hoạt động đột biến, háo hức trực tiếp trên Bộ sưu tập. Brian Goetz đã giải thích lý do cho điều đó.

Điều gì về việc cho phép nhiều đường truyền cho các đường ống dựa trên bộ sưu tập nhưng không cho phép các đường ống không dựa trên bộ sưu tập? Nó không nhất quán, nhưng nó hợp lý. Nếu bạn đang đọc các giá trị từ mạng, tất nhiên bạn không thể duyệt lại chúng. Nếu bạn muốn duyệt chúng nhiều lần, bạn phải kéo chúng vào một bộ sưu tập một cách rõ ràng.

Nhưng hãy khám phá cho phép nhiều đường truyền từ các đường ống dựa trên bộ sưu tập. Hãy nói rằng bạn đã làm điều này:

Iterable<?> it = source.filter(...).map(...).filter(...).map(...);
it.into(dest1);
it.into(dest2);

(Các intohoạt động bây giờ được đánh vần collect(toList()).)

Nếu nguồn là một bộ sưu tập, thì into()cuộc gọi đầu tiên sẽ tạo ra một chuỗi các Trình lặp trở lại nguồn, thực hiện các hoạt động đường ống và gửi kết quả vào đích. Cuộc gọi thứ hai để into()sẽ tạo ra một chuỗi các vòng lặp, và thực hiện các hoạt động đường ống dẫn một lần nữa . Điều này rõ ràng không sai nhưng nó có tác dụng thực hiện tất cả các hoạt động của bộ lọc và bản đồ lần thứ hai cho mỗi phần tử. Tôi nghĩ rằng nhiều lập trình viên sẽ ngạc nhiên về hành vi này.

Như tôi đã đề cập ở trên, chúng tôi đã nói chuyện với các nhà phát triển Guava. Một trong những điều tuyệt vời mà họ có là một Nghĩa địa ý tưởng nơi họ mô tả các tính năng mà họ quyết định không thực hiện cùng với các lý do. Ý tưởng về các bộ sưu tập lười biếng nghe có vẻ khá tuyệt, nhưng đây là những gì họ nói về nó. Hãy xem xét một List.filter()hoạt động trả về một List:

Mối quan tâm lớn nhất ở đây là quá nhiều hoạt động trở thành đề xuất đắt tiền, theo thời gian tuyến tính. Nếu bạn muốn lọc một danh sách và lấy lại danh sách, và không chỉ là Bộ sưu tập hoặc Iterable, bạn có thể sử dụng ImmutableList.copyOf(Iterables.filter(list, predicate)), "nêu lên trước" những gì nó đang làm và mức độ đắt đỏ của nó.

Để lấy một ví dụ cụ thể, chi phí của get(0)hoặc size()trong Danh sách là bao nhiêu? Đối với các lớp thường được sử dụng như ArrayList, chúng là O (1). Nhưng nếu bạn gọi một trong số này trong danh sách được lọc một cách lười biếng, thì nó phải chạy bộ lọc qua danh sách sao lưu và tất cả các hoạt động đột ngột này là O (n). Tệ hơn, nó phải đi qua danh sách sao lưu trên mọi hoạt động.

Điều này dường như chúng ta quá lười biếng. Đó là một điều để thiết lập một số hoạt động và trì hoãn thực hiện thực tế cho đến khi bạn "Đi". Đó là một cách khác để thiết lập mọi thứ theo cách che giấu một lượng tính toán tiềm năng lớn.

Khi đề xuất không cho phép các luồng không tuyến tính hoặc "không sử dụng lại", Paul Sandoz đã mô tả các hậu quả tiềm ẩn của việc cho phép chúng làm phát sinh "kết quả bất ngờ hoặc khó hiểu". Ông cũng đề cập rằng việc thực hiện song song sẽ khiến mọi thứ trở nên phức tạp hơn. Cuối cùng, tôi nói thêm rằng một hoạt động đường ống có tác dụng phụ sẽ dẫn đến các lỗi khó hiểu và tối nghĩa nếu hoạt động đó được thực hiện bất ngờ nhiều lần, hoặc ít nhất là một số lần khác so với dự kiến ​​của lập trình viên. (Nhưng các lập trình viên Java không viết các biểu thức lambda với các tác dụng phụ, phải không? LÀM SAO ??)

Vì vậy, đó là lý do cơ bản cho thiết kế API Luồng Java 8 cho phép truyền tải một lần và yêu cầu một đường ống tuyến tính (không phân nhánh) nghiêm ngặt. Nó cung cấp hành vi nhất quán trên nhiều nguồn luồng khác nhau, nó phân tách rõ ràng sự lười biếng khỏi các hoạt động háo hức và nó cung cấp một mô hình thực thi đơn giản.


Liên quan đến IEnumerable, tôi không phải là một chuyên gia về C # và .NET, vì vậy tôi sẽ đánh giá cao việc được sửa chữa (một cách nhẹ nhàng) nếu tôi rút ra bất kỳ kết luận không chính xác nào. Tuy nhiên, nó xuất hiện IEnumerablecho phép nhiều giao dịch hành xử khác nhau với các nguồn khác nhau; và nó cho phép một cấu trúc phân nhánh của các IEnumerablehoạt động lồng nhau , điều này có thể dẫn đến một số tính toán lại đáng kể. Mặc dù tôi đánh giá cao rằng các hệ thống khác nhau tạo ra sự đánh đổi khác nhau, đây là hai đặc điểm mà chúng tôi tìm cách tránh trong thiết kế API Luồng Java 8.

Ví dụ quicksort do OP đưa ra rất thú vị, khó hiểu và tôi rất tiếc phải nói, hơi kinh khủng. Gọi QuickSortsẽ mất một IEnumerablevà trả về một IEnumerable, vì vậy không có sự sắp xếp nào thực sự được thực hiện cho đến khi trận chung kết IEnumerableđược duyệt. Mặc dù vậy, những gì cuộc gọi dường như làm là xây dựng một cấu trúc cây IEnumerablesphản ánh phân vùng mà quicksort sẽ làm, mà không thực sự thực hiện nó. (Rốt cuộc, đây là sự tính toán lười biếng.) Nếu nguồn có N phần tử, cây sẽ là phần tử N rộng nhất, và nó sẽ ở mức lg (N) sâu.

Dường như đối với tôi - và một lần nữa, tôi không phải là chuyên gia về C # hay .NET - rằng điều này sẽ khiến một số cuộc gọi trông vô hại, chẳng hạn như lựa chọn trục qua ints.First(), đắt hơn so với vẻ ngoài của chúng. Ở cấp độ đầu tiên, dĩ nhiên, đó là O (1). Nhưng hãy xem xét một phân vùng sâu trong cây, ở cạnh bên phải. Để tính toán phần tử đầu tiên của phân vùng này, toàn bộ nguồn phải được duyệt qua, một hoạt động O (N). Nhưng vì các phân vùng ở trên là lười biếng, chúng phải được tính toán lại, yêu cầu so sánh O (lg N). Vì vậy, việc chọn trục sẽ là thao tác O (N lg N), tốn kém như toàn bộ.

Nhưng chúng tôi không thực sự sắp xếp cho đến khi chúng tôi đi qua trả lại IEnumerable. Trong thuật toán quicksort tiêu chuẩn, mỗi cấp độ phân vùng sẽ nhân đôi số lượng phân vùng. Mỗi phân vùng chỉ có một nửa kích thước, vì vậy mỗi cấp độ vẫn ở độ phức tạp O (N). Cây phân vùng cao O (lg N), vì vậy tổng công việc là O (N lg N).

Với cây IEnumerables lười biếng, ở dưới cùng của cây có N phân vùng. Việc tính toán mỗi phân vùng đòi hỏi phải có một phần tử N, mỗi phần tử yêu cầu so sánh lg (N) trên cây. Để tính toán tất cả các phân vùng ở dưới cùng của cây, sau đó, yêu cầu so sánh O (N ^ 2 lg N).

(Điều này có đúng không? Tôi khó có thể tin điều này. Ai đó làm ơn kiểm tra cái này cho tôi.)

Trong mọi trường hợp, nó thực sự tuyệt vời IEnumerablecó thể được sử dụng theo cách này để xây dựng các cấu trúc tính toán phức tạp. Nhưng nếu nó làm tăng độ phức tạp tính toán nhiều như tôi nghĩ, thì có vẻ như lập trình theo cách này là điều nên tránh trừ khi người ta cực kỳ cẩn thận.


35
Trước hết, cảm ơn bạn vì câu trả lời tuyệt vời và không hạ thấp! Đây là lời giải thích chính xác nhất và chính xác nhất mà tôi nhận được. Theo như ví dụ QuickSort, có vẻ như bạn đã đúng về ints. Đầu tiên đầy hơi khi mức độ đệ quy tăng lên. Tôi tin rằng điều này có thể dễ dàng khắc phục bằng cách tính toán 'gt' và 'lt' (bằng cách thu thập kết quả với ToArray). Điều đó đang được nói, nó chắc chắn hỗ trợ quan điểm của bạn rằng phong cách lập trình này có thể phải chịu giá hiệu suất bất ngờ. (Tiếp tục trong bình luận thứ hai)
Vitaliy

18
Mặt khác, từ kinh nghiệm của tôi với C # (hơn 5 năm) tôi có thể nói rằng việc loại bỏ các tính toán 'dư thừa' không khó lắm khi bạn gặp phải vấn đề về hiệu suất (hoặc bị cấm, Nếu ai đó không thể nghĩ ra và giới thiệu ảnh hưởng bên đó). Dường như với tôi rằng có quá nhiều sự thỏa hiệp đã được thực hiện để đảm bảo độ tinh khiết của API, với chi phí cho các khả năng như C #. Bạn chắc chắn đã giúp tôi điều chỉnh quan điểm của mình.
Vitaliy

7
@Vitaliy Cảm ơn bạn đã trao đổi ý kiến ​​công bằng. Tôi đã học được một chút về C # và .NET từ việc điều tra và viết câu trả lời này.
Stuart Marks

10
Nhận xét nhỏ: ReSharper là một tiện ích mở rộng Visual Studio giúp với C #. Với mã QuickSort ở trên, ReSharper thêm cảnh báo cho mỗi lần sử dụngints : "Có thể liệt kê nhiều số liệu của IEnumerable". Sử dụng IEenumerablenhiều hơn một lần là đáng ngờ và nên tránh. Tôi cũng chỉ ra câu hỏi này (mà tôi đã trả lời), trong đó cho thấy một số cảnh báo với cách tiếp cận .Net (bên cạnh hiệu suất kém): Liệt kê <T> và sự khác biệt đáng kể của
IE

4
@Kobi Rất thú vị khi có một cảnh báo như vậy trong ReSharper. Cảm ơn con trỏ cho câu trả lời của bạn. Tôi không biết C # /. NET vì vậy tôi sẽ phải chọn nó một cách cẩn thận, nhưng dường như nó thể hiện các vấn đề tương tự như các mối quan tâm thiết kế mà tôi đã đề cập ở trên.
Stuart Marks

122

Lý lịch

Trong khi câu hỏi có vẻ đơn giản, câu trả lời thực tế đòi hỏi một số nền tảng để có ý nghĩa. Nếu bạn muốn bỏ qua kết luận, hãy cuộn xuống ...

Chọn điểm so sánh của bạn - Chức năng cơ bản

Sử dụng các khái niệm cơ bản, khái niệm của C # IEnumerablecó liên quan chặt chẽ hơn với JavaIterable , có thể tạo ra nhiều Trình lặp như bạn muốn. IEnumerablestạo ra IEnumerators. Java IterabletạoIterators

Lịch sử của mỗi khái niệm là tương tự nhau, trong đó cả hai IEnumerableIterablecó một động lực cơ bản để cho phép lặp kiểu 'for-every' trên các thành viên của bộ sưu tập dữ liệu. Đó là một sự đơn giản hóa vì cả hai đều cho phép nhiều hơn thế, và họ cũng đến giai đoạn đó thông qua các tiến trình khác nhau, nhưng đó là một tính năng phổ biến đáng kể bất kể.

Chúng ta hãy so sánh tính năng đó: trong cả hai ngôn ngữ, nếu một lớp thực hiện IEnumerable/ Iterable, thì lớp đó phải thực hiện ít nhất một phương thức duy nhất (đối với C #, đó là GetEnumeratorđối với Java iterator()). Trong mỗi trường hợp, thể hiện được trả về từ đó ( IEnumerator/ Iterator) cho phép bạn truy cập các thành viên hiện tại và tiếp theo của dữ liệu. Tính năng này được sử dụng trong cú pháp ngôn ngữ cho từng ngôn ngữ.

Chọn điểm so sánh của bạn - Chức năng nâng cao

IEnumerabletrong C # đã được mở rộng để cho phép một số tính năng ngôn ngữ khác ( chủ yếu liên quan đến Linq ). Các tính năng được thêm vào bao gồm các lựa chọn, phép chiếu, tập hợp, v.v. Các tiện ích mở rộng này có động lực mạnh mẽ từ việc sử dụng trong lý thuyết tập hợp, tương tự như các khái niệm Cơ sở dữ liệu quan hệ và SQL.

Java 8 cũng đã được thêm chức năng để cho phép một mức độ lập trình chức năng bằng cách sử dụng Streams và Lambdas. Lưu ý rằng các luồng Java 8 không chủ yếu được thúc đẩy bởi lý thuyết tập hợp, mà bằng lập trình chức năng. Bất kể, có rất nhiều điểm tương đồng.

Vì vậy, đây là điểm thứ hai. Các cải tiến được thực hiện cho C # đã được triển khai như một sự cải tiến cho IEnumerablekhái niệm này. Tuy nhiên, trong Java, các cải tiến được thực hiện bằng cách tạo ra các khái niệm cơ bản mới về Lambdas và Stream, và sau đó cũng tạo ra một cách tương đối tầm thường để chuyển đổi từ IteratorsIterablessang Stream và ngược lại.

Vì vậy, việc so sánh IEnumerable với khái niệm Stream của Java là chưa hoàn chỉnh. Bạn cần so sánh nó với API Luồng và Bộ sưu tập kết hợp trong Java.

Trong Java, Luồng không giống như Iterables hoặc Iterators

Các luồng không được thiết kế để giải quyết các vấn đề giống như các trình lặp:

  • Lặp đi lặp lại là một cách để mô tả chuỗi dữ liệu.
  • Luồng là một cách mô tả một chuỗi các biến đổi dữ liệu.

Với một Iterator, bạn nhận được một giá trị dữ liệu, xử lý nó và sau đó nhận một giá trị dữ liệu khác.

Với Luồng, bạn xâu chuỗi một chuỗi các chức năng lại với nhau, sau đó bạn cung cấp giá trị đầu vào cho luồng và nhận giá trị đầu ra từ chuỗi kết hợp. Lưu ý, theo thuật ngữ Java, mỗi hàm được gói gọn trong một Streamthể hiện duy nhất . API Streams cho phép bạn liên kết một chuỗi các Streamtrường hợp theo cách xâu chuỗi một chuỗi các biểu thức chuyển đổi.

Để hoàn thành Streamkhái niệm, bạn cần một nguồn dữ liệu để cung cấp luồng và chức năng đầu cuối tiêu thụ luồng.

Cách bạn cung cấp các giá trị vào luồng trên thực tế có thể là từ một Iterable, nhưng Streambản thân chuỗi không phải là một Iterable, nó là một hàm ghép.

A Streamcũng có ý định lười biếng, theo nghĩa là nó chỉ hoạt động khi bạn yêu cầu một giá trị từ nó.

Lưu ý các giả định và tính năng quan trọng của Luồng:

  • A Streamtrong Java là một công cụ chuyển đổi, nó biến đổi một mục dữ liệu ở một trạng thái, sang trạng thái khác.
  • các luồng không có khái niệm về thứ tự hoặc vị trí dữ liệu, chỉ cần chuyển đổi bất cứ thứ gì chúng được yêu cầu.
  • các luồng có thể được cung cấp với dữ liệu từ nhiều nguồn, bao gồm các luồng khác, Iterators, Iterables, Collection,
  • bạn không thể "thiết lập lại" một luồng, điều đó sẽ giống như "lập trình lại chuyển đổi". Đặt lại nguồn dữ liệu có lẽ là những gì bạn muốn.
  • về mặt logic, chỉ có 1 mục dữ liệu 'trong chuyến bay' trong luồng bất kỳ lúc nào (trừ khi luồng là luồng song song, tại thời điểm đó, có 1 mục trên mỗi luồng). Điều này độc lập với nguồn dữ liệu có thể có nhiều hơn các mục hiện tại 'sẵn sàng' được cung cấp cho luồng hoặc trình thu thập luồng có thể cần tổng hợp và giảm nhiều giá trị.
  • Các luồng có thể không liên kết (vô hạn), chỉ bị giới hạn bởi nguồn dữ liệu hoặc trình thu thập (cũng có thể là vô hạn).
  • Các luồng là 'chuỗi', đầu ra của việc lọc một luồng, là một luồng khác. Các giá trị đầu vào và được chuyển đổi bởi một luồng có thể lần lượt được cung cấp cho một luồng khác thực hiện một chuyển đổi khác. Dữ liệu, ở trạng thái biến đổi của nó chảy từ luồng này sang luồng tiếp theo. Bạn không cần phải can thiệp và kéo dữ liệu từ một luồng và cắm nó vào luồng tiếp theo.

So sánh C #

Khi bạn cho rằng Luồng Java chỉ là một phần của hệ thống cung cấp, truyền phát và thu thập và Luồng và Trình lặp thường được sử dụng cùng với Bộ sưu tập, thì không có gì lạ khi khó liên quan đến các khái niệm tương tự hầu như tất cả được nhúng vào một IEnumerablekhái niệm duy nhất trong C #.

Các phần của IEnumerable (và các khái niệm liên quan chặt chẽ) là rõ ràng trong tất cả các khái niệm Iterator, Iterable, Lambda và Stream của Java.

Có những điều nhỏ mà các khái niệm Java có thể làm khó hơn trong IEnumerable và ngược lại.


Phần kết luận

  • Không có vấn đề thiết kế nào ở đây, chỉ là vấn đề trong việc kết hợp các khái niệm giữa các ngôn ngữ.
  • Luồng giải quyết vấn đề theo một cách khác
  • Các luồng thêm chức năng vào Java (chúng thêm một cách làm khác, chúng không lấy đi chức năng)

Thêm Luồng cho bạn nhiều lựa chọn hơn khi giải quyết vấn đề, điều này là công bằng để phân loại là 'tăng cường sức mạnh', chứ không phải 'giảm bớt', 'lấy đi' hoặc 'hạn chế' nó.

Tại sao Luồng Java lại một lần?

Câu hỏi này là sai, bởi vì các luồng là chuỗi chức năng, không phải dữ liệu. Tùy thuộc vào nguồn dữ liệu cung cấp luồng, bạn có thể đặt lại nguồn dữ liệu và cung cấp cùng luồng hoặc luồng khác nhau.

Không giống như IEnumerable của C #, trong đó một đường ống thực thi có thể được thực thi nhiều lần như chúng ta muốn, trong Java, một luồng có thể được 'lặp lại' chỉ một lần.

So sánh một IEnumerablevới một Streamlà sai lầm. Ngữ cảnh bạn đang sử dụng để nói IEnumerablecó thể được thực thi nhiều lần bạn muốn, tốt nhất so với Java Iterables, có thể được lặp lại nhiều lần như bạn muốn. Một Java Streamđại diện cho một tập hợp con của IEnumerablekhái niệm và không phải là tập hợp con cung cấp dữ liệu và do đó không thể là "chạy lại".

Bất kỳ cuộc gọi đến một hoạt động đầu cuối sẽ đóng luồng, khiến nó không thể sử dụng được. 'Tính năng' này lấy đi rất nhiều năng lượng.

Phát biểu đầu tiên là đúng, theo một nghĩa nào đó. Tuyên bố 'lấy đi quyền lực' thì không. Bạn vẫn đang so sánh Streams nó IEnumerables. Hoạt động đầu cuối trong luồng giống như mệnh đề 'break' trong vòng lặp for. Bạn luôn có thể có một luồng khác, nếu bạn muốn và nếu bạn có thể cung cấp lại dữ liệu bạn cần. Một lần nữa, nếu bạn coi IEnumerablenó giống như một Iterable, đối với câu lệnh này, Java sẽ làm tốt.

Tôi tưởng tượng lý do cho việc này không phải là kỹ thuật. Những cân nhắc thiết kế đằng sau hạn chế kỳ lạ này là gì?

Lý do là về mặt kỹ thuật và vì một lý do đơn giản là Stream một tập hợp con của những gì nó nghĩ. Tập hợp con luồng không kiểm soát việc cung cấp dữ liệu, vì vậy bạn nên đặt lại nguồn cung cấp, không phải luồng. Trong bối cảnh đó, nó không quá xa lạ.

Ví dụ QuickSort

Ví dụ quicksort của bạn có chữ ký:

IEnumerable<int> QuickSort(IEnumerable<int> ints)

Bạn đang coi đầu vào IEnumerablelà nguồn dữ liệu:

IEnumerable<int> lt = ints.Where(i => i < pivot);

Ngoài ra, giá trị trả về IEnumerablecũng là một nguồn cung cấp dữ liệu và vì đây là hoạt động Sắp xếp, nên thứ tự của nguồn cung cấp đó rất quan trọng. Nếu bạn coi Iterablelớp Java là đối sánh phù hợp cho điều này, cụ thể là Listchuyên môn hóa Iterable, vì Danh sách là nguồn cung cấp dữ liệu có thứ tự hoặc lặp được đảm bảo, thì mã Java tương đương với mã của bạn sẽ là:

Stream<Integer> quickSort(List<Integer> ints) {
    // Using a stream to access the data, instead of the simpler ints.isEmpty()
    if (!ints.stream().findAny().isPresent()) {
        return Stream.of();
    }

    // treating the ints as a data collection, just like the C#
    final Integer pivot = ints.get(0);

    // Using streams to get the two partitions
    List<Integer> lt = ints.stream().filter(i -> i < pivot).collect(Collectors.toList());
    List<Integer> gt = ints.stream().filter(i -> i > pivot).collect(Collectors.toList());

    return Stream.concat(Stream.concat(quickSort(lt), Stream.of(pivot)),quickSort(gt));
}    

Lưu ý rằng có một lỗi (mà tôi đã sao chép), trong đó sắp xếp không xử lý các giá trị trùng lặp một cách duyên dáng, đó là một loại 'giá trị duy nhất'.

Cũng lưu ý cách mã Java sử dụng nguồn dữ liệu ( List) và các khái niệm truyền phát tại các điểm khác nhau và trong C # hai "tính cách" đó có thể được thể hiện chỉ trong IEnumerable. Ngoài ra, mặc dù tôi đã sử dụng Listlàm loại cơ sở, tôi có thể đã sử dụng tổng quát hơn Collectionvà với một chuyển đổi iterator-to-Stream nhỏ, tôi có thể đã sử dụng tổng quát hơnIterable


9
Nếu bạn đang nghĩ về 'lặp đi lặp lại' một luồng, bạn đang làm sai. Một luồng biểu thị trạng thái dữ liệu tại một thời điểm cụ thể trong một chuỗi các phép biến đổi. Dữ liệu đi vào hệ thống trong một nguồn phát, sau đó chảy từ luồng này sang luồng tiếp theo, thay đổi trạng thái khi nó đi, cho đến khi nó được thu thập, giảm hoặc đổ, ở cuối. A Streamlà một khái niệm theo thời gian, không phải là 'hoạt động vòng lặp' .... (tiếp)
rolfl

7
Với Luồng, bạn có dữ liệu vào luồng trông giống X và thoát khỏi luồng trông giống Y. Có một chức năng mà luồng thực hiện chuyển đổi đó f(x)Luồng đóng gói chức năng, nó không gói gọn dữ liệu chảy qua
rolfl

4
IEnumerablecũng có thể cung cấp các giá trị ngẫu nhiên, không bị ràng buộc và hoạt động trước khi dữ liệu tồn tại.
Arturo Torres Sánchez

6
@Vitaliy: Nhiều phương thức nhận được IEnumerable<T>kỳ vọng nó đại diện cho một bộ sưu tập hữu hạn có thể được lặp lại nhiều lần. Một số điều có thể lặp lại nhưng không đáp ứng các điều kiện đó IEnumerable<T>do không có giao diện chuẩn nào phù hợp với dự luật, nhưng các phương pháp mong đợi các bộ sưu tập hữu hạn có thể lặp đi lặp lại nhiều lần sẽ dễ bị hỏng nếu được đưa ra những điều có thể lặp lại không tuân theo các điều kiện đó .
supercat

5
quickSortVí dụ của bạn có thể đơn giản hơn nhiều nếu nó trả về a Stream; nó sẽ lưu hai .stream()cuộc gọi và một .collect(Collectors.toList())cuộc gọi. Nếu sau đó bạn thay thế Collections.singleton(pivot).stream()bằng Stream.of(pivot)mã trở nên gần như có thể đọc được
Holger

22

Streams được xây dựng xung quanh Spliterators là các đối tượng trạng thái, có thể thay đổi. Họ không có hành động thiết lập lại trên mạng và thực tế, yêu cầu hỗ trợ hành động tua lại như vậy sẽ làm mất đi nhiều sức mạnh. Làm thế nào sẽ Random.ints()có nghĩa vụ để xử lý một yêu cầu như vậy?

Mặt khác, đối với Streamcác s có nguồn gốc có thể truy xuất lại, có thể dễ dàng xây dựng một tương đương Streamđể được sử dụng lại. Chỉ cần đặt các bước được thực hiện để xây dựng Streamthành một phương pháp có thể sử dụng lại. Hãy nhớ rằng việc lặp lại các bước này không phải là một hoạt động tốn kém vì tất cả các bước này là các hoạt động lười biếng; công việc thực tế bắt đầu với hoạt động của thiết bị đầu cuối và tùy thuộc vào hoạt động của thiết bị đầu cuối thực tế, mã khác nhau hoàn toàn có thể được thực thi.

Tùy thuộc vào bạn, người viết phương thức như vậy, sẽ chỉ định cách gọi phương thức hai lần ngụ ý: nó có tái tạo chính xác cùng một chuỗi không, như các luồng được tạo cho một mảng hoặc bộ sưu tập không được sửa đổi, hoặc nó tạo ra một luồng với một ngữ nghĩa tương tự nhưng các yếu tố khác nhau như một luồng ints ngẫu nhiên hoặc một luồng các dòng đầu vào giao diện điều khiển, v.v.


Bằng cách này, để tránh nhầm lẫn, một hoạt động thiết bị đầu cuối tiêu thụ các Streamkhác biệt với đóng cửa các Streamnhư gọi close()trên dòng nào (đó là cần thiết cho con suối có liên quan đến nguồn lực như thế nào, ví dụ như sản xuất bởi Files.lines()).


Có vẻ như rất nhiều nhầm lẫn bắt nguồn từ so sánh sai IEnumerablevới Stream. An IEnumerableđại diện cho khả năng cung cấp một thực tế IEnumerator, vì vậy nó giống như Iterabletrong Java. Ngược lại, a Streamlà một loại trình vòng lặp và có thể so sánh với một IEnumeratorvì vậy thật sai lầm khi cho rằng loại dữ liệu này có thể được sử dụng nhiều lần trong .NET, hỗ trợ cho IEnumerator.Resetlà tùy chọn. Các ví dụ được thảo luận ở đây thay vì sử dụng thực tế là IEnumerablecó thể được sử dụng để tìm nạp các IEnumerator s mới và cũng hoạt động với các Java Collection; bạn có thể có được một mới Stream. Nếu các nhà phát triển Java quyết định thêm trực tiếp các Streamhoạt động Iterable, với các hoạt động trung gian sẽ trả về một hoạt động khácIterable, nó thực sự có thể so sánh và nó có thể hoạt động theo cùng một cách.

Tuy nhiên, các nhà phát triển đã quyết định chống lại nó và quyết định được thảo luận trong câu hỏi này . Điểm lớn nhất là sự nhầm lẫn về các hoạt động Thu thập háo hức và các hoạt động Stream lười biếng. Bằng cách nhìn vào API .NET, tôi (vâng, cá nhân) thấy nó hợp lý. Mặc dù trông có vẻ hợp lý khi chỉ nhìn IEnumerablemột mình, một Bộ sưu tập cụ thể sẽ có rất nhiều phương thức thao túng trực tiếp Bộ sưu tập và rất nhiều phương thức trả lại sự lười biếng IEnumerable, trong khi bản chất cụ thể của phương pháp không phải lúc nào cũng có thể nhận ra bằng trực giác. Ví dụ tồi tệ nhất mà tôi tìm thấy (trong vài phút tôi đã xem xét) là List.Reverse()tên của nó khớp chính xác với tên của người được thừa kế (đây có phải là điểm cuối đúng cho các phương thức mở rộng không?) Enumerable.Reverse()Trong khi có hành vi hoàn toàn trái ngược.


Tất nhiên, đây là hai quyết định riêng biệt. Loại thứ nhất tạo ra Streammột loại khác biệt với Iterable/ Collectionvà loại thứ hai để tạo ra Streammột loại trình lặp một lần thay vì một loại lặp khác. Nhưng những quyết định này đã được đưa ra cùng nhau và nó có thể là trường hợp phân tách hai quyết định này không bao giờ được xem xét. Nó không được tạo ra có thể so sánh với .NET.

Quyết định thiết kế API thực tế là để thêm một kiểu cải tiến của iterator, các Spliterator. Spliterators có thể được cung cấp bởi các Iterables cũ (đó là cách chúng được trang bị thêm) hoặc các triển khai hoàn toàn mới. Sau đó, Streamđã được thêm vào như một giao diện người dùng cấp cao đến cấp độ khá thấp Spliterator. Đó là nó. Bạn có thể thảo luận về việc liệu một thiết kế khác sẽ tốt hơn, nhưng điều đó không hiệu quả, nó sẽ không thay đổi, theo cách chúng được thiết kế bây giờ.

Có một khía cạnh thực hiện khác mà bạn phải xem xét. Streams không phải là cấu trúc dữ liệu bất biến. Mỗi hoạt động trung gian có thể trả về một thể hiện mới Streamđóng gói một thể hiện cũ nhưng nó cũng có thể điều khiển cá thể của chính nó thay vào đó và trả về chính nó (điều đó không loại trừ ngay cả khi thực hiện cả hai cho cùng một hoạt động). Các ví dụ thường được biết đến là các hoạt động như parallelhoặc unorderedkhông thêm bước nào khác mà thao túng toàn bộ đường ống). Có cấu trúc dữ liệu có thể thay đổi như vậy và cố gắng sử dụng lại (hoặc thậm chí tệ hơn, sử dụng nó nhiều lần cùng một lúc) không chơi tốt


Để đầy đủ, đây là ví dụ quicksort của bạn được dịch sang StreamAPI Java . Điều đó cho thấy rằng nó không thực sự làm mất đi nhiều sức mạnh.

static Stream<Integer> quickSort(Supplier<Stream<Integer>> ints) {

  final Optional<Integer> optPivot = ints.get().findAny();
  if(!optPivot.isPresent()) return Stream.empty();

  final int pivot = optPivot.get();

  Supplier<Stream<Integer>> lt = ()->ints.get().filter(i -> i < pivot);
  Supplier<Stream<Integer>> gt = ()->ints.get().filter(i -> i > pivot);

  return Stream.of(quickSort(lt), Stream.of(pivot), quickSort(gt)).flatMap(s->s);
}

Nó có thể được sử dụng như

List<Integer> l=new Random().ints(100, 0, 1000).boxed().collect(Collectors.toList());
System.out.println(l);
System.out.println(quickSort(l::stream)
    .map(Object::toString).collect(Collectors.joining(", ")));

Bạn có thể viết nó nhỏ gọn hơn nữa

static Stream<Integer> quickSort(Supplier<Stream<Integer>> ints) {
    return ints.get().findAny().map(pivot ->
         Stream.of(
                   quickSort(()->ints.get().filter(i -> i < pivot)),
                   Stream.of(pivot),
                   quickSort(()->ints.get().filter(i -> i > pivot)))
        .flatMap(s->s)).orElse(Stream.empty());
}

1
Chà, tiêu thụ hay không, cố gắng tiêu thụ nó một lần nữa ném ra một ngoại lệ là luồng đã bị đóng , không được tiêu thụ. Đối với vấn đề đặt lại một luồng các số nguyên ngẫu nhiên, như bạn đã nói - tùy thuộc vào người viết thư viện xác định hợp đồng chính xác của thao tác đặt lại.
Vitaliy

2
Không, thông báo là luồng Stream đã được vận hành hoặc đóng lại và chúng tôi không nói về một hoạt động thiết lập lại cài đặt trực tuyến nhưng gọi hai hoặc nhiều hoạt động của thiết bị đầu cuối trên Streamđó trong khi việc đặt lại các nguồn Spliteratorsẽ được ngụ ý. Và tôi khá chắc chắn nếu điều đó là có thể, đã có câu hỏi về SO như Tại sao việc gọi count()hai lần một lần Streamcho kết quả khác nhau mỗi lần, v.v.
Holger

1
Nó hoàn toàn hợp lệ cho Count () để đưa ra các kết quả khác nhau. Count () là một truy vấn trên một luồng và nếu luồng đó có thể thay đổi (hoặc chính xác hơn, luồng đại diện cho kết quả của một truy vấn trên một bộ sưu tập có thể thay đổi) thì nó được mong đợi. Hãy xem API của C #. Họ giải quyết tất cả những vấn đề này một cách duyên dáng.
Vitaliy

4
Những gì bạn gọi là hoàn toàn hợp lệ, là một hành vi phản trực giác. Rốt cuộc, đó là động lực chính để hỏi về việc sử dụng một luồng nhiều lần để xử lý kết quả, dự kiến ​​sẽ giống nhau, theo những cách khác nhau. Mọi câu hỏi về SO về bản chất không thể tái sử dụng của Streams cho đến nay đều xuất phát từ nỗ lực giải quyết vấn đề bằng cách gọi các hoạt động của thiết bị đầu cuối nhiều lần (rõ ràng, nếu không bạn sẽ không chú ý) dẫn đến một giải pháp âm thầm bị phá vỡ nếu StreamAPI cho phép với kết quả khác nhau trên mỗi đánh giá. Đây là một ví dụ tốt đẹp .
Holger

3
Trên thực tế, ví dụ của bạn thể hiện hoàn hảo những gì xảy ra nếu một lập trình viên không hiểu ý nghĩa của việc áp dụng nhiều hoạt động của thiết bị đầu cuối. Chỉ cần nghĩ về những gì xảy ra khi mỗi hoạt động này sẽ được áp dụng cho một tập hợp các yếu tố hoàn toàn khác nhau. Nó chỉ hoạt động nếu nguồn của luồng trả về các phần tử giống nhau trên mỗi truy vấn nhưng đây chính xác là giả định sai mà chúng ta đang nói đến.
Holger

8

Tôi nghĩ rằng có rất ít sự khác biệt giữa hai khi bạn nhìn kỹ.

Ở mặt của nó, một cái IEnumerabledường như là một cấu trúc có thể tái sử dụng:

IEnumerable<int> numbers = new int[] { 1, 2, 3, 4, 5 };

foreach (var n in numbers) {
    Console.WriteLine(n);
}

Tuy nhiên, trình biên dịch thực sự đang làm một chút công việc để giúp chúng tôi ra ngoài; nó tạo ra đoạn mã sau:

IEnumerable<int> numbers = new int[] { 1, 2, 3, 4, 5 };

IEnumerator<int> enumerator = numbers.GetEnumerator();
while (enumerator.MoveNext()) {
    Console.WriteLine(enumerator.Current);
}

Mỗi lần bạn thực sự lặp đi lặp lại trên vô số, trình biên dịch sẽ tạo ra một điều tra viên. Điều tra viên không thể tái sử dụng; các cuộc gọi tiếp theo MoveNextsẽ chỉ trả về false và không có cách nào để thiết lập lại từ đầu. Nếu bạn muốn lặp lại các số một lần nữa, bạn sẽ cần tạo một thể hiện liệt kê khác.


Để minh họa rõ hơn rằng IEnumerable có (có thể có) 'tính năng' giống như Luồng Java, hãy xem xét một liệt kê có nguồn số không phải là một bộ sưu tập tĩnh. Ví dụ: chúng ta có thể tạo một đối tượng có thể đếm được, tạo ra một chuỗi gồm 5 số ngẫu nhiên:

class Generator : IEnumerator<int> {
    Random _r;
    int _current;
    int _count = 0;

    public Generator(Random r) {
        _r = r;
    }

    public bool MoveNext() {
        _current= _r.Next();
        _count++;
        return _count <= 5;
    }

    public int Current {
        get { return _current; }
    }
 }

class RandomNumberStream : IEnumerable<int> {
    Random _r = new Random();
    public IEnumerator<int> GetEnumerator() {
        return new Generator(_r);
    }
    public IEnumerator IEnumerable.GetEnumerator() {
        return this.GetEnumerator();
    }
}

Bây giờ chúng ta có mã rất giống với liệt kê dựa trên mảng trước đó, nhưng với lần lặp thứ hai hơn numbers:

IEnumerable<int> numbers = new RandomNumberStream();

foreach (var n in numbers) {
    Console.WriteLine(n);
}
foreach (var n in numbers) {
    Console.WriteLine(n);
}

Lần thứ hai chúng ta lặp đi lặp lại, numberschúng ta sẽ nhận được một chuỗi số khác nhau, không thể sử dụng lại theo cùng một nghĩa. Hoặc, chúng ta có thể đã viết RandomNumberStreammột ngoại lệ nếu bạn cố gắng lặp đi lặp lại nhiều lần, làm cho vô số thực sự không thể sử dụng được (như Luồng Java).

Ngoài ra, sắp xếp nhanh chóng dựa trên vô số của bạn có nghĩa là gì khi áp dụng cho một RandomNumberStream?


Phần kết luận

Vì vậy, sự khác biệt lớn nhất là .NET cho phép bạn sử dụng lại một IEnumerablecách hoàn toàn bằng cách tạo một cái mới IEnumeratortrong nền bất cứ khi nào nó cần để truy cập các phần tử trong chuỗi.

Hành vi ngầm này thường hữu ích (và 'mạnh mẽ' như bạn nêu), bởi vì chúng ta có thể lặp đi lặp lại trên một bộ sưu tập.

Nhưng đôi khi, hành vi ngầm này thực sự có thể gây ra vấn đề. Nếu nguồn dữ liệu của bạn không tĩnh hoặc tốn kém để truy cập (như cơ sở dữ liệu hoặc trang web), thì rất nhiều giả định về việc IEnumerablephải được loại bỏ; tái sử dụng không phải là đơn giản


2

Có thể bỏ qua một số biện pháp bảo vệ "chạy một lần" trong API Stream; ví dụ: chúng ta có thể tránh các java.lang.IllegalStateExceptiontrường hợp ngoại lệ (với thông báo "luồng đã được vận hành theo hoặc đóng") bằng cách tham chiếu và sử dụng lại Spliterator(chứ không phải Streamtrực tiếp).

Ví dụ: mã này sẽ chạy mà không ném ngoại lệ:

    Spliterator<String> split = Stream.of("hello","world")
                                      .map(s->"prefix-"+s)
                                      .spliterator();

    Stream<String> replayable1 = StreamSupport.stream(split,false);
    Stream<String> replayable2 = StreamSupport.stream(split,false);


    replayable1.forEach(System.out::println);
    replayable2.forEach(System.out::println);

Tuy nhiên, đầu ra sẽ được giới hạn ở

prefix-hello
prefix-world

thay vì lặp lại đầu ra hai lần. Điều này là do nguồn ArraySpliteratorđược sử dụng làm Streamnguồn là trạng thái và lưu trữ vị trí hiện tại của nó. Khi chúng tôi phát lại điều này, Streamchúng tôi bắt đầu lại vào cuối.

Chúng tôi có một số tùy chọn để giải quyết thách thức này:

  1. Chúng ta có thể sử dụng một trạng thái không quốc tịch Stream phương pháp tạo như Stream#generate(). Chúng tôi sẽ phải quản lý trạng thái bên ngoài bằng mã riêng của mình và đặt lại giữa Stream"replay":

    Spliterator<String> split = Stream.generate(this::nextValue)
                                      .map(s->"prefix-"+s)
                                      .spliterator();
    
    Stream<String> replayable1 = StreamSupport.stream(split,false);
    Stream<String> replayable2 = StreamSupport.stream(split,false);
    
    
    replayable1.forEach(System.out::println);
    this.resetCounter();
    replayable2.forEach(System.out::println);
  2. Một giải pháp khác (tốt hơn một chút nhưng không hoàn hảo) cho vấn đề này là viết riêng của chúng tôi ArraySpliterator (hoặc Streamnguồn tương tự ) bao gồm một số khả năng để thiết lập lại bộ đếm hiện tại. Nếu chúng ta sử dụng nó để tạo ra, Streamchúng ta có thể phát lại chúng thành công.

    MyArraySpliterator<String> arraySplit = new MyArraySpliterator("hello","world");
    Spliterator<String> split = StreamSupport.stream(arraySplit,false)
                                            .map(s->"prefix-"+s)
                                            .spliterator();
    
    Stream<String> replayable1 = StreamSupport.stream(split,false);
    Stream<String> replayable2 = StreamSupport.stream(split,false);
    
    
    replayable1.forEach(System.out::println);
    arraySplit.reset();
    replayable2.forEach(System.out::println);
  3. Giải pháp tốt nhất cho vấn đề này (theo ý kiến ​​của tôi) là tạo một bản sao mới của bất kỳ trạng thái nào Spliteratorđược sử dụng trong Streamđường ống khi các toán tử mới được gọi trên Stream. Điều này phức tạp hơn và liên quan để thực hiện, nhưng nếu bạn không phiền khi sử dụng thư viện của bên thứ ba, cyclops-Reac có một Streamtriển khai thực hiện chính xác điều này. (Tiết lộ: Tôi là nhà phát triển chính cho dự án này.)

    Stream<String> replayableStream = ReactiveSeq.of("hello","world")
                                                 .map(s->"prefix-"+s);
    
    
    
    
    replayableStream.forEach(System.out::println);
    replayableStream.forEach(System.out::println);

Cái này sẽ in

prefix-hello
prefix-world
prefix-hello
prefix-world

như mong đợi.

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.