Cách thêm các phần tử của luồng Java8 vào Danh sách hiện có


Câu trả lời:


197

LƯU Ý: câu trả lời của nosid cho thấy cách thêm vào bộ sưu tập hiện có bằng cách sử dụng forEachOrdered(). Đây là một kỹ thuật hữu ích và hiệu quả để đột biến các bộ sưu tập hiện có. Câu trả lời của tôi giải quyết lý do tại sao bạn không nên sử dụng Collectorđể thay đổi bộ sưu tập hiện có.

Câu trả lời ngắn gọn là không , ít nhất, không nói chung, bạn không nên sử dụng một Collectorđể sửa đổi một bộ sưu tập hiện có.

Lý do là các bộ sưu tập được thiết kế để hỗ trợ song song, thậm chí trên các bộ sưu tập không an toàn cho chuỗi. Cách họ làm điều này là để mỗi luồng hoạt động độc lập trên bộ sưu tập kết quả trung gian của riêng nó. Cách mỗi luồng nhận được bộ sưu tập của riêng nó là gọi Collector.supplier()cái được yêu cầu để trả về bộ sưu tập mới mỗi lần.

Các bộ sưu tập kết quả trung gian này sau đó được hợp nhất, một lần nữa theo kiểu giới hạn luồng, cho đến khi có một bộ sưu tập kết quả duy nhất. Đây là kết quả cuối cùng của collect()hoạt động.

Một vài câu trả lời từ Balderassylias đã đề xuất sử dụng Collectors.toCollection()và sau đó chuyển qua một nhà cung cấp trả về một danh sách hiện có thay vì một danh sách mới. Điều này vi phạm yêu cầu đối với nhà cung cấp, đó là trả lại một bộ sưu tập mới, trống mỗi lần.

Điều này sẽ làm việc cho các trường hợp đơn giản, như các ví dụ trong câu trả lời của họ chứng minh. Tuy nhiên, nó sẽ thất bại, đặc biệt nếu luồng được chạy song song. (Một phiên bản tương lai của thư viện có thể thay đổi theo một cách không lường trước được sẽ khiến nó bị lỗi, ngay cả trong trường hợp tuần tự.)

Hãy lấy một ví dụ đơn giản:

List<String> destList = new ArrayList<>(Arrays.asList("foo"));
List<String> newList = Arrays.asList("0", "1", "2", "3", "4", "5");
newList.parallelStream()
       .collect(Collectors.toCollection(() -> destList));
System.out.println(destList);

Khi tôi chạy chương trình này, tôi thường nhận được một ArrayIndexOutOfBoundsException. Điều này là do nhiều luồng đang hoạt động ArrayList, một cấu trúc dữ liệu không an toàn của luồng. OK, hãy làm cho nó được đồng bộ hóa:

List<String> destList =
    Collections.synchronizedList(new ArrayList<>(Arrays.asList("foo")));

Điều này sẽ không còn thất bại với một ngoại lệ. Nhưng thay vì kết quả mong đợi:

[foo, 0, 1, 2, 3]

nó cho kết quả kỳ lạ như thế này:

[foo, 2, 3, foo, 2, 3, 1, 0, foo, 2, 3, foo, 2, 3, 1, 0, foo, 2, 3, foo, 2, 3, 1, 0, foo, 2, 3, foo, 2, 3, 1, 0]

Đây là kết quả của các hoạt động tích lũy / hợp nhất giới hạn luồng mà tôi đã mô tả ở trên. Với một luồng song song, mỗi luồng gọi nhà cung cấp để có bộ sưu tập riêng cho tích lũy trung gian. Nếu bạn vượt qua một nhà cung cấp trả về cùng một bộ sưu tập, mỗi luồng sẽ thêm kết quả của nó vào bộ sưu tập đó. Vì không có thứ tự giữa các luồng, kết quả sẽ được nối vào một số thứ tự tùy ý.

Sau đó, khi các bộ sưu tập trung gian này được hợp nhất, điều này về cơ bản hợp nhất danh sách với chính nó. Danh sách được hợp nhất bằng cách sử dụng List.addAll(), cho biết kết quả không được xác định nếu bộ sưu tập nguồn được sửa đổi trong quá trình hoạt động. Trong trường hợp này, ArrayList.addAll()một thao tác sao chép mảng, do đó, nó kết thúc việc sao chép chính nó, đó là loại mà người ta mong đợi, tôi đoán vậy. (Lưu ý rằng việc triển khai Danh sách khác có thể có hành vi hoàn toàn khác.) Dù sao, điều này giải thích các kết quả kỳ lạ và các yếu tố trùng lặp ở đích.

Bạn có thể nói: "Tôi sẽ đảm bảo chạy luồng của tôi một cách tuần tự" và tiếp tục và viết mã như thế này

stream.collect(Collectors.toCollection(() -> existingList))

dù sao. Tôi khuyên bạn không nên làm điều này. Nếu bạn kiểm soát luồng, chắc chắn, bạn có thể đảm bảo rằng nó sẽ không chạy song song. Tôi hy vọng rằng một phong cách lập trình sẽ xuất hiện nơi các luồng được truyền đi thay vì các bộ sưu tập. Nếu ai đó trao cho bạn một luồng và bạn sử dụng mã này, nó sẽ thất bại nếu luồng xảy ra song song. Tồi tệ hơn, ai đó có thể trao cho bạn một luồng tuần tự và mã này sẽ hoạt động tốt trong một thời gian, vượt qua tất cả các thử nghiệm, v.v. Sau đó, một số lượng thời gian tùy ý sau đó, mã ở nơi khác trong hệ thống có thể thay đổi để sử dụng các luồng song song sẽ gây ra mã của bạn để phá vỡ.

OK, sau đó chỉ cần đảm bảo nhớ gọi sequential()trên bất kỳ luồng nào trước khi bạn sử dụng mã này:

stream.sequential().collect(Collectors.toCollection(() -> existingList))

Tất nhiên, bạn sẽ nhớ làm điều này mỗi lần, phải không? :-) Hãy nói rằng bạn làm. Sau đó, nhóm thực hiện sẽ tự hỏi tại sao tất cả các triển khai song song được làm cẩn thận của họ không cung cấp bất kỳ sự tăng tốc nào. Và một lần nữa, họ sẽ theo dõi mã của bạn , điều này buộc toàn bộ luồng phải chạy tuần tự.

Đừng làm điều đó.


Giải thích tuyệt vời! - cảm ơn vì đã làm rõ điều này. Tôi sẽ chỉnh sửa câu trả lời của mình để khuyên bạn đừng bao giờ làm điều này với các luồng song song có thể.
Balder

3
Nếu câu hỏi là, nếu có một phần tử để thêm các phần tử của luồng vào danh sách hiện có, thì câu trả lời ngắn gọn là . Xem câu trả lời của tôi. Tuy nhiên, tôi đồng ý với bạn rằng việc sử dụng Collector.toCollection () kết hợp với danh sách hiện có là sai.
nosid

Thật. Tôi đoán phần còn lại của chúng tôi đều nghĩ về các nhà sưu tập.
Stuart Marks

Câu trả lời chính xác! Tôi rất muốn sử dụng giải pháp tuần tự ngay cả khi bạn khuyên rõ ràng chống lại vì như đã nêu nó phải hoạt động tốt. Nhưng thực tế là javadoc yêu cầu đối số nhà cung cấp của toCollectionphương thức phải trả về một bộ sưu tập mới và trống mỗi lần thuyết phục tôi không làm. Tôi thực sự muốn phá vỡ hợp đồng javadoc của các lớp Java cốt lõi.
phóng to

1
@AlexCurvers Nếu bạn muốn luồng có tác dụng phụ, bạn gần như chắc chắn muốn sử dụng forEachOrdered. Các tác dụng phụ bao gồm thêm các yếu tố vào một bộ sưu tập hiện có, bất kể nó đã có các yếu tố hay chưa. Nếu bạn muốn các yếu tố của một dòng đặt vào một mới thu thập, sử dụng collect(Collectors.toList())hoặc toSet()hoặc toCollection().
Stuart Marks

169

Theo như tôi có thể thấy, tất cả các câu trả lời khác cho đến nay đã sử dụng một trình thu thập để thêm các phần tử vào luồng hiện có. Tuy nhiên, có một giải pháp ngắn hơn và nó hoạt động cho cả hai luồng liên tiếp và song song. Bạn chỉ có thể sử dụng phương thức forEachOrdered kết hợp với tham chiếu phương thức.

List<String> source = ...;
List<Integer> target = ...;

source.stream()
      .map(String::length)
      .forEachOrdered(target::add);

Hạn chế duy nhất là, nguồnđích đó là các danh sách khác nhau, vì bạn không được phép thay đổi nguồn của luồng miễn là nó được xử lý.

Lưu ý rằng giải pháp này hoạt động cho cả hai luồng liên tiếp và song song. Tuy nhiên, nó không được hưởng lợi từ sự tương tranh. Tham chiếu phương thức được truyền cho forEachOrdered sẽ luôn được thực hiện tuần tự.


6
+1 Thật buồn cười khi nhiều người cho rằng không có khả năng khi có một. Btw. Tôi đưa vào forEach(existing::add)như một khả năng trong một câu trả lời hai tháng trước . Lẽ ra tôi nên nói thêm forEachOrderedcũng ...
Holger

5
Có bất kỳ lý do bạn sử dụng forEachOrderedthay vì forEach?
viên

6
@membersound: forEachOrderedhoạt động cho cả hai luồng liên tiếpsong song . Ngược lại, forEachcó thể thực thi đồng thời đối tượng hàm đã truyền cho các luồng song song. Trong trường hợp này, đối tượng hàm phải được đồng bộ hóa đúng cách, ví dụ bằng cách sử dụng a Vector<Integer>.
nosid

@BrianGoetz: Tôi phải thừa nhận rằng tài liệu của Stream.forEachOrdered là một chút không chính xác. Tuy nhiên, tôi không thể thấy bất kỳ giải thích hợp lý nào về thông số kỹ thuật này , trong đó không có mối quan hệ nào xảy ra trước khi có bất kỳ hai cuộc gọi nào target::add. Bất kể chủ đề nào mà phương thức được gọi, không có cuộc đua dữ liệu . Tôi đã mong đợi bạn biết điều đó.
nosid

Đây là câu trả lời hữu ích nhất, theo như tôi nghĩ. Nó thực sự cho thấy một cách thực tế để chèn các mục vào danh sách hiện có từ một luồng, đó là những gì câu hỏi yêu cầu (mặc dù từ "thu thập" sai lệch)
Wheezil

12

Câu trả lời ngắn gọn là không (hoặc nên là không). EDIT: yeah, điều đó là có thể (xem câu trả lời của assylias bên dưới), nhưng hãy tiếp tục đọc. EDIT2: nhưng hãy xem câu trả lời của Stuart Marks để biết thêm một lý do tại sao bạn vẫn không nên làm điều đó!

Câu trả lời dài hơn:

Mục đích của các cấu trúc này trong Java 8 là giới thiệu một số khái niệm về Lập trình hàm cho ngôn ngữ; trong Lập trình chức năng, các cấu trúc dữ liệu thường không được sửa đổi, thay vào đó, các cấu trúc mới được tạo ra từ các cấu trúc cũ bằng các phép biến đổi như bản đồ, bộ lọc, gấp / giảm và nhiều thứ khác.

Nếu bạn phải sửa đổi danh sách cũ, chỉ cần thu thập các mục được ánh xạ vào một danh sách mới:

final List<Integer> newList = list.stream()
                                  .filter(n -> n % 2 == 0)
                                  .collect(Collectors.toList());

và sau đó làm list.addAll(newList)- một lần nữa: nếu bạn thực sự phải.

(hoặc xây dựng một danh sách mới nối liền danh sách cũ và danh sách mới và gán lại cho listbiến số này, điều này hơi nhiều một chút theo tinh thần của FP so với addAll)

Đối với API: mặc dù API cho phép nó (một lần nữa, hãy xem câu trả lời của assylias), bạn nên cố gắng tránh làm điều đó bất kể, ít nhất là nói chung. Tốt nhất là không nên chiến đấu với mô hình (FP) và cố gắng học nó hơn là chiến đấu với nó (mặc dù Java thường không phải là ngôn ngữ của FP) và chỉ dùng đến các chiến thuật "bẩn hơn" nếu thực sự cần thiết.

Câu trả lời thực sự dài: (nghĩa là nếu bạn bao gồm nỗ lực thực sự tìm và đọc phần giới thiệu / sách của FP theo đề xuất)

Để tìm hiểu lý do tại sao sửa đổi danh sách hiện tại nói chung là một ý tưởng tồi và dẫn đến mã ít bảo trì hơn trừ khi bạn sửa đổi một biến cục bộ và thuật toán của bạn ngắn và / hoặc tầm thường, nằm ngoài phạm vi của câu hỏi về khả năng duy trì mã Giới thiệu tốt về lập trình chức năng (có hàng trăm) và bắt đầu đọc. Một lời giải thích "xem trước" sẽ giống như: nó nghe có vẻ toán học hơn và dễ dàng hơn để sửa đổi dữ liệu (trong hầu hết các phần của chương trình của bạn) và dẫn đến mức độ cao hơn và ít kỹ thuật hơn (cũng như thân thiện với con người hơn, một khi bộ não của bạn chuyển từ các tư duy mệnh lệnh kiểu cũ) của logic chương trình.


@assylias: về mặt logic, điều đó không sai vì có hoặc một phần; Dù sao, thêm một ghi chú.
Erik Kaplun

1
Câu trả lời ngắn gọn là đúng. Các lớp lót được đề xuất sẽ thành công trong các trường hợp đơn giản nhưng thất bại trong trường hợp chung.
Stuart Marks

Câu trả lời dài hơn hầu hết là đúng, nhưng thiết kế của API chủ yếu là về sự song song và ít về lập trình chức năng. Mặc dù tất nhiên có nhiều điều về FP có thể tuân theo song song, vì vậy hai khái niệm này được liên kết tốt.
Stuart Marks

@StuartMarks: Thú vị: trong trường hợp nào thì giải pháp được cung cấp trong câu trả lời của assylias bị phá vỡ? (và những điểm tốt về sự song song. Tôi đoán tôi đã quá háo hức ủng hộ FP)
Erik Kaplun

@Erik ALLik Tôi đã thêm một câu trả lời về vấn đề này.
Stuart Marks

11

Erik Allik đã đưa ra những lý do rất chính đáng, tại sao rất có thể bạn sẽ không muốn thu thập các yếu tố của một luồng vào Danh sách hiện có.

Dù sao, bạn có thể sử dụng một lớp lót sau đây, nếu bạn thực sự cần chức năng này.

Nhưng như Stuart Marks giải thích trong câu trả lời của mình, bạn không bao giờ nên làm điều này, nếu các luồng có thể là các luồng song song - hãy tự chịu rủi ro khi sử dụng ...

list.stream().collect(Collectors.toCollection(() -> myExistingList));

à, đó là một sự xấu hổ: P
Erik Kaplun

2
Kỹ thuật này sẽ thất bại khủng khiếp nếu luồng được chạy song song.
Stuart Marks

1
Nhà cung cấp bộ sưu tập có trách nhiệm đảm bảo rằng nó không thất bại - ví dụ: bằng cách cung cấp một bộ sưu tập đồng thời.
Balder

2
Không, mã này vi phạm yêu cầu của toCollection (), đó là nhà cung cấp trả lại một bộ sưu tập mới, trống của loại thích hợp. Ngay cả khi đích đến an toàn theo luồng, việc hợp nhất được thực hiện cho trường hợp song song sẽ dẫn đến kết quả không chính xác.
Stuart Marks

1
@Balder Tôi đã thêm một câu trả lời cần làm rõ điều này.
Stuart Marks

4

Bạn chỉ cần giới thiệu danh sách ban đầu của bạn là danh sách Collectors.toList()trả về.

Đây là bản demo:

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class Reference {

  public static void main(String[] args) {
    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
    System.out.println(list);

    // Just collect even numbers and start referring the new list as the original one.
    list = list.stream()
               .filter(n -> n % 2 == 0)
               .collect(Collectors.toList());
    System.out.println(list);
  }
}

Và đây là cách bạn có thể thêm các yếu tố mới được tạo vào danh sách ban đầu của mình chỉ trong một dòng.

List<Integer> list = ...;
// add even numbers from the list to the list again.
list.addAll(list.stream()
                .filter(n -> n % 2 == 0)
                .collect(Collectors.toList())
);

Đó là những gì Mô hình lập trình chức năng này cung cấp.


Tôi muốn nói làm thế nào để thêm / thu thập vào một danh sách hiện có không chỉ là gán lại.
codefx

1
Chà, về mặt kỹ thuật, bạn không thể thực hiện loại công cụ đó trong mô hình Lập trình hàm, tất cả đều là luồng. Trong Lập trình chức năng, trạng thái không được sửa đổi, thay vào đó, các trạng thái mới được tạo trong các cấu trúc dữ liệu liên tục, làm cho nó an toàn cho các mục đích tương tranh và nhiều chức năng hơn. Cách tiếp cận tôi đã đề cập là những gì bạn có thể làm hoặc bạn có thể sử dụng cách tiếp cận hướng đối tượng kiểu cũ, nơi bạn lặp lại qua từng yếu tố và giữ hoặc loại bỏ các yếu tố khi bạn thấy phù hợp.
Aman Agnihotri

0

targetList = sourceList.stream (). Flatmap (List :: stream) .collect (Collector.toList ());


0

Tôi sẽ ghép nối danh sách cũ và danh sách mới dưới dạng luồng và lưu kết quả vào danh sách đích. Hoạt động tốt song song, quá.

Tôi sẽ sử dụng ví dụ về câu trả lời được chấp nhận được đưa ra bởi Stuart Marks:

List<String> destList = Arrays.asList("foo");
List<String> newList = Arrays.asList("0", "1", "2", "3", "4", "5");

destList = Stream.concat(destList.stream(), newList.stream()).parallel()
            .collect(Collectors.toList());
System.out.println(destList);

//output: [foo, 0, 1, 2, 3, 4, 5]

Hy vọng nó giúp.

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.