ฉันควรใช้สตรีมเมื่อใด


102

ฉันเพิ่งเจอคำถามเมื่อใช้ a Listและstream()วิธีการของมัน แม้ว่าฉันจะรู้วิธีใช้ แต่ฉันก็ไม่ค่อยแน่ใจว่าควรใช้เมื่อใด

ตัวอย่างเช่นฉันมีรายการที่มีเส้นทางต่างๆไปยังสถานที่ต่างๆ ตอนนี้ฉันต้องการตรวจสอบว่าเส้นทางเดียวที่ระบุมีเส้นทางใด ๆ ที่ระบุในรายการหรือไม่ ฉันต้องการส่งคืนbooleanโดยพิจารณาว่าเป็นไปตามเงื่อนไขหรือไม่

แน่นอนว่านี่ไม่ใช่งานหนัก แต่ฉันสงสัยว่าฉันควรใช้สตรีมหรือสำหรับ (-each) ลูป

รายการ

private static final List<String> EXCLUDE_PATHS = Arrays.asList(new String[]{
    "my/path/one",
    "my/path/two"
});

ตัวอย่าง - สตรีม

private boolean isExcluded(String path){
    return EXCLUDE_PATHS.stream()
                        .map(String::toLowerCase)
                        .filter(path::contains)
                        .collect(Collectors.toList())
                        .size() > 0;
}

ตัวอย่าง - สำหรับ - แต่ละวง

private boolean isExcluded(String path){
    for (String excludePath : EXCLUDE_PATHS) {
        if(path.contains(excludePath.toLowerCase())){
            return true;
        }
    }
    return false;
}

หมายเหตุว่าpathพารามิเตอร์อยู่เสมอเป็นตัวพิมพ์เล็ก

การเดาแรกของฉันคือสำหรับแต่ละวิธีนั้นเร็วกว่าเพราะลูปจะกลับมาทันทีหากตรงตามเงื่อนไข ในขณะที่สตรีมยังคงวนซ้ำในรายการทั้งหมดเพื่อให้การกรองเสร็จสมบูรณ์

สมมติฐานของฉันถูกต้องหรือไม่? ถ้าเป็นเช่นนั้นทำไม (หรือเมื่อนั้น ) ฉันจึงจะใช้stream()?


11
สตรีมสามารถแสดงออกและอ่านได้ง่ายกว่าฟอร์ลูปแบบเดิม ในภายหลังคุณต้องระมัดระวังเกี่ยวกับอินทรินส์ของ if-then และเงื่อนไขเป็นต้นการแสดงออกของสตรีมมีความชัดเจนมาก: แปลงชื่อไฟล์เป็นตัวพิมพ์เล็กจากนั้นกรองตามบางสิ่งจากนั้นนับรวบรวม ฯลฯ ผลลัพธ์: ซ้ำมาก การแสดงออกของขั้นตอนการคำนวณ
Jean-Baptiste Yunès

12
ไม่มีความจำเป็นสำหรับnew String[]{…}ที่นี่ แค่ใช้Arrays.asList("my/path/one", "my/path/two")
Holger

4
ถ้าแหล่งของคุณเป็นมีความจำเป็นต่อการเรียกร้องใดString[]Arrays.asListคุณสามารถสตรีมผ่านอาร์เรย์โดยใช้Arrays.stream(array). อย่างไรก็ตามฉันมีปัญหาในการทำความเข้าใจจุดประสงค์ของการisExcludedทดสอบทั้งหมด มันน่าสนใจจริงๆหรือไม่ว่าองค์ประกอบของEXCLUDE_PATHSมันมีอยู่ที่ไหนสักแห่งในเส้นทาง? คือisExcluded("my/path/one/foo/bar/baz")จะกลับมาtrueเช่นเดียวกับisExcluded("foo/bar/baz/my/path/one/")
Holger

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

1
เหตุใดจึงtoLowerCaseใช้กับค่าคงที่ซึ่งเป็นตัวพิมพ์เล็กอยู่แล้ว ไม่ควรนำมาใช้กับการpathโต้แย้ง?
Sebastian Redl

คำตอบ:


80

สมมติฐานของคุณถูกต้อง การใช้งานสตรีมของคุณช้ากว่าแบบ for-loop

การใช้สตรีมนี้ควรเร็วพอ ๆ กับ for-loop แม้ว่า:

EXCLUDE_PATHS.stream()  
                               .map(String::toLowerCase)
                               .anyMatch(path::contains);

สิ่งนี้จะวนซ้ำผ่านรายการการใช้งานString::toLowerCaseและตัวกรองกับรายการทีละรายการและสิ้นสุดที่รายการแรกที่ตรงกัน

ทั้งสองcollect()& anyMatch()เป็นการดำเนินการของเทอร์มินัล anyMatch()ออกจากรายการแรกที่พบในขณะที่collect()ต้องประมวลผลรายการทั้งหมด


2
น่ากลัวไม่รู้เกี่ยวfindFirst()กับfilter(). เห็นได้ชัดว่าผมไม่ทราบวิธีการใช้สตรีมเช่นเดียวกับที่ผมคิด
mcuenez

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

หลังจากแก้ไขแล้วฉันรู้สึกว่าคำตอบของคุณเป็นคำตอบที่ควรได้รับการยอมรับเนื่องจากคุณตอบคำถามของฉันในความคิดเห็นของคำตอบอื่นด้วย แม้ว่าฉันต้องการให้เครดิต @ rvit34 สำหรับการโพสต์รหัส :-)
mcuenez

34

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

ด้วย.filter(path::contains).collect(Collectors.toList()).size() > 0วิธีการของคุณคุณกำลังประมวลผลองค์ประกอบทั้งหมดและรวบรวมเป็นองค์ประกอบชั่วคราวListก่อนที่จะเปรียบเทียบขนาด แต่สิ่งนี้แทบจะไม่สำคัญสำหรับสตรีมที่ประกอบด้วยสององค์ประกอบ

การใช้.map(String::toLowerCase).anyMatch(path::contains)สามารถประหยัดรอบของ CPU และหน่วยความจำหากคุณมีองค์ประกอบจำนวนมาก ถึงกระนั้นสิ่งนี้จะแปลงแต่ละรายการStringเป็นการแทนตัวพิมพ์เล็กจนกว่าจะพบรายการที่ตรงกัน เห็นได้ชัดว่ามีจุดในการใช้

private static final List<String> EXCLUDE_PATHS =
    Stream.of("my/path/one", "my/path/two").map(String::toLowerCase)
          .collect(Collectors.toList());

private boolean isExcluded(String path) {
    return EXCLUDE_PATHS.stream().anyMatch(path::contains);
}

แทน. คุณจึงไม่ต้องแปลงซ้ำเป็น lowcase ในทุกครั้งที่เรียกisExcludedใช้ หากจำนวนองค์ประกอบEXCLUDE_PATHSหรือความยาวของสตริงมีมากคุณอาจลองใช้

private static final List<Predicate<String>> EXCLUDE_PATHS =
    Stream.of("my/path/one", "my/path/two").map(String::toLowerCase)
          .map(s -> Pattern.compile(s, Pattern.LITERAL).asPredicate())
          .collect(Collectors.toList());

private boolean isExcluded(String path){
    return EXCLUDE_PATHS.stream().anyMatch(p -> p.test(path));
}

การรวบรวมสตริงเป็นรูปแบบ regex ด้วยLITERALแฟล็กทำให้ทำงานเหมือนกับการทำงานของสตริงทั่วไป แต่ช่วยให้เอ็นจิ้นใช้เวลาในการเตรียมการเช่นใช้อัลกอริทึม Boyer Moore เพื่อให้มีประสิทธิภาพมากขึ้นเมื่อเทียบกับการเปรียบเทียบจริง

แน่นอนว่าสิ่งนี้จะจ่ายออกไปก็ต่อเมื่อมีการทดสอบในภายหลังเพียงพอที่จะชดเชยเวลาที่ใช้ในการเตรียมการ การพิจารณาว่าจะเป็นเช่นนั้นหรือไม่เป็นหนึ่งในการพิจารณาประสิทธิภาพที่แท้จริงนอกเหนือจากคำถามแรกว่าการดำเนินการนี้จะมีความสำคัญต่อประสิทธิภาพหรือไม่ ไม่ใช่คำถามว่าจะใช้สตรีมหรือforลูป

อย่างไรก็ตามตัวอย่างโค้ดด้านบนยังคงใช้ตรรกะของรหัสเดิมของคุณซึ่งดูน่าสงสัยสำหรับฉัน ของคุณisExcludedวิธีการส่งกลับtrueถ้าเส้นทางที่ระบุมีการใด ๆ ขององค์ประกอบในรายการจึงส่งกลับtrueสำหรับ/some/prefix/to/my/path/oneเช่นเดียวกับหรือแม้กระทั่งmy/path/one/and/some/suffix/some/prefix/to/my/path/one/and/some/suffix

แม้dummy/path/onerousจะถือว่าเป็นไปตามเกณฑ์เนื่องจากcontainsสตริงmy/path/one...


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

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

สตรีมจริงเหมาะอย่างยิ่งสำหรับการเพิ่มประสิทธิภาพหน่วยความจำเมื่อจำนวนหน่วยความจำที่ใช้งานได้หมดขีด จำกัด ของเซิร์ฟเวอร์
ColacX

21

ใช่. คุณพูดถูก วิธีการสตรีมของคุณจะมีค่าใช้จ่ายบางส่วน แต่คุณสามารถใช้โครงสร้างดังกล่าว:

private boolean isExcluded(String path) {
    return  EXCLUDE_PATHS.stream().map(String::toLowerCase).anyMatch(path::contains);
}

เหตุผลหลักในการใช้สตรีมคือทำให้โค้ดของคุณง่ายขึ้นและอ่านง่าย


3
เป็นanyMatchทางลัดสำหรับfilter(...).findFirst().isPresent()?
mcuenez

6
ใช่แล้ว! ดีกว่าคำแนะนำแรกของฉันด้วยซ้ำ
Stefan Pries

8

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

หากเราต้องการประสิทธิภาพเราควรใช้ ParallelStream ซึ่งออกแบบมาเพื่อ โดยทั่วไปแล้วอนุกรมจะช้ากว่า

มีบทความดีในการอ่านเกี่ยวกับการเป็น, และผลการปฏิบัติงาน ForLoopStreamParallelStream

ในโค้ดของคุณเราสามารถใช้วิธีการยุติเพื่อหยุดการค้นหาในนัดแรกได้ (anyMatch ... )


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

0

อย่างที่คนอื่นพูดถึงจุดดีมากมาย แต่ฉันแค่อยากจะพูดถึงการประเมินแบบขี้เกียจในการประเมินกระแส เมื่อเราmap()สร้างสตรีมของพา ธ ตัวพิมพ์เล็กเราไม่ได้สร้างสตรีมทั้งหมดในทันที แต่สตรีมจะถูกสร้างขึ้นอย่างเฉื่อยชาซึ่งเป็นสาเหตุที่ประสิทธิภาพควรเทียบเท่ากับแบบดั้งเดิมสำหรับลูป มันไม่ได้ทำการสแกนแบบเต็มmap()และanyMatch()ถูกดำเนินการในเวลาเดียวกัน เมื่อanyMatch()คืนค่าเป็นจริงมันจะลัดวงจร

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