Javadoc of Collector cho thấy cách thu thập các phần tử của luồng vào Danh sách mới. Có một lớp lót nào thêm kết quả vào một ArrayList hiện có không?
Javadoc of Collector cho thấy cách thu thập các phần tử của luồng vào Danh sách mới. Có một lớp lót nào thêm kết quả vào một ArrayList hiện có không?
Câu trả lời:
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ừ Balder và assylias đã đề 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 đó.
toCollection
phươ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.
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()
.
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 và đí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ự.
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 forEachOrdered
cũng ...
forEachOrdered
thay vì forEach
?
forEachOrdered
hoạt động cho cả hai luồng liên tiếp và song song . Ngược lại, forEach
có 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>
.
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 đó.
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 list
biế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.
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));
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.
targetList = sourceList.stream (). Flatmap (List :: stream) .collect (Collector.toList ());
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.
Collection
”