ทำไมต้องใช้ combiner ในการลดเมธอดที่แปลง type ใน java 8


142

ฉันมีปัญหาในการทำความเข้าใจบทบาทที่combinerเติมเต็มในreduceวิธีสตรีมอย่างสมบูรณ์

ตัวอย่างเช่นรหัสต่อไปนี้ไม่ได้รวบรวม:

int length = asList("str1", "str2").stream()
            .reduce(0, (accumulatedInt, str) -> accumulatedInt + str.length());

ข้อผิดพลาดในการคอมไพล์แจ้งว่า: (อาร์กิวเมนต์ไม่ตรงกัน; int ไม่สามารถแปลงเป็น java.lang.String)

แต่รหัสนี้จะรวบรวม:

int length = asList("str1", "str2").stream()  
    .reduce(0, (accumulatedInt, str ) -> accumulatedInt + str.length(), 
                (accumulatedInt, accumulatedInt2) -> accumulatedInt + accumulatedInt2);

ฉันเข้าใจว่ามีการใช้วิธี combiner ในลำธารขนาน - ดังนั้นในตัวอย่างของฉันมันได้รวมเข้าด้วยกันสอง ints สะสมกลาง

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

ใครสามารถทำให้กระจ่างเกี่ยวกับเรื่องนี้?


คำถามที่เกี่ยวข้อง: stackoverflow.com/questions/24202473/…
nosid

2
aha มันใช้สำหรับสตรีมขนาน ... ฉันเรียกว่านามธรรมที่น่ากลัว!
Andy

คำตอบ:


77

ทั้งสองและสามรุ่นข้อโต้แย้งของที่คุณพยายามที่จะใช้งานไม่ยอมรับชนิดเดียวกันสำหรับreduceaccumulator

อาร์กิวเมนต์สองตัวreduceถูกกำหนดเป็น :

T reduce(T identity,
         BinaryOperator<T> accumulator)

ในกรณีของคุณ T คือ String ดังนั้นBinaryOperator<T>ควรยอมรับอาร์กิวเมนต์สองตัวและส่งคืนสตริง แต่คุณจะผ่านมันเป็น int และ String ซึ่งผลในการรวบรวมข้อผิดพลาดที่คุณได้ argument mismatch; int cannot be converted to java.lang.String- ที่จริงแล้วฉันคิดว่าการส่งค่า 0 เป็นค่าตัวตนก็ไม่ถูกต้องเช่นกันเนื่องจากคาดว่าจะมีสตริง (T)

โปรดทราบด้วยว่าการลดรุ่นนี้จะประมวลผลสตรีมของ Ts และส่งกลับค่า T ดังนั้นคุณจึงไม่สามารถใช้มันเพื่อลดการส่งกระแสข้อมูลของสตริงไปยัง int

อาร์กิวเมนต์สามตัวreduceถูกกำหนดเป็น :

<U> U reduce(U identity,
             BiFunction<U,? super T,U> accumulator,
             BinaryOperator<U> combiner)

ในกรณีของคุณ U คือจำนวนเต็มและ T คือสตริงดังนั้นวิธีนี้จะลดจำนวนสตรีมของสตริงเป็นจำนวนเต็ม

สำหรับตัวBiFunction<U,? super T,U>สะสมคุณสามารถส่งพารามิเตอร์ของสองประเภทที่แตกต่างกัน (U และ? super T) ซึ่งในกรณีของคุณคือจำนวนเต็มและสตริง นอกจากนี้ค่าเอกลักษณ์คุณยอมรับ Integer ในกรณีของคุณดังนั้นการส่งผ่าน 0 จึงเป็นเรื่องปกติ

อีกวิธีหนึ่งในการบรรลุสิ่งที่คุณต้องการ:

int length = asList("str1", "str2").stream().mapToInt (s -> s.length())
            .reduce(0, (accumulatedInt, len) -> accumulatedInt + len);

นี่คือประเภทของกระแสตรงกับประเภทของการกลับมาของเพื่อให้คุณสามารถใช้รุ่นสองพารามิเตอร์ของreducereduce

แน่นอนคุณไม่ต้องใช้reduceเลย:

int length = asList("str1", "str2").stream().mapToInt (s -> s.length())
            .sum();

8
ในฐานะที่เป็นตัวเลือกที่สองในรหัสสุดท้ายของคุณคุณสามารถใช้งานได้mapToInt(String::length)มากกว่าmapToInt(s -> s.length())ไม่แน่ใจว่าตัวเลือกหนึ่งจะดีกว่าตัวเลือกอื่นหรือไม่
skiwi

20
หลายคนจะพบคำตอบนี้เพราะพวกเขาไม่เข้าใจว่าทำไมcombinerมันถึงต้องการ แต่ทำไมไม่มีaccumulatorมันก็พอ ในกรณีดังกล่าว: Combiner จำเป็นเฉพาะสำหรับสตรีมแบบขนานเพื่อรวมผลลัพธ์ "สะสม" ของเธรด
ddekany

1
ฉันไม่พบคำตอบที่เป็นประโยชน์โดยเฉพาะของคุณ - เพราะคุณไม่ได้อธิบายว่า combiner ควรทำอย่างไรและฉันจะทำงานได้อย่างไรหากไม่มี! ในกรณีของฉันฉันต้องการลดประเภท T เป็น U แต่ไม่มีทางที่จะทำได้แบบขนานเลย มันเป็นไปไม่ได้. คุณจะบอกระบบได้อย่างไรว่าฉันไม่ต้องการ / ต้องการความเท่าเทียมและออกจาก combiner?
Zordid

@ Zordid the Streams API ไม่มีตัวเลือกในการลดประเภท T ไปยัง U โดยไม่ต้องผ่าน combiner
Eran

216

คำตอบ Eran ของการอธิบายความแตกต่างระหว่างสองหาเรื่องและสามหาเรื่องรุ่นreduceในอดีตจะช่วยลดStream<T>การTในขณะที่หลังจะช่วยลดการStream<T> Uแต่ก็ไม่ได้จริงอธิบายความจำเป็นสำหรับการทำงาน combiner เพิ่มเติมเมื่อลดการStream<T>U

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

ก่อนอื่นเรามาพิจารณาการลดลงของสองรุ่น:

T reduce(I, (T, T) -> T)

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

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

ตอนนี้ขอพิจารณาการดำเนินงานที่ลดลงสองหาเรื่องสมมุติที่ช่วยลดการStream<T> Uในภาษาอื่น ๆ สิ่งนี้เรียกว่าการดำเนินการ"fold"หรือ "fold-left" ดังนั้นนั่นคือสิ่งที่ฉันจะเรียกที่นี่ หมายเหตุสิ่งนี้ไม่มีอยู่ใน Java

U foldLeft(I, (U, T) -> U)

(โปรดทราบว่าค่าตัวตนIเป็นประเภท U)

รุ่นต่อเนื่องของfoldLeftจะเหมือนกับรุ่นต่อเนื่องreduceยกเว้นว่าค่ากลางจะเป็นประเภท U แทนประเภท T แต่เป็นอย่างอื่น (การfoldRightดำเนินการตามสมมติฐานจะคล้ายกันยกเว้นว่าการดำเนินการจะดำเนินการจากขวาไปซ้ายแทนจากซ้ายไปขวา)

foldLeftตอนนี้พิจารณารุ่นขนาน เริ่มกันเลยโดยแยกกระแสออกเป็นส่วน ๆ จากนั้นเราสามารถให้แต่ละเธรด N ลดค่า T ในเซ็กเมนต์ของมันเป็นค่ากลาง N ของประเภท U ได้แล้วตอนนี้อะไร? เราจะรับค่า N ประเภทของ U ลงมาเป็นผลลัพธ์เดียวของ type U ได้อย่างไร

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

U reduce(I, (U, T) -> U, (U, U) -> U)

หรือโดยใช้ไวยากรณ์ Java:

<U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)

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

ท้ายที่สุด Java ไม่ได้จัดเตรียมfoldLeftและfoldRightดำเนินการเพราะมันหมายถึงการเรียงลำดับเฉพาะของการดำเนินการที่เรียงตามลำดับโดยเนื้อแท้ การปะทะกันนี้มีหลักการการออกแบบที่ระบุไว้ข้างต้นในการให้บริการ API ที่สนับสนุนการทำงานแบบต่อเนื่องและแบบขนาน


7
ดังนั้นคุณจะทำอย่างไรถ้าคุณต้องการfoldLeftเพราะการคำนวณขึ้นอยู่กับผลลัพธ์ก่อนหน้าและไม่สามารถขนานกันได้?
อะมีบา

5
@amoebe คุณสามารถใช้ foldLeft forEachOrderedคุณเองโดยใช้ แม้ว่าสถานะกลางจะต้องถูกเก็บไว้ในตัวแปรที่จับได้
Stuart Marks

@ StuartMarks ขอบคุณฉันลงเอยด้วยการใช้jOOλ พวกเขามีความเรียบร้อยการดำเนินงานของ foldLeft
อะมีบา

1
รักคำตอบนี้! แก้ไขให้ฉันถ้าฉันผิด: นี่จะอธิบายว่าทำไมตัวอย่างการรันของ OP (อันที่สอง) จะไม่เรียก combiner เมื่อทำงานเป็นลำดับสตรีม
Luigi Cortese

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

116

เนื่องจากฉันชอบดูเดิลและลูกศรเพื่ออธิบายแนวคิด ... มาเริ่มกันเลย!

จากสตริงไปยังสตริง (สตรีมลำดับ)

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

คุณสามารถบรรลุสิ่งนี้ด้วย

String res = Arrays.asList("one", "two","three","four")
        .stream()
        .reduce("",
                (accumulatedStr, str) -> accumulatedStr + str);  //accumulator

และสิ่งนี้จะช่วยให้คุณเห็นภาพว่าเกิดอะไรขึ้น:

ป้อนคำอธิบายรูปภาพที่นี่

ฟังก์ชั่นการสะสมจะแปลงทีละขั้นตอนองค์ประกอบในสตรีม (สีแดง) ของคุณเป็นค่าที่ลดลง (สีเขียว) สุดท้าย ฟังก์ชั่นสะสมเพียงแค่แปลงวัตถุเข้าไปอีกStringString

จาก String ไปยัง int (สตรีมขนาน)

สมมติว่ามี 4 สายเหมือนกัน: เป้าหมายใหม่ของคุณคือการรวมความยาวของพวกเขาและคุณต้องการที่จะขนานกระแสของคุณ

สิ่งที่คุณต้องการคือสิ่งนี้:

int length = Arrays.asList("one", "two","three","four")
        .parallelStream()
        .reduce(0,
                (accumulatedInt, str) -> accumulatedInt + str.length(),                 //accumulator
                (accumulatedInt, accumulatedInt2) -> accumulatedInt + accumulatedInt2); //combiner

และนี่คือรูปแบบของสิ่งที่เกิดขึ้น

ป้อนคำอธิบายรูปภาพที่นี่

ที่นี่ฟังก์ชั่นตัวสะสม (a BiFunction) ช่วยให้คุณสามารถแปลงStringข้อมูลของคุณเป็นintข้อมูล เป็นกระแสขนานมันแยกออกเป็นสองส่วน (สีแดง) ซึ่งแต่ละส่วนจะแยกออกจากกันอย่างอิสระจากกันและก่อให้เกิดผลลัพธ์ (ส้ม) บางส่วน จำเป็นต้องมีผู้กำหนด combiner เพื่อกำหนดกฎสำหรับการรวมintผลลัพธ์บางส่วนเข้าในขั้นสุดท้าย (สีเขียว)intอย่างใดอย่างหนึ่ง

จาก String ไปยัง int (สตรีมลำดับ)

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


7
ขอบคุณสำหรับสิ่งนี้. ฉันไม่จำเป็นต้องอ่าน ฉันหวังว่าพวกเขาจะได้เพิ่มฟังก์ชั่นการพับที่ผิดปกติ
Lodewijk Bogaards

1
@LodewijkBogaards ดีใจที่ได้ช่วย! JavaDocที่นี่เป็นความลับที่ค่อนข้างแน่นอน
Luigi Cortese

@LuigiCortese ในกระแสขนานมันจะแบ่งองค์ประกอบเพื่อจับคู่เสมอ?
TheLogicGuy

1
ฉันขอขอบคุณคำตอบที่ชัดเจนและมีประโยชน์ของคุณ ฉันต้องการพูดซ้ำในสิ่งที่คุณพูดว่า: "เอาล่ะต้องมี combiner อยู่ดี แต่มันจะไม่ถูกเรียกใช้" นี่เป็นส่วนหนึ่งของการเขียนโปรแกรมเชิงปฏิบัติ Brave New World ของ Java ซึ่งฉันมั่นใจได้ว่านับครั้งไม่ถ้วน "ทำให้โค้ดของคุณกระชับและอ่านง่ายขึ้น" หวังว่าตัวอย่างของ (คำพูดนิ้ว) ที่รัดกุมชัดเจนเช่นนี้ยังมีอยู่น้อยและอยู่ไกล
dnuttle

มันจะดีกว่ามากที่จะแสดงการลดด้วยแปดสาย ...
Ekaterina Ivanova iceja.net

0

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

list.stream().reduce(identity,
                     accumulator,
                     combiner);

สร้างผลลัพธ์เช่นเดียวกับ:

list.stream().map(i -> accumulator(identity, i))
             .reduce(identity,
                     combiner);

mapเคล็ดลับดังกล่าวขึ้นอยู่กับเฉพาะaccumulatorและcombinerอาจทำให้สิ่งต่าง ๆ ช้าลงมาก
Tagir Valeev

หรือเพิ่มความเร็วขึ้นอย่างมากเนื่องจากตอนนี้คุณสามารถทำให้ง่ายขึ้นaccumulatorโดยปล่อยพารามิเตอร์แรก
quiz123

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