Tôi nên trả lại Bộ sưu tập hoặc Luồng?


163

Giả sử tôi có một phương thức trả về chế độ xem chỉ đọc vào danh sách thành viên:

class Team {
    private List < Player > players = new ArrayList < > ();

    // ...

    public List < Player > getPlayers() {
        return Collections.unmodifiableList(players);
    }
}

Hơn nữa, giả sử rằng tất cả các khách hàng làm là lặp lại danh sách một lần, ngay lập tức. Có lẽ để đưa người chơi vào một JList hoặc một cái gì đó. Khách hàng không lưu trữ một tài liệu tham khảo vào danh sách để kiểm tra sau này!

Thay vào kịch bản chung này, tôi có nên trả lại một luồng thay thế không?

public Stream < Player > getPlayers() {
    return players.stream();
}

Hoặc là trả về một luồng không thành ngữ trong Java? Các luồng được thiết kế để luôn bị "chấm dứt" bên trong cùng một biểu thức mà chúng được tạo ra?


12
Chắc chắn không có gì sai với điều này như một thành ngữ. Rốt cuộc, players.stream()chỉ là một phương thức trả về một luồng cho người gọi. Câu hỏi thực sự là, bạn có thực sự muốn hạn chế người gọi đến một giao dịch đơn lẻ và cũng từ chối anh ta truy cập vào bộ sưu tập của bạn qua CollectionAPI không? Có lẽ người gọi chỉ muốn addAllnó đến một bộ sưu tập khác?
Marko Topolnik

2
Tất cả phụ thuộc vào. Bạn luôn có thể thực hiện bộ sưu tập.stream () cũng như Stream.collect (). Vì vậy, tùy thuộc vào bạn và người gọi sử dụng chức năng đó.
Raja Anbazhagan

Câu trả lời:


222

Câu trả lời là, như mọi khi, "nó phụ thuộc". Nó phụ thuộc vào mức độ lớn của bộ sưu tập được trả lại. Nó phụ thuộc vào việc kết quả có thay đổi theo thời gian hay không và mức độ quan trọng của kết quả trả về là như thế nào. Và nó phụ thuộc rất nhiều vào cách người dùng có khả năng sử dụng câu trả lời.

Đầu tiên, lưu ý rằng bạn luôn có thể nhận được Bộ sưu tập từ một luồng và ngược lại:

// If API returns Collection, convert with stream()
getFoo().stream()...

// If API returns Stream, use collect()
Collection<T> c = getFooStream().collect(toList());

Vì vậy, câu hỏi là, cái nào hữu ích hơn cho người gọi của bạn.

Nếu kết quả của bạn có thể là vô hạn, chỉ có một lựa chọn: Luồng.

Nếu kết quả của bạn có thể rất lớn, có lẽ bạn thích Stream, vì có thể không có bất kỳ giá trị nào trong việc hiện thực hóa tất cả cùng một lúc và làm như vậy có thể tạo ra áp lực lớn.

Nếu tất cả người gọi sẽ làm là lặp qua nó (tìm kiếm, lọc, tổng hợp), bạn nên chọn Stream, vì Stream đã tích hợp sẵn và không cần thiết phải thực hiện một bộ sưu tập (đặc biệt là nếu người dùng có thể không xử lý toàn bộ kết quả.) Đây là một trường hợp rất phổ biến.

Ngay cả khi bạn biết rằng người dùng sẽ lặp đi lặp lại nhiều lần hoặc thay vào đó, bạn vẫn có thể muốn trả về Luồng thay vào đó, vì thực tế đơn giản là bất kỳ Bộ sưu tập nào bạn chọn để đặt nó vào (ví dụ: ArrayList) có thể không phải là hình thức họ muốn, và sau đó người gọi phải sao chép nó. nếu bạn trả về một luồng, họ có thể làm collect(toCollection(factory))và lấy nó ở dạng chính xác mà họ muốn.

Các trường hợp "thích Stream" ở trên hầu hết xuất phát từ thực tế là Stream linh hoạt hơn; bạn có thể liên kết muộn với cách bạn sử dụng nó mà không phải chịu các chi phí và ràng buộc của việc cụ thể hóa nó thành Bộ sưu tập.

Trường hợp bạn phải trả lại Bộ sưu tập là khi có các yêu cầu về tính nhất quán cao và bạn phải tạo ra một ảnh chụp nhanh nhất quán của mục tiêu đang di chuyển. Sau đó, bạn sẽ muốn đặt các yếu tố vào một bộ sưu tập sẽ không thay đổi.

Vì vậy, tôi sẽ nói rằng hầu hết thời gian, Stream là câu trả lời đúng - nó linh hoạt hơn, nó không áp đặt chi phí vật chất thường không cần thiết và có thể dễ dàng chuyển thành Bộ sưu tập bạn chọn nếu cần. Nhưng đôi khi, bạn có thể phải trả lại Bộ sưu tập (giả sử, do yêu cầu về tính nhất quán cao) hoặc bạn có thể muốn trả lại Bộ sưu tập vì bạn biết người dùng sẽ sử dụng nó như thế nào và biết đây là điều thuận tiện nhất cho họ.


6
Giống như tôi đã nói, có một vài trường hợp nó sẽ không bay, chẳng hạn như những trường hợp khi bạn muốn trả lại ảnh chụp nhanh trong thời gian của mục tiêu đang di chuyển, đặc biệt là khi bạn có yêu cầu về tính nhất quán cao. Nhưng hầu hết thời gian, Stream dường như là sự lựa chọn chung hơn, trừ khi bạn biết điều gì đó cụ thể về cách nó sẽ được sử dụng.
Brian Goetz

8
@Marko Ngay cả khi bạn giới hạn câu hỏi của mình quá hẹp, tôi vẫn không đồng ý với kết luận của bạn. Có lẽ bạn đang cho rằng việc tạo ra một luồng nào đó đắt hơn nhiều so với việc gói bộ sưu tập bằng một trình bao bọc bất biến? (Và, ngay cả khi bạn không, chế độ xem luồng bạn nhận được trên trình bao bọc còn tệ hơn so với những gì bạn thoát khỏi bản gốc; vì UnmodifiableList không ghi đè trình phân tách (), bạn thực sự sẽ mất tất cả tính song song.) thiên vị quen thuộc; bạn đã biết Bộ sưu tập trong nhiều năm và điều đó có thể khiến bạn mất lòng tin vào người mới.
Brian Goetz

5
@MarkoTopolnik Chắc chắn. Mục tiêu của tôi là giải quyết câu hỏi thiết kế API chung, đang trở thành Câu hỏi thường gặp. Về chi phí, lưu ý rằng, nếu bạn chưa có bộ sưu tập cụ thể, bạn có thể trả lại hoặc gói (OP có, nhưng thường không có), thực hiện một bộ sưu tập trong phương thức getter không rẻ hơn trả lại luồng và cho phép người gọi thực hiện một (và dĩ nhiên là việc vật chất hóa sớm có thể tốn kém hơn nhiều, nếu người gọi không cần nó hoặc nếu bạn trả về ArrayList nhưng người gọi muốn Rodset.) Nhưng Stream là mới và mọi người thường cho rằng $$$ nhiều hơn nó là.
Brian Goetz

4
@MarkoTopolnik Mặc dù trong bộ nhớ là trường hợp sử dụng rất quan trọng, nhưng cũng có một số trường hợp khác có hỗ trợ song song tốt, chẳng hạn như các luồng được tạo không theo thứ tự (ví dụ: Stream.generate). Tuy nhiên, trong đó Streams không phù hợp là trường hợp sử dụng phản ứng, nơi dữ liệu đến với độ trễ ngẫu nhiên. Đối với điều đó, tôi sẽ đề nghị RxJava.
Brian Goetz

4
@MarkoTopolnik Tôi không nghĩ chúng tôi không đồng ý, ngoại trừ có lẽ bạn có thể thích chúng tôi tập trung nỗ lực của chúng tôi một chút khác nhau. (Chúng ta đã quen với điều này; không thể làm cho tất cả mọi người hài lòng.) Trung tâm thiết kế cho Luồng tập trung vào các cấu trúc dữ liệu trong bộ nhớ; trung tâm thiết kế cho RxJava tập trung vào các sự kiện được tạo ra từ bên ngoài. Cả hai đều là những thư viện tốt; cả hai đều không rất tốt khi bạn cố gắng áp dụng chúng cho các trường hợp ra khỏi trung tâm thiết kế của họ. Nhưng chỉ vì búa là một công cụ khủng khiếp cho mũi kim, điều đó không cho thấy có gì sai với búa.
Brian Goetz

63

Tôi có một vài điểm để thêm vào câu trả lời xuất sắc của Brian Goetz .

Nó khá phổ biến để trả về một luồng từ một cuộc gọi phương thức kiểu "getter". Xem trang sử dụng Luồng trong javadoc Java 8 và tìm "phương thức ... trả về Luồng" cho các gói khác java.util.Stream. Các phương thức này thường trên các lớp đại diện hoặc có thể chứa nhiều giá trị hoặc tập hợp của một cái gì đó. Trong các trường hợp như vậy, các API thường trả về các bộ sưu tập hoặc mảng của chúng. Vì tất cả các lý do mà Brian lưu ý trong câu trả lời của mình, thật linh hoạt khi thêm các phương thức trả lại luồng tại đây. Nhiều trong số các lớp này đã có các phương thức trả về bộ sưu tập hoặc mảng, bởi vì các lớp có trước API Streams. Nếu bạn đang thiết kế một API mới và thật hợp lý khi cung cấp các phương thức trả về Luồng, thì cũng không cần thiết phải thêm các phương thức trả về bộ sưu tập.

Brian đã đề cập đến chi phí "cụ thể hóa" các giá trị thành một bộ sưu tập. Để khuếch đại điểm này, thực tế có hai chi phí ở đây: chi phí lưu trữ giá trị trong bộ sưu tập (cấp phát bộ nhớ và sao chép) và chi phí tạo giá trị ở vị trí đầu tiên. Chi phí sau thường có thể được giảm hoặc tránh bằng cách tận dụng hành vi tìm kiếm sự lười biếng của Stream. Một ví dụ điển hình cho điều này là các API trong java.nio.file.Files:

static Stream<String>  lines(path)
static List<String>    readAllLines(path)

Không chỉ readAllLinesphải giữ toàn bộ nội dung tệp trong bộ nhớ để lưu nó vào danh sách kết quả, nó còn phải đọc tệp đến cuối trước khi trả về danh sách. Các linesphương pháp có thể trở lại gần như ngay lập tức sau khi nó đã thực hiện một số thiết lập, để lại đọc tập tin và đường phá vỡ cho đến khi sau khi nó là cần thiết - hoặc không gì cả. Đây là một lợi ích rất lớn, ví dụ, nếu người gọi chỉ quan tâm đến mười dòng đầu tiên:

try (Stream<String> lines = Files.lines(path)) {
    List<String> firstTen = lines.limit(10).collect(toList());
}

Tất nhiên không gian bộ nhớ đáng kể có thể được lưu nếu người gọi lọc luồng để chỉ trả về các dòng khớp với một mẫu, v.v.

Một thành ngữ dường như đang nổi lên là đặt tên cho các phương thức trả về luồng theo số nhiều của tên của những thứ mà nó đại diện hoặc chứa, không có gettiền tố. Ngoài ra, mặc dù stream()là tên hợp lý cho phương thức trả về luồng khi chỉ có một bộ giá trị có thể được trả về, đôi khi có các lớp có tập hợp của nhiều loại giá trị. Ví dụ: giả sử bạn có một số đối tượng chứa cả thuộc tính và thành phần. Bạn có thể cung cấp hai API trả về luồng:

Stream<Attribute>  attributes();
Stream<Element>    elements();

3
Điểm tuyệt vời. Bạn có thể nói rõ hơn về nơi bạn đang thấy thành ngữ đặt tên phát sinh không, và bao nhiêu lực kéo (hơi nước?) Nó đang nhặt? Tôi thích ý tưởng về một quy ước đặt tên cho thấy rõ ràng rằng bạn đang nhận được một luồng so với một bộ sưu tập - mặc dù tôi cũng thường mong đợi việc hoàn thành IDE trên "get" để cho tôi biết những gì tôi có thể nhận được.
Joshua Goldberg

1
Tôi cũng rất quan tâm đến việc đặt tên thành ngữ đó
bầu

5
@JoshuaGoldberg JDK dường như đã áp dụng thành ngữ đặt tên này, mặc dù không độc quyền. Hãy xem xét: CharSequence.chars () và .codePoints (), BufferedReader.lines () và Files.lines () tồn tại trong Java 8. Trong Java 9, những điều sau đây đã được thêm vào: Process.children (), NetworkInterface.addresses ( ), Scanner.tokens (), Matcher.results (), java.xml.catalog.Catalog.catalogs (). Các phương thức trả về luồng khác đã được thêm vào mà không sử dụng thành ngữ này - Scanner.find ALL () xuất hiện trong tâm trí - nhưng thành ngữ danh từ số nhiều dường như đã được sử dụng hợp lý trong JDK.
Stuart Marks

1

Các luồng được thiết kế để luôn bị "chấm dứt" bên trong cùng một biểu thức mà chúng được tạo ra?

Đó là cách chúng được sử dụng trong hầu hết các ví dụ.

Lưu ý: trả về Luồng không khác gì trả về Iterator (được thừa nhận với sức mạnh biểu cảm hơn nhiều)

IMHO giải pháp tốt nhất là gói gọn lý do tại sao bạn làm việc này và không trả lại bộ sưu tập.

ví dụ

public int playerCount();
public Player player(int n);

hoặc nếu bạn có ý định đếm chúng

public int countPlayersWho(Predicate<? super Player> test);

2
Vấn đề với câu trả lời này là nó sẽ yêu cầu tác giả dự đoán mọi hành động mà khách hàng muốn thực hiện, nó sẽ làm tăng đáng kể số lượng phương thức trên lớp.
dkatzel

@dkatzel Nó phụ thuộc vào việc người dùng cuối là tác giả hay người mà họ làm việc cùng. Nếu người dùng cuối không thể biết được, thì bạn cần một giải pháp tổng quát hơn. Bạn vẫn có thể muốn giới hạn quyền truy cập vào bộ sưu tập cơ bản.
Peter Lawrey

1

Nếu luồng là hữu hạn và có một hoạt động dự kiến ​​/ bình thường trên các đối tượng được trả lại sẽ ném ngoại lệ được kiểm tra, tôi luôn trả về Bộ sưu tập. Bởi vì nếu bạn định làm gì đó trên mỗi đối tượng có thể ném ngoại lệ kiểm tra, bạn sẽ ghét luồng. Một thiếu thực sự với các luồng tôi không có khả năng xử lý các ngoại lệ được kiểm tra một cách thanh lịch.

Bây giờ, có lẽ đó là một dấu hiệu cho thấy bạn không cần các ngoại lệ được kiểm tra, điều này là công bằng, nhưng đôi khi chúng không thể tránh khỏi.


1

Ngược lại với các bộ sưu tập, các luồng có các đặc điểm bổ sung . Một luồng được trả về bởi bất kỳ phương thức nào có thể là:

  • hữu hạn hay vô hạn
  • song song hoặc tuần tự (với một luồng chung được chia sẻ toàn cầu mặc định có thể tác động đến bất kỳ phần nào khác của ứng dụng)
  • đặt hàng hoặc không đặt hàng

Những khác biệt này cũng tồn tại trong các bộ sưu tập, nhưng chúng là một phần của hợp đồng rõ ràng:

  • Tất cả các Bộ sưu tập có kích thước, Iterator / Iterable có thể là vô hạn.
  • Bộ sưu tập được sắp xếp rõ ràng hoặc không theo thứ tự
  • Song song, may mắn không phải là thứ mà bộ sưu tập quan tâm ngoài sự an toàn của luồng.

Là người tiêu dùng của một luồng (từ trả về phương thức hoặc là tham số phương thức), đây là một tình huống nguy hiểm và khó hiểu. Để đảm bảo thuật toán của họ hoạt động chính xác, người tiêu dùng luồng cần đảm bảo thuật toán không đưa ra giả định sai về các đặc điểm của luồng. Và đó là một điều rất khó để làm. Trong thử nghiệm đơn vị, điều đó có nghĩa là bạn phải nhân tất cả các thử nghiệm của mình để được lặp lại với cùng một nội dung luồng, nhưng với các luồng là

  • (hữu hạn, có trật tự, tuần tự)
  • (hữu hạn, có trật tự, song song)
  • (hữu hạn, không theo thứ tự, tuần tự) ...

Viết các phương thức bảo vệ cho các luồng ném IllegalArgumentException nếu luồng đầu vào có một đặc điểm phá vỡ thuật toán của bạn là khó khăn, vì các thuộc tính bị ẩn.

Điều đó khiến Stream chỉ là một lựa chọn hợp lệ trong chữ ký phương thức khi không có vấn đề nào ở trên, điều này hiếm khi xảy ra.

An toàn hơn nhiều khi sử dụng các kiểu dữ liệu khác trong chữ ký phương thức với một hợp đồng rõ ràng (và không có xử lý nhóm luồng liên quan) khiến cho không thể vô tình xử lý dữ liệu với các giả định sai về thứ tự, kích thước hoặc độ song song (và sử dụng luồng).


2
Mối quan tâm của bạn về các luồng vô hạn là không có cơ sở; câu hỏi là "tôi nên trả lại một bộ sưu tập hay một luồng". Nếu Bộ sưu tập là một khả năng, kết quả là theo định nghĩa hữu hạn. Vì vậy, lo lắng rằng người gọi sẽ có nguy cơ lặp lại vô hạn, cho rằng bạn có thể đã trả lại một bộ sưu tập , là không có cơ sở. Phần còn lại của lời khuyên trong câu trả lời này chỉ là xấu. Tôi nghe có vẻ như bạn đã gặp phải một người sử dụng quá mức Stream và bạn đang quay vòng quá mức theo hướng khác. Có thể hiểu, nhưng lời khuyên tồi.
Brian Goetz

0

Tôi nghĩ rằng nó phụ thuộc vào kịch bản của bạn. Có thể, nếu bạn Teamthực hiện Iterable<Player>, nó là đủ.

for (Player player : team) {
    System.out.println(player);
}

hoặc theo kiểu chức năng:

team.forEach(System.out::println);

Nhưng nếu bạn muốn một api hoàn chỉnh và trôi chảy hơn, một luồng có thể là một giải pháp tốt.


Lưu ý rằng, trong mã mà OP đã đăng, số người chơi gần như vô dụng, ngoài ước tính ('1034 người chơi đang chơi, nhấp vào đây để bắt đầu!') Điều này là do bạn đang trả về một cái nhìn bất biến về bộ sưu tập có thể thay đổi , do đó, số lượng bạn nhận được bây giờ có thể không bằng số lượng ba micrô giây kể từ bây giờ. Vì vậy, trong khi trả lại Bộ sưu tập cung cấp cho bạn một cách "dễ dàng" để đếm số (và thực sự, stream.count()cũng khá dễ dàng), con số đó không thực sự rất có ý nghĩa đối với bất kỳ điều gì ngoài việc gỡ lỗi hoặc ước tính.
Brian Goetz

0

Trong khi một số người trả lời cao cấp hơn đã đưa ra lời khuyên chung tuyệt vời, tôi ngạc nhiên không ai nói rõ:

Nếu bạn đã có Collectiontrong tay "vật chất hóa" (nghĩa là nó đã được tạo trước cuộc gọi - như trường hợp trong ví dụ đã cho, trong đó là trường thành viên), không có điểm nào chuyển đổi nó thành a Stream. Người gọi có thể dễ dàng tự làm điều đó. Trong khi đó, nếu người gọi muốn sử dụng dữ liệu ở dạng ban đầu, bạn chuyển đổi nó thành một Streamcông việc dư thừa để thực hiện lại một bản sao của cấu trúc ban đầu.


-1

Có lẽ một nhà máy Stream sẽ là một lựa chọn tốt hơn. Chiến thắng lớn của việc chỉ trưng bày các bộ sưu tập qua Stream là nó đóng gói tốt hơn cấu trúc dữ liệu của mô hình miền của bạn. Không thể sử dụng các lớp miền của bạn để ảnh hưởng đến hoạt động bên trong của Danh sách hoặc Đặt đơn giản bằng cách hiển thị Luồng.

Nó cũng khuyến khích người dùng của lớp miền của bạn viết mã theo kiểu Java 8 hiện đại hơn. Bạn có thể tăng dần cấu trúc lại theo kiểu này bằng cách giữ các getters hiện có của bạn và thêm các getters trả về Stream mới. Theo thời gian, bạn có thể viết lại mã kế thừa của mình cho đến khi cuối cùng bạn đã xóa tất cả các getters trả về Danh sách hoặc Bộ. Kiểu tái cấu trúc này cảm thấy thực sự tốt khi bạn xóa hết mã kế thừa!


7
Có một lý do này được trích dẫn đầy đủ? Có nguồn không?
Xerus

-5

Tôi có thể có 2 phương thức, một để trả về một Collectionvà một để trả về bộ sưu tập là a Stream.

class Team
{
    private List<Player> players = new ArrayList<>();

// ...

    public List<Player> getPlayers()
    {
        return Collections.unmodifiableList(players);
    }

    public Stream<Player> getPlayerStream()
    {
        return players.stream();
    }

}

Điều này là tốt nhất của cả hai thế giới. Khách hàng có thể chọn nếu họ muốn Danh sách hoặc Luồng và họ không phải thực hiện thêm việc tạo đối tượng để tạo một bản sao bất biến của danh sách chỉ để nhận Luồng.

Điều này cũng chỉ thêm 1 phương thức nữa vào API của bạn để bạn không có quá nhiều phương thức


1
Bởi vì anh ấy muốn lựa chọn giữa hai lựa chọn này và hỏi những ưu và nhược điểm của từng lựa chọn. Hơn nữa, nó cung cấp cho mọi người hiểu rõ hơn về các khái niệm này.
Libert Piou Piou

Xin đừng làm vậy. Hãy tưởng tượng các API!
François Gautier
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.