ในสตรีม Java มองเฉพาะสำหรับการดีบักเท่านั้น?


141

ฉันกำลังอ่านข้อมูลเกี่ยวกับสตรีม Java และค้นพบสิ่งใหม่ ๆ ในขณะที่ดำเนินการไป หนึ่งในสิ่งใหม่ที่ฉันพบคือpeek()ฟังก์ชัน เกือบทุกอย่างที่ฉันอ่านใน peek บอกว่าควรใช้เพื่อดีบักสตรีมของคุณ

จะเกิดอะไรขึ้นถ้าฉันมีสตรีมที่แต่ละบัญชีมีชื่อผู้ใช้ฟิลด์รหัสผ่านและวิธีการเข้าสู่ระบบ () และ loggedIn ()

ฉันยังมี

Consumer<Account> login = account -> account.login();

และ

Predicate<Account> loggedIn = account -> account.loggedIn();

ทำไมต้องแย่ขนาดนี้

List<Account> accounts; //assume it's been setup
List<Account> loggedInAccount = 
accounts.stream()
    .peek(login)
    .filter(loggedIn)
    .collect(Collectors.toList());

เท่าที่ฉันสามารถบอกได้ว่าสิ่งนี้ทำในสิ่งที่ตั้งใจจะทำ มัน;

  • รับรายการบัญชี
  • พยายามเข้าสู่ระบบแต่ละบัญชี
  • กรองบัญชีใด ๆ ที่ไม่ได้เข้าสู่ระบบ
  • รวบรวมบัญชีที่เข้าสู่ระบบไว้ในรายการใหม่

อะไรคือข้อเสียของการทำอะไรแบบนี้? เหตุผลใดที่ฉันไม่ควรดำเนินการต่อ? สุดท้ายถ้าไม่ใช่วิธีนี้แล้วล่ะ?

เวอร์ชันดั้งเดิมนี้ใช้เมธอด. filter () ดังนี้

.filter(account -> {
        account.login();
        return account.loggedIn();
    })

42
เมื่อใดก็ตามที่ฉันพบว่าตัวเองต้องการแลมด้าแบบหลายบรรทัดฉันย้ายบรรทัดไปยังเมธอดส่วนตัวและส่งต่อการอ้างอิงเมธอดแทนแลมด้า
VGR

1
เจตนาคืออะไร - คุณกำลังพยายามเข้าสู่ระบบบัญชีทั้งหมดและกรองตามว่ามีการเข้าสู่ระบบหรือไม่ (ซึ่งอาจเป็นจริงเล็กน้อย) หรือคุณต้องการเข้าสู่ระบบจากนั้นกรองตามว่าได้เข้าสู่ระบบหรือไม่? ฉันขอนี้ในการสั่งซื้อนี้เพราะอาจจะมีการดำเนินการที่คุณต้องการเมื่อเทียบกับforEach peekเพียงเพราะอยู่ใน API ไม่ได้หมายความว่าจะไม่เปิดให้มีการละเมิด (เช่นOptional.of)
Makoto

8
โปรดทราบว่ารหัสของคุณอาจเป็น.peek(Account::login)และ.filter(Account::loggedIn); ไม่มีเหตุผลที่จะต้องเขียน Consumer และ Predicate ที่เรียกใช้วิธีอื่นเช่นนั้น
Joshua Taylor

2
นอกจากนี้ทราบว่ากระแสของ API อย่างชัดเจนลดผลข้างเคียงในพารามิเตอร์พฤติกรรม
Didier L

6
ผู้บริโภคที่มีประโยชน์มักจะมีผลข้างเคียงเสมอซึ่งไม่ท้อแท้แน่นอน สิ่งนี้ถูกกล่าวถึงในส่วนเดียวกัน:“ การดำเนินการสตรีมจำนวนเล็กน้อยเช่นforEach()และpeek()สามารถทำงานผ่านผลข้างเคียงเท่านั้น ควรใช้ด้วยความระมัดระวัง ”. คำพูดของฉันได้มากขึ้นเพื่อเตือนว่าpeekการดำเนินงาน (ซึ่งถูกออกแบบมาเพื่อวัตถุประสงค์ในการแก้จุดบกพร่อง) ไม่ควรถูกแทนที่ด้วยการทำสิ่งเดียวกันภายในการดำเนินการอื่นเช่นหรือmap() filter()
Didier L

คำตอบ:


79

ประเด็นสำคัญจากสิ่งนี้:

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


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

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

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

accounts.forEach(a -> a.login());
List<Account> loggedInAccounts = accounts.stream()
                                         .filter(Account::loggedIn)
                                         .collect(Collectors.toList());

4
หากคุณทำการเข้าสู่ระบบในขั้นตอนก่อนการประมวลผลคุณไม่จำเป็นต้องสตรีมเลย คุณสามารถดำเนินการได้forEachทันทีที่คอลเลกชันที่มา:accounts.forEach(a -> a.login());
Holger

1
@ Holger: จุดที่ยอดเยี่ยม ฉันได้รวมสิ่งนั้นไว้ในคำตอบแล้ว
Makoto

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

2
แน่นอนว่ามันง่ายกว่ามากหากlogin()วิธีการส่งคืนbooleanค่าที่ระบุสถานะความสำเร็จ ...
Holger

3
นั่นคือสิ่งที่ฉันตั้งเป้าไว้ หากlogin()ส่งคืน a booleanคุณสามารถใช้เป็นเพรดิเคตซึ่งเป็นโซลูชันที่สะอาดที่สุด มันก็ยังมีผลข้างเคียง แต่ไม่เป็นไรตราบใดที่มันไม่รบกวนเช่นloginprocess` หนึ่งAccountมีอิทธิพลต่อ process` login` Accountของผู้อื่นไม่มี
Holger

117

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

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

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

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

ดังนั้นสิ่งที่มีประโยชน์ที่สุดที่คุณสามารถทำได้peekคือการตรวจสอบว่าองค์ประกอบสตรีมได้รับการประมวลผลหรือไม่ซึ่งตรงตามที่เอกสาร API ระบุไว้:

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


จะมีปัญหาในอนาคตหรือปัจจุบันในกรณีการใช้งานของ OP หรือไม่? รหัสของเขาทำในสิ่งที่ต้องการเสมอหรือไม่?
ZhongYu

9
@ bayou.io: เท่าที่ผมสามารถมองเห็นมีปัญหาไม่ได้อยู่ในรูปแบบนี้แน่นอน แต่ในขณะที่ฉันพยายามอธิบายการใช้วิธีนี้หมายความว่าคุณต้องจำแง่มุมนี้แม้ว่าคุณจะกลับมาที่รหัสหนึ่งหรือสองปีต่อมาเพื่อรวม«คุณลักษณะขอ 9876 »เข้ากับรหัส ...
Holger

1
"การแอบมองอาจถูกเรียกใช้ตามลำดับโดยพลการและพร้อมกัน" คำพูดนี้ไม่ขัดต่อกฎของพวกเขาสำหรับวิธีการทำงานของการมองเช่น "เมื่อองค์ประกอบถูกใช้ไป"
Jose Martinez

5
@Jose Martinez: มันบอกว่า "เนื่องจากองค์ประกอบถูกใช้จากสตรีมที่เป็นผลลัพธ์ " ซึ่งไม่ใช่การกระทำของเทอร์มินัล แต่เป็นการประมวลผลแม้ว่าการกระทำของเทอร์มินัลอาจใช้องค์ประกอบที่ไม่เป็นระเบียบตราบใดที่ผลลัพธ์สุดท้ายจะสอดคล้องกัน แต่ฉันยังคิดว่าวลีของบันทึก API " ดูองค์ประกอบที่ไหลผ่านจุดหนึ่งในไปป์ไลน์ " อธิบายได้ดีกว่า
Holger

25

บางทีหลักการง่ายๆก็คือถ้าคุณใช้ peek นอกสถานการณ์ "debug" คุณควรทำก็ต่อเมื่อคุณแน่ใจว่าเงื่อนไขการยุติและการกรองระดับกลางคืออะไร ตัวอย่างเช่น:

return list.stream().map(foo->foo.getBar())
                    .peek(bar->bar.publish("HELLO"))
                    .collect(Collectors.toList());

ดูเหมือนจะเป็นกรณีที่ถูกต้องที่คุณต้องการในการดำเนินการเดียวเพื่อเปลี่ยน Foos ทั้งหมดเป็น Bars และบอกพวกเขาทุกคนสวัสดี

ดูเหมือนมีประสิทธิภาพและสง่างามกว่าสิ่งที่ชอบ:

List<Bar> bars = list.stream().map(foo->foo.getBar()).collect(Collectors.toList());
bars.forEach(bar->bar.publish("HELLO"));
return bars;

และคุณจะไม่ต้องทำซ้ำคอลเล็กชันซ้ำสองครั้ง


การทำซ้ำสองครั้งคือ O (2n) = ~ O (n) โอกาสที่คุณจะมีปัญหาด้านประสิทธิภาพเนื่องจากสิ่งนี้มีน้อย อย่างไรก็ตามคุณให้คะแนนความชัดเจนหากคุณไม่ใช้ peek
GauravJ

6

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

ตอนนี้คำถามที่อาจจะ: เราควรกลายพันธุ์สตรีมวัตถุหรือเปลี่ยนสถานะระดับโลกจากภายในฟังก์ชั่นในการเขียนโปรแกรมจาวาสไตล์การทำงาน ?

ถ้าคำตอบใด ๆ ดังกล่าวข้างต้น 2 คำถามคือใช่ (หรือในบางกรณีใช่) แล้วpeek()เป็นแน่นอนไม่เพียง แต่สำหรับแก้จุดบกพร่องเพื่อ , สำหรับเหตุผลเดียวกันกับที่forEach()ไม่ได้เป็นเพียงสำหรับการแก้จุดบกพร่องเพื่อ

สำหรับฉันเมื่อเลือกระหว่างforEach()และpeek()กำลังเลือกสิ่งต่อไปนี้: ฉันต้องการให้ชิ้นส่วนของโค้ดที่กลายพันธุ์อ็อบเจ็กต์สตรีมแนบกับคอมโพสิตได้หรือไม่หรือต้องการแนบโดยตรงกับสตรีม

ฉันคิดว่าpeek()จะจับคู่กับวิธี java9 ได้ดีกว่า เช่นtakeWhile()อาจต้องตัดสินใจว่าจะหยุดการทำซ้ำเมื่อใดโดยอาศัยวัตถุที่กลายพันธุ์แล้วดังนั้นการจับคู่กับวัตถุforEach()จะไม่มีผลเช่นเดียวกัน

ป.ล.ฉันไม่ได้อ้างอิงmap()ที่ใดเพราะในกรณีที่เราต้องการกลายพันธุ์วัตถุ (หรือสภาวะโลก) แทนที่จะสร้างวัตถุใหม่มันจะทำงานเหมือนpeek()กันทุกประการ


4

แม้ว่าฉันจะเห็นด้วยกับคำตอบส่วนใหญ่ข้างต้น แต่ฉันก็มีกรณีหนึ่งที่การใช้ Peek ดูเหมือนจะเป็นวิธีที่สะอาดที่สุด

คล้ายกับกรณีการใช้งานของคุณสมมติว่าคุณต้องการกรองเฉพาะบัญชีที่ใช้งานอยู่จากนั้นทำการล็อกอินในบัญชีเหล่านี้

accounts.stream()
    .filter(Account::isActive)
    .peek(login)
    .collect(Collectors.toList());

Peek มีประโยชน์ในการหลีกเลี่ยงการโทรซ้ำซ้อนในขณะที่ไม่ต้องวนซ้ำคอลเลกชันสองครั้ง:

accounts.stream()
    .filter(Account::isActive)
    .map(account -> {
        account.login();
        return account;
    })
    .collect(Collectors.toList());

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

1

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

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