2015-11-10 14 views
69

Tôi đang đọc về các luồng Java và khám phá những điều mới khi tôi đi cùng. Một trong những điều mới tôi tìm thấy là hàm peek(). Hầu hết mọi thứ tôi đã đọc trên peek đều nói rằng nó nên được sử dụng để gỡ lỗi các luồng của bạn.Trong luồng Java là peek thực sự chỉ để gỡ lỗi?

Điều gì sẽ xảy ra nếu tôi có Luồng mà mỗi Tài khoản có tên người dùng, trường mật khẩu và phương thức login() và loggedIn().

Tôi cũng có

Consumer<Account> login = account -> account.login(); 

Predicate<Account> loggedIn = account -> account.loggedIn(); 

Tại sao điều này sẽ rất xấu?

List<Account> accounts; //assume it's been setup 
List<Account> loggedInAccount = 
accounts.stream() 
    .peek(login) 
    .filter(loggedIn) 
    .collect(Collectors.toList()); 

Hiện tại, tôi có thể nói chính xác điều này dự định làm. Nó;

  • Mất một danh sách tài khoản
  • cố gắng đăng nhập vào từng tài khoản một
  • lọc ra bất kỳ tài khoản mà chưa đăng nhập
  • thu thập các đăng nhập tài khoản vào một danh sách mới

Nhược điểm của việc làm như thế này là gì? Bất kỳ lý do gì tôi không nên tiếp tục? Cuối cùng, nếu không phải là giải pháp này thì sao?

Phiên bản gốc của phương thức này đã sử dụng phương thức .filter() như sau;

+13

Bất cứ lúc nào tôi thấy mình cần một đa dòng lambda, tôi di chuyển dây chuyền đến một phương pháp riêng và vượt qua tham chiếu phương thức thay vì lambda. – VGR

+0

Vâng, tôi hiểu điều này. Tôi chỉ cố gắng thể hiện rõ hơn những gì tôi đang cố gắng đạt được. Cảm ơn mặc dù :) –

+1

Mục đích là gì - bạn đang cố gắng đăng nhập tất cả các tài khoản trong * và * lọc chúng dựa trên liệu chúng có đăng nhập hay không (điều này có thể không đúng)? Hoặc, bạn có muốn đăng nhập chúng vào, * sau đó * lọc chúng dựa trên việc chúng có đăng nhập hay không? Tôi đang yêu cầu điều này theo thứ tự này bởi vì 'forEach' có thể là thao tác bạn muốn trái với' peek'. Chỉ vì nó trong API không có nghĩa là nó không mở để lạm dụng (như 'Optional.of'). – Makoto

Trả lời

41

các takeaway chính từ này:

không sử dụng API trong một cách không mong muốn, ngay cả khi nó hoàn thành mục tiêu ngay lập tức của bạn. Cách tiếp cận đó có thể xảy ra trong tương lai và cũng không rõ ràng với những người bảo trì trong tương lai.


Không có hại trong việc phá vỡ điều này với nhiều thao tác vì chúng là các hoạt động riêng biệt. Có gây hại khi sử dụng API theo cách không rõ ràng và không mong muốn, có thể có phân nhánh nếu hành vi cụ thể này được sửa đổi trong các phiên bản tương lai của Java.

Sử dụng forEach vào hoạt động này sẽ làm cho nó rõ ràng cho nhà duy trì rằng có một tác dụng phụ dành trên mỗi phần tử của accounts, và rằng bạn đang thực hiện một số hoạt động mà có thể đột biến nó.

Nó cũng thông thường hơn theo nghĩa là peek là một hoạt động trung gian không hoạt động trên toàn bộ bộ sưu tập cho đến khi vận hành đầu cuối chạy, nhưng forEach thực sự là một hoạt động đầu cuối. Bằng cách này, bạn có thể tạo lập luận mạnh mẽ về hành vi và luồng mã của bạn thay vì đặt câu hỏi về nếu peek hoạt động giống như forEach trong ngữ cảnh này.

accounts.forEach(a -> a.login()); 
List<Account> loggedInAccounts = accounts.stream() 
             .filter(Account::loggedIn) 
             .collect(Collectors.toList()); 
+2

Nếu bạn thực hiện đăng nhập trong bước tiền xử lý, bạn không cần luồng nào cả. Bạn có thể thực hiện 'forEach' ngay tại bộ sưu tập nguồn:' accounts.forEach (a -> a.login()); ' – Holger

+1

@ Holger: Điểm tuyệt vời. Tôi đã kết hợp điều đó vào câu trả lời. – Makoto

+0

Câu trả lời hay! Tôi đã bị giằng xé giữa điều này và những gì @ Holger nói. Cả hai đều chuyển tải cùng một điều, nhưng điều này có vẻ hơi rõ ràng hơn và có một ví dụ tốt đẹp quá. Tôi sẽ làm theo cách tiếp cận này, mặc dù tôi nghi ngờ sẽ có một nhà phát triển khác sử dụng điều này, bản thân tôi có thể quên đi ý định của hàm. –

57

Điều quan trọng bạn phải hiểu, là con suối được thúc đẩy bởi hoạt động terminal. Các hoạt động thiết bị đầu cuối xác định xem tất cả các yếu tố phải được xử lý hoặc bất kỳ ở tất cả. Vì vậy, collect là một hoạt động xử lý từng mục, trong khi findAny có thể ngừng xử lý các mục khi nó gặp phải một phần tử phù hợp.

count() không được xử lý bất kỳ thành phần nào khi có thể xác định kích thước của luồng mà không xử lý các mục. Vì đây là một tối ưu hóa không được thực hiện trong Java 8, nhưng sẽ được trong Java 9, có thể có bất ngờ khi bạn chuyển sang Java 9 và có mã dựa trên count() xử lý tất cả các mục. Điều này cũng được kết nối với các chi tiết phụ thuộc triển khai khác, ví dụ: ngay cả trong Java 9, việc triển khai tham chiếu sẽ không thể dự đoán kích thước của nguồn luồng vô hạn kết hợp với limit trong khi không có giới hạn cơ bản nào ngăn chặn dự đoán đó.

Kể từ peek cho phép “thực hiện hành động được cung cấp trên mỗi phần tử như các yếu tố được tiêu thụ từ các kết quả dòng”, nó không uỷ quyền xử lý các yếu tố nhưng sẽ thực hiện hành động phụ thuộc vào những gì cần các hoạt động thiết bị đầu cuối. Điều này ngụ ý rằng bạn phải sử dụng nó cẩn thận nếu bạn cần một quá trình xử lý cụ thể, ví dụ: muốn áp dụng một hành động trên tất cả các yếu tố. Nó hoạt động nếu hoạt động đầu cuối được đảm bảo xử lý tất cả các mục, nhưng ngay cả khi đó, bạn phải chắc chắn rằng không phải nhà phát triển tiếp theo thay đổi hoạt động đầu cuối (hoặc bạn quên khía cạnh tinh tế đó).

Hơn nữa, trong khi luồng bảo đảm duy trì thứ tự cuộc gặp gỡ cho một số hoạt động nhất định ngay cả đối với luồng song song, những đảm bảo này không áp dụng cho peek. Khi thu thập vào danh sách, danh sách kết quả sẽ có thứ tự đúng cho các luồng song song được đặt hàng, nhưng hành động peek có thể được gọi theo thứ tự tùy ý và đồng thời.

Vì vậy, điều hữu ích nhất mà bạn có thể làm với peek là để tìm hiểu xem một yếu tố dòng đã được xử lý đó là chính xác những gì tài liệu API cho biết:

Phương pháp này tồn tại chủ yếu để hỗ trợ gỡ lỗi, nơi bạn muốn nhìn thấy các yếu tố khi chảy qua một điểm nhất định trong một đường ống

+0

sẽ có bất kỳ vấn đề, tương lai hoặc hiện tại nào, trong trường hợp sử dụng của OP không? Mã của anh ấy có luôn làm những gì anh ta muốn không? – ZhongYu

+6

@ bayou.io: theo như tôi thấy, không có vấn đề gì trong * dạng chính xác này *. Nhưng khi tôi cố gắng giải thích, sử dụng nó theo cách này ngụ ý bạn phải nhớ khía cạnh này, ngay cả khi bạn quay lại đoạn mã sau một hoặc hai năm để kết hợp «yêu cầu tính năng 9876» vào mã… – Holger

+1

"hành động liếc có thể nhận được được gọi theo thứ tự tùy ý và đồng thời ". Tuyên bố này không vi phạm quy tắc của họ về cách hoạt động của tính năng peek, ví dụ: "như các yếu tố được tiêu thụ"? –

5

Có lẽ quy tắc chung là nếu bạn sử dụng peek bên ngoài kịch bản "gỡ lỗi", bạn chỉ nên làm như vậy nếu bạn chắc chắn về điều kiện lọc và điều kiện lọc trung gian là gì. Ví dụ:

có vẻ là trường hợp hợp lệ mà bạn muốn, trong một thao tác chuyển tất cả Foos thành Bars và yêu cầu tất cả chúng chào.

vẻ hiệu quả hơn và thanh lịch hơn cái gì đó như:

List<Bar> bars = list.stream().map(foo->foo.getBar()).collect(Collectors.toList()); 
bars.forEach(bar->bar.publish("HELLO")); 
return bars; 

và bạn không kết thúc lặp lại một bộ sưu tập hai lần.

1

Mặc dù tôi đồng ý với hầu hết các câu trả lời ở trên, tôi có một trường hợp trong đó sử dụng peek thực sự có vẻ là cách tốt nhất để đi.

Tương tự như trường hợp sử dụng của bạn, giả sử bạn chỉ muốn lọc trên tài khoản đang hoạt động và sau đó thực hiện đăng nhập trên các tài khoản này.

accounts.stream() 
    .filter(Account::isActive) 
    .peek(login) 
    .collect(Collectors.toList()); 

Peek là hữu ích để tránh những cuộc gọi không cần thiết trong khi không cần phải lặp bộ sưu tập hai lần:

accounts.stream() 
    .filter(Account::isActive) 
    .map(account -> { 
     account.login(); 
     return account; 
    }) 
    .collect(Collectors.toList()); 
Các vấn đề liên quan