Java 8 - วิธีที่ดีที่สุดในการแปลงรายการ: แผนที่หรือ foreach?


188

ฉันมีรายการmyListToParseที่ฉันต้องการกรององค์ประกอบและใช้วิธีการในแต่ละองค์ประกอบและเพิ่มผลลัพธ์ในรายการอื่นmyFinalListที่ฉันต้องการกรององค์ประกอบและวิธีการใช้ในแต่ละองค์ประกอบและเพิ่มผลในรายการอื่น

ด้วย Java 8 ฉันสังเกตเห็นว่าฉันสามารถทำได้สองวิธีที่แตกต่างกัน ฉันต้องการทราบวิธีที่มีประสิทธิภาพมากขึ้นระหว่างพวกเขาและเข้าใจว่าทำไมวิธีหนึ่งจึงดีกว่าอีกวิธีหนึ่ง

ฉันเปิดรับข้อเสนอแนะเกี่ยวกับวิธีที่สาม

วิธีที่ 1:

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

วิธีที่ 2:

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

55
อันที่สอง ฟังก์ชั่นที่เหมาะสมไม่ควรมีผลข้างเคียงในการใช้งานครั้งแรกของคุณคุณกำลังแก้ไขโลกภายนอก
ThanksForAllTheFish

37
แค่เรื่องของสไตล์ แต่elt -> elt != nullสามารถถูกแทนที่ด้วยObjects::nonNull
the8472

2
@ the8472 ดียิ่งขึ้นจะทำให้แน่ใจว่าไม่มีค่า null ในคอลเลกชันในสถานที่แรกและใช้แทนร่วมกับOptional<T> flatMap
เฮอร์แมน

2
@SymymonRoziewski ไม่มาก สำหรับบางสิ่งที่ไม่สำคัญอย่างนี้งานที่จำเป็นในการติดตั้งพอร์ตขนานภายใต้ประทุนจะทำให้การใช้เสียงนี้สร้างขึ้น
MK

2
โปรดทราบว่าคุณสามารถเขียน.map(this::doSomething)สมมติว่าdoSomethingเป็นวิธีที่ไม่คงที่ ถ้าเป็นแบบคงที่คุณสามารถแทนที่thisด้วยชื่อคลาส
เฮอร์แมน

คำตอบ:


153

ไม่ต้องกังวลกับความแตกต่างของประสิทธิภาพใด ๆ พวกเขาจะมีค่าน้อยที่สุดในกรณีนี้ตามปกติ

วิธีที่ 2 เป็นที่นิยมมากกว่าเพราะ

  1. ไม่จำเป็นต้องปิดการใช้งานคอลเล็กชันที่มีอยู่นอกนิพจน์แลมบ์ดา

  2. ก็สามารถอ่านได้มากขึ้นเพราะขั้นตอนที่แตกต่างกันที่จะดำเนินการในท่อคอลเลกชันที่เขียนตามลำดับแรกการดำเนินการกรองแล้วการดำเนินการแผนที่แล้วเก็บรวบรวมผล (สำหรับข้อมูลเพิ่มเติมเกี่ยวกับผลประโยชน์ของท่อคอลเลกชันให้ดูที่มาร์ตินฟาวเลอร์ของบทความดี )

  3. คุณสามารถเปลี่ยนวิธีการรวบรวมค่าโดยการแทนที่ค่าCollectorที่ใช้ ในบางกรณีคุณอาจจำเป็นต้องเขียนของคุณเองCollectorแต่ประโยชน์ก็คือคุณสามารถนำมาใช้ซ้ำได้ง่าย


43

ฉันเห็นด้วยกับคำตอบที่มีอยู่ว่ารูปแบบที่สองนั้นดีกว่าเพราะไม่มีผลข้างเคียงใด ๆ และง่ายต่อการขนาน (ใช้กระแสข้อมูลแบบขนาน)

ประสิทธิภาพการทำงานที่ชาญฉลาดจะปรากฏขึ้นพวกเขาจะเทียบเท่าจนกว่าคุณจะเริ่มใช้สตรีมขนาน ในกรณีนั้นแผนที่จะทำงานได้ดีขึ้นมาก ดูผลการวัดประสิทธิภาพขนาดเล็กด้านล่าง:

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

คุณไม่สามารถยกตัวอย่างแรกในลักษณะเดียวกันได้เพราะforEachเป็นวิธีการเทอร์มินัล - มันจะคืนค่าเป็นโมฆะ - ดังนั้นคุณจะถูกบังคับให้ใช้แลมบ์ดาแบบรัฐ แต่ที่เป็นจริงความคิดที่ดีถ้าคุณกำลังใช้กระแสขนาน

สุดท้ายโปรดทราบว่าข้อมูลโค้ดที่สองของคุณสามารถเขียนด้วยวิธีที่สั้นกระชับยิ่งขึ้นด้วยการอ้างอิงวิธีการและการนำเข้าแบบคงที่:

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

1
เกี่ยวกับประสิทธิภาพในกรณีของคุณ "map" จะชนะมากกว่า "forEach" หากคุณใช้ parallelStreams benchmaks ของฉันเป็นมิลลิวินาที: 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 มันขึ้นอยู่กับ assylias แต่สำหรับความเห็นของฉันการแก้ไขของคุณขัดแย้งกับเจตนาของผู้เขียนต้นฉบับ หากคุณต้องการเพิ่มคำตอบของคุณเองควรเพิ่มคำตอบแทนที่จะแก้ไขคำตอบเดิม นอกจากนี้ลิงก์ไปยัง microbenchmark ไม่เกี่ยวข้องกับผลลัพธ์
Tagir Valeev

5

หนึ่งในประโยชน์หลักของการใช้กระแสข้อมูลคือให้ความสามารถในการประมวลผลข้อมูลด้วยวิธีการที่เปิดเผยนั่นคือการใช้รูปแบบการทำงานของการเขียนโปรแกรม นอกจากนี้ยังให้ความสามารถแบบมัลติเธรดสำหรับความหมายฟรีไม่จำเป็นต้องเขียนโค้ดแบบมัลติเธรดใด ๆ เพิ่มเติมเพื่อให้สตรีมของคุณพร้อมกัน

สมมติว่าเหตุผลที่คุณสำรวจรูปแบบการเขียนโปรแกรมนี้คือคุณต้องการใช้ประโยชน์เหล่านี้จากนั้นตัวอย่างโค้ดแรกของคุณอาจไม่ทำงานเนื่องจาก foreachวิธีการนี้จัดอยู่ในประเภทเทอร์มินัล (หมายความว่ามันสามารถสร้างผลข้างเคียง)

วิธีที่สองเป็นที่ต้องการจากมุมมองการตั้งโปรแกรมการทำงานเนื่องจากฟังก์ชั่นแผนที่สามารถรับฟังก์ชั่นแลมบ์ดาได้ อย่างชัดเจนแลมบ์ดาที่ส่งผ่านไปยังฟังก์ชันแผนที่ควรเป็น

  1. ไม่รบกวนหมายความว่าฟังก์ชั่นไม่ควรเปลี่ยนแหล่งที่มาของกระแสถ้ามันไม่เกิดขึ้นพร้อมกัน (เช่น ArrayList )
  2. ไร้สัญชาติเพื่อหลีกเลี่ยงผลลัพธ์ที่ไม่คาดคิดเมื่อทำการประมวลผลแบบขนาน (เกิดจากความแตกต่างของการกำหนดตารางเวลาเธรด)

ประโยชน์อีกประการหนึ่งกับแนวทางที่สองคือหากกระแสขนานและตัวสะสมพร้อมกันและไม่เรียงลำดับลักษณะเหล่านี้สามารถให้คำแนะนำที่เป็นประโยชน์ต่อการดำเนินการลดขนาดเพื่อทำการรวบรวมพร้อมกัน


4

หากคุณใช้Eclipse Collectionsคุณสามารถใช้collectIf()วิธีนี้ได้

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);

มันประเมินผลอย่างกระตือรือร้นและควรจะเร็วกว่าการใช้สตรีมเล็กน้อย

หมายเหตุ:ฉันเป็นคอมมิชชันสำหรับ Eclipse Collections


1

ฉันชอบวิธีที่สอง

เมื่อคุณใช้วิธีแรกถ้าคุณตัดสินใจที่จะใช้กระแสข้อมูลแบบขนานเพื่อปรับปรุงประสิทธิภาพคุณจะไม่สามารถควบคุมลำดับที่จะเพิ่มองค์ประกอบในรายการผลลัพธ์forEachได้

เมื่อคุณใช้toListStreams API จะรักษาลำดับแม้ว่าคุณจะใช้สตรีมแบบขนาน


ฉันไม่แน่ใจว่านี่เป็นคำแนะนำที่ถูกต้อง: เขาสามารถใช้forEachOrderedแทนforEachถ้าเขาต้องการใช้กระแสข้อมูลแบบขนาน แต่ยังคงรักษาลำดับไว้ แต่ในฐานะที่เป็นเอกสารสำหรับforEachรัฐการรักษาความสงบเรียบร้อยเพื่อผลประโยชน์ของความเท่าเทียม ฉันสงสัยว่าเป็นเช่นtoListนั้นด้วย
เฮอร์แมน

0

มีตัวเลือกที่สามคือใช้stream().toArray()ดูความคิดเห็นภายใต้สาเหตุที่ไม่มีสตรีมมีเมธอด toListเหตุผลที่ไม่ได้มีวิธีการสตรีมมันจะช้ากว่า forEach () หรือ collect () และแสดงออกน้อยกว่า มันอาจได้รับการปรับให้เหมาะสมในรุ่นต่อมาของ JDK ดังนั้นควรเพิ่มที่นี่ในกรณี

ทะลึ่ง List<String>

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

ด้วยเกณฑ์มาตรฐานไมโครไมโคร, รายการ 1M, โมฆะ 20% และการแปลงอย่างง่ายใน doSomething ()

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;
}

ผลลัพธ์ที่ได้คือ

ขนาน:

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}

ลำดับ:

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}

แบบขนานโดยไม่มีค่า null และตัวกรอง (ดังนั้นสตรีมSIZED): toArrays มีประสิทธิภาพที่ดีที่สุดในกรณีดังกล่าวและ.forEach()ล้มเหลวด้วย "indexOutOfBounds" ใน ArrayList ของผู้รับต้องแทนที่ด้วย.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

อาจเป็นวิธีที่ 3

ฉันชอบแยกตรรกะเสมอ

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

หากใช้ Pary Libaries คนที่ 3 ก็สามารถตอบสนองต่อไซคลอปได้กำหนด Lazy ขยายคอลเลกชันที่มีฟังก์ชั่นนี้ในตัวตัวอย่างเช่นเราสามารถเขียน

ListX myListToParse;

ListX myFinalList = myListToParse.filter (elt -> elt! = null) .map (elt -> doSomething (elt));

myFinalList จะไม่ถูกประเมินจนกว่าจะเข้าถึงครั้งแรก (และหลังจากนั้นรายการ materialized จะถูกแคชและนำกลับมาใช้ใหม่)

[การเปิดเผยข้อมูลฉันเป็นผู้นำการพัฒนาไซคลอปตอบสนอง]

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.