Java 8 - Cách tốt nhất để chuyển đổi một danh sách: bản đồ hoặc foreach?


188

Tôi có một danh sách myListToParsemà tôi muốn lọc các phần tử và áp dụng một phương thức cho mỗi phần tử và thêm kết quả vào danh sách khácmyFinalList .

Với Java 8 tôi nhận thấy rằng tôi có thể làm điều đó theo 2 cách khác nhau. Tôi muốn biết cách hiệu quả hơn giữa họ và hiểu tại sao một cách tốt hơn so với cách khác.

Tôi mở cho bất kỳ đề nghị về một cách thứ ba.

Cách 1:

myFinalList = new ArrayList<>();
myListToParse.stream()
        .filter(elt -> elt != null)
        .forEach(elt -> myFinalList.add(doSomething(elt)));

Cách 2:

myFinalList = myListToParse.stream()
        .filter(elt -> elt != null)
        .map(elt -> doSomething(elt))
        .collect(Collectors.toList()); 

55
Cái thứ hai. Một chức năng phù hợp sẽ không có tác dụng phụ, trong lần thực hiện đầu tiên, bạn đang sửa đổi thế giới bên ngoài.
ThanksFor ALLTheFish 4/2/2015

37
chỉ là vấn đề về phong cách, nhưng elt -> elt != nullcó thể được thay thế bằngObjects::nonNull
the8472

2
@ the8472 Thậm chí tốt hơn là đảm bảo không có giá trị null trong bộ sưu tập ở vị trí đầu tiên và sử dụng Optional<T>thay thế kết hợp với flatMap.
herman 4/2/2015

2
@SzymonRoziewski, không hẳn. Đối với một cái gì đó tầm thường như thế này, công việc cần thiết để thiết lập dòng song song dưới mui xe sẽ làm cho việc sử dụng cấu trúc này bị tắt tiếng.
MK

2
Lưu ý rằng bạn có thể viết .map(this::doSomething)giả sử đó doSomethinglà một phương thức không tĩnh. Nếu nó tĩnh, bạn có thể thay thế thisbằng tên lớp.
herman 4/2/2015

Câu trả lời:


153

Đừng lo lắng về bất kỳ sự khác biệt về hiệu suất, chúng sẽ trở nên tối thiểu trong trường hợp này thông thường.

Phương pháp 2 thích hợp hơn vì

  1. nó không yêu cầu làm biến đổi một bộ sưu tập tồn tại bên ngoài biểu thức lambda,

  2. nó dễ đọc hơn vì các bước khác nhau được thực hiện trong đường ống thu thập được viết tuần tự: đầu tiên là thao tác lọc, sau đó là thao tác bản đồ, sau đó thu thập kết quả (để biết thêm về lợi ích của đường ống thu thập, xem bài viết xuất sắc của Martin Fowler ),

  3. bạn có thể dễ dàng thay đổi cách thu thập các giá trị bằng cách thay thế giá trị được Collectorsử dụng. Trong một số trường hợp bạn có thể cần phải viết riêng của bạn Collector, nhưng sau đó lợi ích là bạn có thể dễ dàng sử dụng lại điều đó.


43

Tôi đồng ý với các câu trả lời hiện có rằng hình thức thứ hai tốt hơn bởi vì nó không có bất kỳ tác dụng phụ nào và dễ song song hơn (chỉ sử dụng một luồng song song).

Hiệu suất khôn ngoan, có vẻ như chúng tương đương cho đến khi bạn bắt đầu sử dụng các luồng song song. Trong trường hợp đó, bản đồ sẽ thực hiện tốt hơn nhiều. Xem bên dưới kết quả điểm chuẩn vi mô :

Benchmark                         Mode  Samples    Score   Error  Units
SO28319064.forEach                avgt      100  187.310 ± 1.768  ms/op
SO28319064.map                    avgt      100  189.180 ± 1.692  ms/op
SO28319064.mapWithParallelStream  avgt      100   55,577 ± 0,782  ms/op

Bạn không thể tăng ví dụ đầu tiên theo cách tương tự vì forEach là một phương thức đầu cuối - nó trả về khoảng trống - vì vậy bạn buộc phải sử dụng lambda có trạng thái. Nhưng đó thực sự là một ý tưởng tồi nếu bạn đang sử dụng các luồng song song .

Cuối cùng lưu ý rằng đoạn mã thứ hai của bạn có thể được viết theo cách ngắn gọn hơn với các tham chiếu phương thức và nhập tĩnh:

myFinalList = myListToParse.stream()
    .filter(Objects::nonNull)
    .map(this::doSomething)
    .collect(toList()); 

1
Về hiệu suất, trong trường hợp của bạn, "bản đồ" thực sự chiến thắng "forEach" nếu bạn sử dụngallelStreams. Benchmaks tôi trong mili giây: SO28319064.forEach: 187.310 ± 1.768 ms / op - SO28319064.map: 189.180 ± 1.692 ms / op --SO28319064.mapParallelStream: 55.577 ± 0.782 ms / op
Giuseppe Bertone

2
@GiuseppeBertone, tùy thuộc vào assylias, nhưng theo ý kiến ​​của tôi, bản chỉnh sửa của bạn mâu thuẫn với ý định của tác giả ban đầu. Nếu bạn muốn thêm câu trả lời của riêng mình, tốt hơn là thêm nó thay vì chỉnh sửa câu trả lời hiện có rất nhiều. Ngoài ra, bây giờ liên kết đến microbenchmark không liên quan đến kết quả.
Tagir Valeev

5

Một trong những lợi ích chính của việc sử dụng các luồng là nó mang lại khả năng xử lý dữ liệu theo cách khai báo, nghĩa là sử dụng một kiểu lập trình chức năng. Nó cũng cung cấp khả năng đa luồng cho ý nghĩa miễn phí, không cần phải viết thêm bất kỳ mã đa luồng nào để làm cho luồng của bạn đồng thời.

Giả sử lý do bạn khám phá phong cách lập trình này là vì bạn muốn khai thác những lợi ích này thì mẫu mã đầu tiên của bạn có khả năng không hoạt động do foreachphương thức được phân loại là thiết bị đầu cuối (có nghĩa là nó có thể tạo ra tác dụng phụ).

Cách thứ hai được ưa thích từ quan điểm lập trình chức năng vì chức năng bản đồ có thể chấp nhận các hàm lambda không trạng thái. Rõ ràng hơn, lambda được chuyển đến chức năng bản đồ nên

  1. Không can thiệp, có nghĩa là hàm không nên thay đổi nguồn của luồng nếu nó không đồng thời (ví dụ ArrayList).
  2. Không trạng thái để tránh kết quả bất ngờ khi thực hiện xử lý song song (gây ra bởi sự khác biệt lập lịch trình luồng).

Một lợi ích khác với cách tiếp cận thứ hai là nếu luồng song song và bộ thu đồng thời và không có thứ tự thì các đặc điểm này có thể cung cấp các gợi ý hữu ích cho hoạt động giảm để thực hiện thu thập đồng thời.


4

Nếu bạn sử dụng Bộ sưu tập Eclipse, bạn có thể sử dụng collectIf()phương thức này.

MutableList<Integer> source =
    Lists.mutable.with(1, null, 2, null, 3, null, 4, null, 5);

MutableList<String> result = source.collectIf(Objects::nonNull, String::valueOf);

Assert.assertEquals(Lists.immutable.with("1", "2", "3", "4", "5"), result);

Nó đánh giá một cách háo hức và sẽ nhanh hơn một chút so với sử dụng Stream.

Lưu ý: Tôi là người đi làm cho Bộ sưu tập Eclipse.


1

Tôi thích cách thứ hai.

Khi bạn sử dụng cách đầu tiên, nếu bạn quyết định sử dụng luồng song song để cải thiện hiệu suất, bạn sẽ không kiểm soát thứ tự các yếu tố sẽ được thêm vào danh sách đầu ra forEach.

Khi bạn sử dụng toList, API luồng sẽ duy trì thứ tự ngay cả khi bạn sử dụng luồng song song.


Tôi không chắc đây là lời khuyên chính xác: anh ta có thể sử dụng forEachOrderedthay vì forEachnếu anh ta muốn sử dụng một luồng song song nhưng vẫn giữ trật tự. Nhưng như tài liệu cho forEachcác quốc gia, giữ gìn trật tự gặp gỡ hy sinh lợi ích của sự song song. Tôi nghi ngờ đó cũng là trường hợp toListsau đó.
herman 4/2/2015

0

Có một tùy chọn thứ ba - sử dụng stream().toArray()- xem các bình luận bên dưới tại sao luồng không có phương thức toList . Hóa ra là chậm hơn forEach () hoặc coll () và ít biểu cảm hơn. Nó có thể được tối ưu hóa trong các bản dựng JDK sau này, vì vậy, thêm nó vào đây chỉ trong trường hợp.

giả định List<String>

    myFinalList = Arrays.asList(
            myListToParse.stream()
                    .filter(Objects::nonNull)
                    .map(this::doSomething)
                    .toArray(String[]::new)
    );

với điểm chuẩn vi mô, mục nhập 1M, null 20% và biến đổi đơn giản trong doS Something ()

private LongSummaryStatistics benchmark(final String testName, final Runnable methodToTest, int samples) {
    long[] timing = new long[samples];
    for (int i = 0; i < samples; i++) {
        long start = System.currentTimeMillis();
        methodToTest.run();
        timing[i] = System.currentTimeMillis() - start;
    }
    final LongSummaryStatistics stats = Arrays.stream(timing).summaryStatistics();
    System.out.println(testName + ": " + stats);
    return stats;
}

kết quả là

song song, tương đông:

toArray: LongSummaryStatistics{count=10, sum=3721, min=321, average=372,100000, max=535}
forEach: LongSummaryStatistics{count=10, sum=3502, min=249, average=350,200000, max=389}
collect: LongSummaryStatistics{count=10, sum=3325, min=265, average=332,500000, max=368}

tuần tự:

toArray: LongSummaryStatistics{count=10, sum=5493, min=517, average=549,300000, max=569}
forEach: LongSummaryStatistics{count=10, sum=5316, min=427, average=531,600000, max=571}
collect: LongSummaryStatistics{count=10, sum=5380, min=444, average=538,000000, max=557}

song song không có null và bộ lọc (vì vậy luồng là SIZED): toArrays có hiệu suất tốt nhất trong trường hợp đó và .forEach()không thành công với "indexOutOfBound" trên ArrayList người nhận, phải thay thế bằng.forEachOrdered()

toArray: LongSummaryStatistics{count=100, sum=75566, min=707, average=755,660000, max=1107}
forEach: LongSummaryStatistics{count=100, sum=115802, min=992, average=1158,020000, max=1254}
collect: LongSummaryStatistics{count=100, sum=88415, min=732, average=884,150000, max=1014}

0

Có thể là Phương pháp 3.

Tôi luôn thích giữ logic riêng biệt.

Predicate<Long> greaterThan100 = new Predicate<Long>() {
            @Override
            public boolean test(Long currentParameter) {
                return currentParameter > 100;
            }
        };

        List<Long> sourceLongList = Arrays.asList(1L, 10L, 50L, 80L, 100L, 120L, 133L, 333L);
        List<Long> resultList = sourceLongList.parallelStream().filter(greaterThan100).collect(Collectors.toList());

0

Nếu sử dụng Libary Pary thứ 3 là ok cyclops- Reac xác định các bộ sưu tập mở rộng Lazy có chức năng này được tích hợp. Ví dụ: chúng ta có thể viết đơn giản

ListX myListToPude;

ListX myFinalList = myListToPude.filter (elt -> elt! = Null) .map (elt -> doS Something (elt));

myFinalList không được đánh giá cho đến lần truy cập đầu tiên (và ở đó sau khi danh sách cụ thể hóa được lưu trữ và sử dụng lại).

[Tiết lộ Tôi là nhà phát triển chính của cyclops-Reac]

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.