Câu hỏi Trong luồng Java là peek thực sự chỉ để gỡ lỗi?


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à peek() chức năng. 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.

Điều gì sẽ xảy ra nếu tôi có một Luồng mà mỗi Tài khoản có một 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 lại tệ như vậy?

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

Bây giờ theo như tôi có thể nói điều này không chính xác những gì nó dự định làm. Nó;

  • Lấy danh sách tài khoản
  • Cố gắng đăng nhập vào từng tài khoản
  • Lọc ra bất kỳ tài khoản nào chưa đăng nhập
  • Thu thập các tài khoản đã đăng nhập vào 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;

.filter(account -> {
        account.login();
        return account.loggedIn();
    })

76
2017-11-10 17:12


gốc


Bất cứ lúc nào tôi thấy mình cần một lambda đa dòng, tôi di chuyển các dòng đến một phương thức riêng và chuyển tham chiếu phương thức thay cho lambda. - VGR
Mục đích là gì - bạn đang cố đăng nhập tất cả các tài khoản trong và lọc chúng dựa trên nếu chúng được đăng nhập (điều này có thể là sự thật)? Hoặc, bạn có muốn đăng nhập họ không, sau đó lọc chúng dựa trên việc họ có đăng nhập hay không? Tôi hỏi điều này theo thứ tự này vì forEach có thể là hoạt động bạn muốn thay vì peek. Chỉ vì nó trong API không có nghĩa là nó không mở để lạm dụng (như Optional.of). - Makoto
Bộ lọc dựa vào nếu họ đã thực sự đăng nhập. Ví dụ: nếu tên người dùng sai thì nó sẽ không đăng nhập. Vì vậy, tôi muốn kiểm tra xem nó có được đăng nhập hay không. Nếu không thì nó sẽ bị ném bởi Bộ lọc. - Adam.J
Cũng lưu ý rằng mã của bạn chỉ có thể là .peek(Account::login) và .filter(Account::loggedIn); không có lý do gì để viết một Consumer and Predicate mà chỉ gọi một phương thức khác như thế. - Joshua Taylor
Người tiêu dùng hữu ích luôn có tác dụng phụ, những người không được khuyến khích. Điều này thực sự được đề cập trong cùng một phần: “Một số lượng nhỏ các hoạt động của luồng, chẳng hạn như forEach() và peek(), chỉ có thể hoạt động thông qua các tác dụng phụ; chúng nên được sử dụng cẩn thận.”. Nhận xét của tôi là nhiều hơn để nhắc nhở rằng peek hoạt động (được thiết kế cho mục đích gỡ lỗi) không nên được thay thế bằng cách làm điều tương tự bên trong một hoạt động khác như map() hoặc là filter(). - Didier L


Các câu trả lời:


Các takeaway quan trọng từ này:

Không sử dụng API theo cách không mong muốn, ngay cả khi nó hoàn thành mục tiêu trước mắt của bạn. Cách tiếp cận đó có thể phá vỡ 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ỡ này ra nhiều hoạt động, vì họ là hoạt động riêng biệt. Ở đó  tác hại trong việc sử dụng API theo cách không rõ ràng và không mong muốn, có thể có các 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ề hoạt động này sẽ làm rõ với người bảo trì rằng có một dự định tác dụng phụ trên mỗi phần tử của accountsvà rằng bạn đang thực hiện một số thao tác có thể thay đổi nó.

Nó cũng thông thường hơn theo nghĩa 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 hoạt động của thiết bị đầ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 trái với việc đặt câu hỏi về peek sẽ hành xử 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());

49
2017-11-10 17:55



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
@ Holger: Điểm tuyệt vời. Tôi đã kết hợp điều đó vào câu trả lời. - Makoto
@ Adam.J: Đúng vậy, câu trả lời của tôi tập trung nhiều hơn vào câu hỏi chung có trong tiêu đề của bạn, tức là phương pháp này thực sự chỉ để gỡ lỗi, bằng cách giải thích các khía cạnh của phương pháp đó. Câu trả lời này được hợp nhất hơn trong trường hợp sử dụng thực tế của bạn và cách thực hiện thay thế. Vì vậy, bạn có thể nói, cùng nhau họ cung cấp hình ảnh đầy đủ. Thứ nhất, lý do tại sao đây không phải là mục đích sử dụng, thứ hai là kết luận, không dính vào việc sử dụng không chủ định và phải làm gì thay vào đó. Sau này sẽ có sử dụng thực tế hơn cho bạn. - Holger
@ JoshuaTaylor: Nếu nó không phải là một bộ sưu tập hoặc một mảng, bạn đang làm một cái gì đó với I / O và bạn đang cố gắng để hành động trên nó trong một cách mà có lẽ không có ý định. Các takeaway chính từ này (bên cạnh một trong những đậm) sẽ được thực hiện các hoạt động chuyển đổi trước bạn lọc. Cần biến đổi và bộ lọc trong cùng một bước có thể được xem như là một trường hợp cạnh hoặc một mùi mã, và tôi sẽ nghiêng về phía sau. - Makoto
Đó là những gì tôi đang nhắm tới. Nếu login() trả về một boolean, bạn có thể sử dụng nó như một biến vị ngữ là giải pháp sạch nhất. Nó vẫn có tác dụng phụ, nhưng điều đó không sao, miễn là nó không can thiệp, tức là login process` của một Account không ảnh hưởng đến quá trình đăng nhập` của người khác Account. - Holger


Điều quan trọng bạn phải hiểu, là các luồng được thúc đẩy bởi hoạt động đầu cuối. 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ì thế 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 yếu tố phù hợp.

count() có thể không xử lý bất kỳ phần tử nào khi nó 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 nó sẽ có trong Java 9, có thể có bất ngờ khi bạn chuyển sang Java 9 và có mã dựa vào 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 một 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 cản dự đoán như vậy.

peek cho phép "thực hiện tác vụ được cung cấp trên mỗi phần tử khi các phần tử được tiêu thụ từ luồng kết quả”, Nó không bắt buộc phải xử lý các phần tử nhưng sẽ thực hiện hành động phụ thuộc vào những gì mà hoạt động của thiết bị đầu cuối cần. Đ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 các luồng đảm bảo duy trì thứ tự cuộc gặp gỡ để kết hợp các hoạt động nhất định ngay cả đối với các luồng song song, các đả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 peek hành động có thể được gọi theo thứ tự tùy ý và đồng thời.

Điều hữu ích nhất bạn có thể làm với peek là để tìm hiểu xem liệu yếu tố luồng có được xử lý hay không chính xác là 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 xem các phần tử khi chúng chạy qua một điểm nhất định trong một đường ống


68
2017-11-10 17:50



sẽ có bất kỳ vấn đề, tương lai hoặc hiện tại, 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
@ bayou.io: theo như tôi thấy, không có vấn đề gì trong hình thức chính xác này. Nhưng như 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ã một hoặc hai năm sau để kết hợp «yêu cầu tính năng 9876» vào mã… - Holger
"hành động peek có thể đượ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ụ"? - Jose Martinez
@Jose Martinez: Nó nói "là yếu tố được tiêu thụ từ luồng kết quả”, Không phải là hành động đầu cuối mà là quá trình xử lý, mặc dù ngay cả hành động cuối cùng cũng có thể tiêu hao các phần tử miễn là kết quả cuối cùng nhất quán. Nhưng tôi cũng nghĩ rằng, cụm từ của ghi chú API, “xem các yếu tố khi chúng chảy qua một điểm nhất định trong một đường ống”Một công việc tốt hơn khi mô tả nó. - Holger


Có lẽ nguyên 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ụ:

return list.stream().map(foo->foo.getBar())
                    .peek(bar->bar.publish("HELLO"))
                    .collect(Collectors.toList());

có vẻ là một trường hợp hợp lệ mà bạn muốn, trong một hoạt động để chuyển đổi tất cả Foos thành Bars và nói với họ tất cả xin chào.

Có vẻ hiệu quả hơn và thanh lịch hơn một 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 việc lặp lại một bộ sưu tập hai lần.


9
2017-11-11 12:52





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ẻ như là cách sạch 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 các 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 hữu ích để tránh cuộc gọi dư thừa trong khi không phải lặp lại bộ sưu tập hai lần:

accounts.stream()
    .filter(Account::isActive)
    .map(account -> {
        account.login();
        return account;
    })
    .collect(Collectors.toList());

2
2017-10-26 14:45



Tất cả những gì bạn phải làm là lấy đúng phương thức đăng nhập đó. Tôi thực sự không thấy cách nhìn trộm là cách sạch sẽ nhất. Người nào đó đang đọc mã của bạn nên biết rằng bạn thực sự lạm dụng API. Mã tốt và sạch sẽ không ép buộc người đọc đưa ra giả định về mã. - kaba713


Tôi sẽ nói rằng peek cung cấp khả năng phân cấp mã có thể thay đổi luồng đối tượng hoặc sửa đổi trạng thái toàn cục (dựa trên chúng), thay vì nhồi nhét mọi thứ vào hàm đơn giản hoặc sáng tác được chuyển đến một phương thức đầu cuối.

Bây giờ câu hỏi có thể là: chúng ta có nên thay đổi các đối tượng luồng hay thay đổi trạng thái toàn cục từ bên trong các hàm trong lập trình hàm kiểu java?

Nếu câu trả lời cho bất kỳ câu hỏi nào ở trên là có (hoặc: trong một số trường hợp có) thì peek() Là chắc chắn không chỉ cho mục đích gỡ lỗi, vì cùng một lý do forEach() không chỉ dành cho mục đích gỡ lỗi.

Đối với tôi khi lựa chọn giữa forEach() và peek(), đang chọn những điều sau đây: Tôi có muốn các đoạn mã làm thay đổi luồng đối tượng được gắn vào một composable hay tôi muốn chúng đính kèm trực tiếp vào luồng không?

Tôi nghĩ peek() sẽ ghép nối tốt hơn với các phương thức java9. ví dụ. takeWhile() có thể cần phải quyết định thời điểm dừng lặp lại dựa trên một đối tượng đã bị đột biến, vì vậy hãy xoay nó với forEach() sẽ không có tác dụng tương tự.

P.S. Tôi chưa tham chiếu map() ở bất kỳ đâu vì trong trường hợp chúng ta muốn thay đổi các đối tượng (hoặc trạng thái toàn cầu), thay vì tạo các đối tượng mới, nó hoạt động chính xác như peek().


1
2018-06-21 14:14