Java 8 Streams - รวบรวม vs ลด


143

เมื่อคุณจะใช้collect()VS reduce()? มีใครบ้างที่เป็นตัวอย่างที่ดีและเป็นรูปธรรมว่าเมื่อใดควรไปทางเดียวหรือดีกว่า

javadoc กล่าวว่าการเก็บรวบรวม () การลดลงแน่นอน

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

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


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

1
โปรดดูที่ "สตรีมใน Java 8: ลดกับการสะสม" โดย Angelika Langer - youtube.com/watch?v=oWlWEKNM5Aw
MasterJoe

คำตอบ:


115

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

collectเป็นการดำเนินการรวมที่ "คอลเลกชัน" ถูกสร้างขึ้นและแต่ละองค์ประกอบจะ "เพิ่ม" ในคอลเลกชันนั้น คอลเลกชันในส่วนต่างๆของสตรีมจะถูกรวมเข้าด้วยกัน

เอกสารที่คุณเชื่อมโยงให้เหตุผลสำหรับการมีสองวิธีที่แตกต่างกัน:

หากเราต้องการที่จะนำสายอักขระและเชื่อมต่อกันเป็นสายยาวเดียวเราสามารถบรรลุสิ่งนี้ด้วยการลดธรรมดา:

 String concatenated = strings.reduce("", String::concat)  

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

ดังนั้นประเด็นก็คือการขนานกันในทั้งสองกรณี แต่ในreduceกรณีที่เราใช้ฟังก์ชั่นกับองค์ประกอบกระแสตัวเอง ในcollectกรณีที่เราใช้ฟังก์ชั่นกับภาชนะที่ไม่แน่นอน


1
หากเป็นกรณีสำหรับการรวบรวม: "วิธีการที่มีประสิทธิภาพมากกว่านั้นคือการรวบรวมผลลัพธ์ลงใน StringBuilder" แล้วทำไมเราถึงต้องใช้การลดลง
jimhooker2002

2
@ Jimhooker2002 อ่านซ้ำ หากคุณพูดการคำนวณผลิตภัณฑ์จากนั้นฟังก์ชั่นการลดสามารถนำไปใช้กับสตรีมแบบแยกในแบบขนานและรวมเข้าด้วยกันในตอนท้าย กระบวนการลดขนาดจะส่งผลให้เกิดประเภทเป็นสตรีมเสมอ การรวบรวมจะใช้เมื่อคุณต้องการรวบรวมผลลัพธ์ลงในคอนเทนเนอร์ที่ไม่แน่นอนเช่นเมื่อผลลัพธ์นั้นเป็นประเภทที่แตกต่างกันไปยังสตรีม นี่เป็นข้อได้เปรียบที่สามารถใช้อินสแตนซ์เดียวของคอนเทนเนอร์สำหรับแต่ละสตรีมที่แยกได้ แต่ข้อเสียที่ต้องรวมกันในตอนท้าย
Boris the Spider

1
@ jimhooker2002 ในตัวอย่างผลิตภัณฑ์ที่intเป็นไม่เปลี่ยนรูปดังนั้นคุณไม่สามารถได้อย่างง่ายดายใช้การดำเนินการเก็บรวบรวม คุณสามารถแฮ็คสกปรกเช่นใช้งานAtomicIntegerหรือกำหนดเองIntWrapperแต่ทำไมคุณ การดำเนินการพับนั้นแตกต่างจากการดำเนินการรวบรวม
Boris the Spider

17
นอกจากนี้ยังมีreduceวิธีอื่นที่คุณสามารถส่งคืนวัตถุประเภทแตกต่างจากองค์ประกอบของกระแส
damluar

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

40

เหตุผลก็คือ:

  • collect() สามารถทำงานกับวัตถุผลลัพธ์ที่ไม่แน่นอน
  • reduce()ถูกออกแบบมาเพื่อทำงานกับวัตถุผลลัพธ์ที่ไม่เปลี่ยนรูป

ตัวอย่าง " reduce()กับไม่เปลี่ยนรูป"

public class Employee {
  private Integer salary;
  public Employee(String aSalary){
    this.salary = new Integer(aSalary);
  }
  public Integer getSalary(){
    return this.salary;
  }
}

@Test
public void testReduceWithImmutable(){
  List<Employee> list = new LinkedList<>();
  list.add(new Employee("1"));
  list.add(new Employee("2"));
  list.add(new Employee("3"));

  Integer sum = list
  .stream()
  .map(Employee::getSalary)
  .reduce(0, (Integer a, Integer b) -> Integer.sum(a, b));

  assertEquals(Integer.valueOf(6), sum);
}

ตัวอย่าง " collect()with mutable"

เช่นหากคุณต้องการคำนวณผลรวมด้วยตนเองการใช้งานcollect()จะไม่สามารถใช้งานได้BigDecimalแต่MutableIntจากorg.apache.commons.lang.mutableตัวอย่างเช่น ดู:

public class Employee {
  private MutableInt salary;
  public Employee(String aSalary){
    this.salary = new MutableInt(aSalary);
  }
  public MutableInt getSalary(){
    return this.salary;
  }
}

@Test
public void testCollectWithMutable(){
  List<Employee> list = new LinkedList<>();
  list.add(new Employee("1"));
  list.add(new Employee("2"));

  MutableInt sum = list.stream().collect(
    MutableInt::new, 
    (MutableInt container, Employee employee) -> 
      container.add(employee.getSalary().intValue())
    , 
    MutableInt::add);
  assertEquals(new MutableInt(3), sum);
}

นี้ทำงานได้เพราะสะสม container.add(employee.getSalary().intValue());ไม่ควรจะกลับวัตถุใหม่กับผล แต่ที่จะเปลี่ยนสถานะของไม่แน่นอนประเภทcontainerMutableInt

หากคุณต้องการที่จะใช้BigDecimalแทนสำหรับcontainerคุณไม่สามารถใช้collect()วิธีการที่container.add(employee.getSalary());จะไม่เปลี่ยนcontainerเพราะBigDecimalมันไม่เปลี่ยนรูป (นอกเหนือจากนี้BigDecimal::newจะไม่ทำงานเนื่องจากBigDecimalไม่มีตัวสร้างที่ว่างเปล่า)


2
โปรดทราบว่าคุณกำลังใช้IntegerConstructor ( new Integer(6)) ซึ่งเลิกใช้แล้วในเวอร์ชัน Java รุ่นใหม่กว่า
MC Emperor

1
ดี @MCEm Emperor! ฉันเปลี่ยนเป็นInteger.valueOf(6)
Sandro

@Sandro - ฉันสับสน ทำไมคุณถึงบอกว่า collect () ทำงานได้เฉพาะกับวัตถุที่ไม่แน่นอน ฉันใช้มันเพื่อเชื่อมสตริง String allNames = employee.stream () .map (Employee :: getNameString) .collect (Collector.joining (",")) .toString ();
MasterJoe

1
@ MasterJoe2 มันง่าย ในระยะสั้น - การใช้งานยังคงใช้StringBuilderซึ่งไม่แน่นอน ดู: hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/share/ …
Sandro

30

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

เพื่อแสดงปัญหาลองสมมติว่าคุณต้องการบรรลุCollectors.toList()โดยใช้การลดแบบง่ายๆ

List<Integer> numbers = stream.reduce(
        new ArrayList<Integer>(),
        (List<Integer> l, Integer e) -> {
            l.add(e);
            return l;
        },
        (List<Integer> l1, List<Integer> l2) -> {
            l1.addAll(l2);
            return l1;
        });

Collectors.toList()นี่คือเทียบเท่า List<Integer>แต่ในกรณีนี้คุณกลายพันธุ์ ดังที่เราทราบว่าArrayListไม่ปลอดภัยต่อเธรดและไม่ปลอดภัยที่จะเพิ่ม / ลบค่าออกจากมันในขณะที่วนซ้ำดังนั้นคุณจะได้รับข้อยกเว้นพร้อมกันหรือข้อยกเว้นArrayIndexOutOfBoundsExceptionชนิดใด ๆ (โดยเฉพาะอย่างยิ่งเมื่อทำงานแบบขนาน) เมื่อคุณอัปเดตรายการหรือ combiner พยายามที่จะรวมรายการเพราะคุณกำลังกลายพันธุ์รายการโดยการสะสม (เพิ่ม) จำนวนเต็มไป ถ้าคุณต้องการทำให้เธรดนี้ปลอดภัยคุณต้องผ่านรายการใหม่ทุกครั้งที่ทำให้ประสิทธิภาพลดลง

ในทางตรงกันข้ามการCollectors.toList()ทำงานในลักษณะที่คล้ายกัน อย่างไรก็ตามมันรับประกันความปลอดภัยของด้ายเมื่อคุณสะสมค่าลงในรายการ จากเอกสารสำหรับcollectวิธีการ :

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

ดังนั้นเพื่อตอบคำถามของคุณ:

เมื่อคุณจะใช้collect()VS reduce()?

ถ้าคุณมีค่าไม่เปลี่ยนรูปเช่นints, doubles, Stringsแล้วลดปกติทำงานได้ดี อย่างไรก็ตามหากคุณจำเป็นต้องreduceพูดถึง a List(โครงสร้างข้อมูลที่ไม่แน่นอน) คุณต้องใช้การลดที่ไม่แน่นอนกับcollectวิธีการ


ในข้อมูลโค้ดฉันคิดว่าปัญหาคือตัวตน (ในกรณีนี้เป็นอินสแตนซ์เดียวของ ArrayList) และคิดว่ามันเป็น "ไม่เปลี่ยนรูป" เพื่อให้พวกเขาสามารถเริ่มxหัวข้อ ตัวอย่างที่ดี
rogerdpack

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

public static void main(String[] args) { List<Integer> l = new ArrayList<>(); l.add(1); l.add(10); l.add(3); l.add(-3); l.add(-4); List<Integer> numbers = l.stream().reduce( new ArrayList<Integer>(), (List<Integer> l2, Integer e) -> { l2.add(e); return l2; }, (List<Integer> l1, List<Integer> l2) -> { l1.addAll(l2); return l1; });for(Integer i:numbers)System.out.println(i); } }ฉันพยายามและไม่ได้รับข้อยกเว้น
CCm

@amarnathharish ปัญหาเกิดขึ้นเมื่อคุณพยายามเรียกใช้ในแบบคู่ขนานและหลายเธรดพยายามเข้าถึงรายการเดียวกัน
george

11

ให้กระแสเป็น <- b <- c <- d

ในการลด

คุณจะมี ((a # b) # c) # d

โดยที่ # คือการดำเนินการที่น่าสนใจที่คุณต้องการทำ

ในการสะสม

นักสะสมของคุณจะมีโครงสร้างการรวบรวมบางอย่างเค

K สิ้นเปลือง K จึงสิ้นเปลืองข. K จึงสิ้นเปลือง c. K จากนั้นใช้ d

ในตอนท้ายคุณถาม K ว่าผลลัพธ์สุดท้ายคืออะไร

K จากนั้นมอบให้คุณ


2

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

ตัวอย่างเช่นหากคุณต้องการอ่านข้อมูลบางอย่างจากไฟล์ประมวลผลและใส่ลงในฐานข้อมูลบางอย่างคุณอาจท้ายด้วยรหัส java stream คล้ายกับสิ่งนี้:

streamDataFromFile(file)
            .map(data -> processData(data))
            .map(result -> database.save(result))
            .collect(Collectors.toList());

ในกรณีนี้เราใช้collect()บังคับให้จาวาทำการสตรีมข้อมูลผ่านและทำให้มันบันทึกผลลัพธ์ลงในฐานข้อมูล ไม่มีcollect()ข้อมูลจะไม่อ่านและไม่เคยเก็บไว้

รหัสนี้สร้างjava.lang.OutOfMemoryError: Java heap spaceข้อผิดพลาดรันไทม์อย่างมีความสุขหากขนาดไฟล์ใหญ่พอหรือขนาดฮีปต่ำพอ เหตุผลที่ชัดเจนคือพยายามรวบรวมข้อมูลทั้งหมดที่สร้างผ่านสตรีม (และที่จริงแล้วมีการจัดเก็บไว้ในฐานข้อมูลแล้ว) ลงในคอลเลกชันที่เป็นผลลัพธ์

อย่างไรก็ตามหากคุณแทนที่collect()ด้วยreduce()- มันจะไม่เป็นปัญหาอีกต่อไปเพราะหลังจะลดและยกเลิกข้อมูลทั้งหมดที่ทำไว้

ในตัวอย่างที่นำเสนอให้แทนที่collect()ด้วยบางสิ่งด้วยreduce:

.reduce(0L, (aLong, result) -> aLong, (aLong1, aLong2) -> aLong1);

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


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

2

นี่คือตัวอย่างรหัส

List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
int sum = list.stream().reduce((x,y) -> {
        System.out.println(String.format("x=%d,y=%d",x,y));
        return (x + y);
    }).get();

System.out.println (ผลรวม);

นี่คือผลการดำเนินการ:

x=1,y=2
x=3,y=3
x=6,y=4
x=10,y=5
x=15,y=6
x=21,y=7
28

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


0

ตามเอกสาร

ตัวรวบรวมการลด () จะมีประโยชน์มากที่สุดเมื่อใช้ในการลดหลายระดับดาวน์สตรีมของการจัดกลุ่มโดยหรือแบ่งเป็นโดย หากต้องการลดการสตรีมแบบง่ายให้ใช้ Stream.reduce (BinaryOperator) แทน

ดังนั้นโดยทั่วไปคุณจะต้องใช้reducing()ก็ต่อเมื่อถูกบังคับภายในการรวบรวม นี่เป็นอีกตัวอย่าง :

 For example, given a stream of Person, to calculate the longest last name 
 of residents in each city:

    Comparator<String> byLength = Comparator.comparing(String::length);
    Map<String, String> longestLastNameByCity
        = personList.stream().collect(groupingBy(Person::getCity,
            reducing("", Person::getLastName, BinaryOperator.maxBy(byLength))));

ตามการลดการสอนนี้บางครั้งมีประสิทธิภาพน้อยกว่า

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

ดังนั้นข้อมูลประจำตัวคือ "ใช้ซ้ำ" ในสถานการณ์ที่ลดลงดังนั้นจึงมีประสิทธิภาพมากกว่า.reduceหากเป็นไปได้

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