Trong các luồng Java có thực sự chỉ dùng để gỡ lỗi?


136

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 Luồng của bạn.

Điều gì xảy ra nếu tôi có Luồng trong đó 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à loginIn ().

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 tệ?

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 thực hiện chính xác những gì nó dự định làm. Nó;

  • Có một danh sách các tài khoản
  • Thử đăng nhập vào từng tài khoản
  • Lọc bất kỳ tài khoản nào không đăng nhập
  • Thu thập các tài khoản đã đăng nhập vào một danh sách mới

Nhược điểm của việc làm một cái gì đó như thế này là gì? Bất kỳ lý do tôi không nên tiến hành? 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 điều này đã sử dụng phương thức .filter () như sau;

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

38
Bất cứ khi nào tôi thấy mình cần một lambda nhiều dòng, tôi di chuyển các dòng sang một phương thức riêng tư và chuyển tham chiếu phương thức thay vì lambda.
VGR

1
Ý định là gì - bạn đang cố gắng đăng nhập tất cả các tài khoản lọc chúng dựa trên việc chúng đã đăng nhập (điều này có thể đúng sự thật)? Hoặc, bạn có muốn đăng nhập chúng, sau đó lọc chúng dựa trên việc chúng đã đăng nhập hay chưa? Tôi đang hỏi điều này theo thứ tự này bởi vì forEachcó thể là hoạt động bạn muốn trái ngược 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

8
Cũng lưu ý rằng mã của bạn chỉ có thể .peek(Account::login).filter(Account::loggedIn); không có lý do gì để viết Người tiêu dùng và Dự đoán mà chỉ gọi một phương thức khác như thế.
Joshua Taylor

2
Cũng lưu ý rằng API luồng rõ ràng không khuyến khích các tác dụng phụ trong các tham số hành vi .
Didier L

6
Người tiêu dùng hữu ích luôn có tác dụng phụ, tất nhiên những điều đó 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 luồng, chẳng hạn như forEach()peek(), chỉ có thể hoạt động thông qua các tác dụng phụ; những thứ này nên được sử dụng cẩn thận. Mùi. Nhận xét của tôi là nhiều hơn để nhắc nhở rằng peekhoạt động (được thiết kế cho mục đích gỡ lỗi) không nên được thay thế bằng cách thực hiện điều tương tự bên trong một hoạt động khác như map()hoặc filter().
Didier L

Câu trả lời:


77

Điểm nổi bật của việc này:

Đừng sử dụng API theo cách không có chủ ý, 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ể bị phá vỡ trong tương lai, và nó cũng không rõ ràng đối với những người duy trì trong tương lai.


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

Sử dụng forEachcho hoạt động này sẽ làm cho người bảo trì thấy rõ rằng có một tác dụng phụ dự định đối với từng yếu tố củaaccounts và rằng bạn đang thực hiện một số thao tác có thể làm thay đổi nó.

Nó cũng thông thường hơn theo nghĩa peeklà 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 forEachthực sự là một hoạt động đầu cuối. Bằng cách này, bạn có thể đưa ra những lập luận mạnh mẽ xung quanh hành vi và dòng mã của mình thay vì đặt câu hỏi về việc liệu peekcó hành xử giống như forEachtrong bối cảnh này hay không.

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

3
Nếu bạn thực hiện đăng nhập trong bước tiền xử lý, bạn hoàn toàn không cần một luồng. Bạn có thể thực hiện forEachngay 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 nó vào câu trả lời.
Makoto

2
@ 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 nhiều 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 ngoài ý muốn và phải làm gì để thay thế. Sau này sẽ có sử dụng thực tế hơn cho bạn.
Holger

2
Tất nhiên, mọi thứ sẽ dễ dàng hơn nhiều nếu login()phương thức trả về một booleangiá trị biểu thị trạng thái thành công.
Holger

3
Đó là những gì tôi đã nhắm đến. Nếu login()trả về a boolean, bạn có thể sử dụng nó như một vị ngữ là giải pháp sạch nhất. Nó vẫn có tác dụng phụ, nhưng không sao, miễn là nó không can thiệp, tức là quá trình login'của người Accountnày không ảnh hưởng đến quá trình đăng nhập' của người khác Account.
Holger

111

Điều quan trọng bạn phải hiểu là các luồng được điều khiển bởi hoạt động của thiết bị đầu cuối . Hoạt động của thiết bị đầu cuối xác định liệu tất cả các yếu tố phải được xử lý hay bất kỳ yếu tố nào. Vì vậy, collectmột hoạt động xử lý từng mục, trong khi findAnycó thể dừng xử lý các mục một khi nó gặp một yếu tố phù hợp.

count()có thể không xử lý bất kỳ yếu 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 sẽ có trong Java 9, nên có thể có những 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 vào 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ớilimit trong khi không có giới hạn cơ bản nào ngăn chặn dự đoán đó.

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

Vì vậy, điều hữu ích nhất bạn có thể làm peeklà tìm hiểu xem phần tử luồng đã được xử lý chưa, đó chính xác là những gì tài liệu API nói:

Phương thức 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 đường ống


sẽ có bất kỳ vấn đề, tương lai hay hiện tại, trong trường hợp sử dụng của OP? Liệu mã của anh ấy luôn làm những gì anh ấy muốn?
Trung Dương

9
@ bayou.io: theo như tôi thấy, không có vấn đề gì trong mẫu 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 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ã Mã
Holger

1
"hành động nhìn trộm có thể được gọi theo thứ tự tùy ý và đồng thời". Không phải tuyên bố này đi ngược lại quy tắc của họ về cách thức hoạt động của peek, ví dụ "như các yếu tố được tiêu thụ"?
Jose Martinez

5
@Jose Martinez: Nó nói rằng các phần tử được tiêu thụ từ luồng kết quả , không phải là hành động đầu cuối mà là xử lý, mặc dù hành động đầu cuối có thể tiêu thụ các phần tử không theo thứ tự miễn là kết quả cuối cùng phù hợp. Nhưng tôi cũng nghĩ rằng, cụm từ của ghi chú API, đã thấy các yếu tố khi chúng đi qua một điểm nhất định trong một đường ống, làm một công việc tốt hơn trong việc mô tả nó.
Holger

23

Có lẽ một nguyên tắc nhỏ là nếu bạn sử dụng peek 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ề các điều kiện lọc kết thúc và lọc trung gian là gì. Ví dụ:

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

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

Có vẻ hiệu quả và thanh lịch hơn những thứ 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.


4

Tôi có thể nói rằng nó peekcung cấp khả năng phân cấp mã có thể làm thay đổi các đối tượng luồng hoặc sửa đổi trạng thái toàn cầu (dựa trên chúng), thay vì nhồi mọi thứ vào một hàm đơn giản hoặc được tạo thành 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 hoặc thay đổi trạng thái toàn cầu từ bên trong các hàm trong lập trình java kiểu chức năng không?

Nếu câu trả lời cho bất kỳ bên trên 2 câu hỏi là có (hoặc: trong một số trường hợp có) sau đó peek()chắc chắn không phải chỉ dành cho mục đích gỡ lỗi , vì lý do tương tự mà 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()peek() , là chọn các tùy chọn sau: Tôi có muốn các đoạn mã làm thay đổi các đối tượng luồng được gắn vào một bộ tổng hợp hay tôi muốn chúng gắn trực tiếp vào luồng?

Tôi nghĩ rằng peek()sẽ kết hợp 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 khi nào dừng lặp lại dựa trên một đối tượng đã bị đột biến, do đó, việc so sánh nó với nó forEach()sẽ không có tác dụng tương tự.

PS Tôi chưa tham chiếu ở map()bất cứ đâu vì trong trường hợp chúng tôi 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ư thế peek().


3

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 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 vào các tài khoản này.

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

Peek rất 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());

3
Tất cả bạn phải làm là để có được phương thức đăng nhập đó ngay. Tôi thực sự không thấy cách nhìn trộm là cách sạch nhất để đi. Làm thế nào một người đang đọc mã của bạn biết rằng bạn thực sự sử dụng sai API. Mã tốt và sạch không buộc người đọc phải đưa ra các giả định về mã.
kaba713

1

Các giải pháp chức năng là làm cho đối tượng tài khoản bất biến. Vì vậy, account.login () phải trả về một đối tượng tài khoản mới. Điều này có nghĩa là hoạt động bản đồ có thể được sử dụng để đăng nhập thay vì nhìn trộm.

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.