Luồng Java 8 - thu thập so với giảm


143

Khi nào bạn sẽ sử dụng collect()vs reduce()? Có ai có những ví dụ cụ thể, tốt khi nào thì tốt hơn là đi bằng cách này hay cách khác?

Javadoc đề cập rằng thu thập () là một sự giảm đột biến .

Cho rằng đó là một mức giảm có thể thay đổi, tôi cho rằng nó đòi hỏi phải đồng bộ hóa (bên trong), do đó, có thể gây bất lợi cho hiệu suất. Có lẽ reduce()là dễ dàng song song hơn với chi phí phải tạo ra một cấu trúc dữ liệu mới để trả về sau mỗi bước giảm.

Tuy nhiên, các tuyên bố trên chỉ là phỏng đoán và tôi muốn một chuyên gia bấm chuông ở đây.


1
Phần còn lại của trang bạn liên kết để giải thích nó: Cũng như với less (), một lợi ích của việc thể hiện thu thập theo cách trừu tượng này là nó có thể trực tiếp song song hóa: chúng ta có thể tích lũy song song một phần và sau đó kết hợp chúng, miễn là các chức năng tích lũy và kết hợp đáp ứng các yêu cầu thích hợp.
JB Nizet

1
cũng xem "Luồng trong Java 8: Giảm so với thu thập" của Angelika Langer - youtube.com/watch?v=oWlWEKNM5Aw
MasterJoe2

Câu trả lời:


115

reducelà một hoạt động " gấp ", nó áp dụng một toán tử nhị phân cho mỗi phần tử trong luồng trong đó đối số đầu tiên cho toán tử là giá trị trả về của ứng dụng trước và đối số thứ hai là phần tử luồng hiện tại.

collectlà một hoạt động tổng hợp trong đó một "bộ sưu tập" được tạo và mỗi phần tử được "thêm" vào bộ sưu tập đó. Bộ sưu tập trong các phần khác nhau của luồng sau đó được thêm vào với nhau.

Các tài liệu mà bạn liên kết đưa ra lý do cho việc có hai cách tiếp cận khác nhau:

Nếu chúng ta muốn lấy một chuỗi các chuỗi và nối chúng thành một chuỗi dài duy nhất, chúng ta có thể đạt được điều này với mức giảm thông thường:

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

Chúng tôi sẽ nhận được kết quả mong muốn, và nó thậm chí sẽ hoạt động song song. Tuy nhiên, chúng tôi có thể không hài lòng về hiệu suất! Việc triển khai như vậy sẽ thực hiện rất nhiều việc sao chép chuỗi và thời gian chạy sẽ là O (n ^ 2) về số lượng ký tự. Một cách tiếp cận hiệu quả hơn sẽ là tích lũy kết quả vào StringBuilder, đây là một thùng chứa có thể thay đổi để tích lũy chuỗi. Chúng ta có thể sử dụng cùng một kỹ thuật để song song hóa việc giảm đột biến như chúng ta làm với việc giảm thông thường.

Vì vậy, vấn đề là sự song song giống nhau trong cả hai trường hợp nhưng trong reducetrường hợp chúng ta áp dụng hàm cho chính các phần tử luồng. Trong collecttrường hợp chúng ta áp dụng hàm cho một thùng chứa có thể thay đổi.


1
Nếu đây là trường hợp thu thập: "Cách tiếp cận hiệu quả hơn sẽ là tích lũy kết quả vào StringBuilder" thì tại sao chúng ta lại sử dụng giảm?
jimhooker2002

2
@ Jimhooker2002 đọc lại nó. Nếu bạn là, tính toán sản phẩm thì chức năng khử đơn giản có thể được áp dụng cho các luồng phân tách song song và sau đó kết hợp với nhau ở cuối. Quá trình giảm luôn dẫn đến loại là luồng. Thu thập được sử dụng khi bạn muốn thu thập kết quả vào một thùng chứa có thể thay đổi, tức là khi kết quả là một loại khác với luồng. Điều này có lợi thế là một thể hiện duy nhất của container có thể được sử dụng cho mỗi luồng phân tách nhưng nhược điểm mà các container cần được kết hợp ở cuối.
Boris the Spider

1
@ jimhooker2002 trong ví dụ về sản phẩm, intbất biến nên bạn không thể dễ dàng sử dụng thao tác thu thập. Bạn có thể thực hiện một hack bẩn như sử dụng một AtomicIntegerhoặc một số tùy chỉnh IntWrappernhưng tại sao bạn lại như vậy? Một hoạt động gấp chỉ đơn giản là khác với một hoạt động thu thập.
Boris the Spider

17
Ngoài ra còn có một reducephương thức khác , nơi bạn có thể trả về các đối tượng loại khác với các thành phần của luồng.
damluar

1
một trường hợp nữa mà bạn sẽ sử dụng thu thập thay vì giảm là khi hoạt động giảm liên quan đến việc thêm các phần tử vào một bộ sưu tập, sau đó mỗi khi hàm tích lũy của bạn xử lý một phần tử, nó tạo ra một bộ sưu tập mới bao gồm phần tử không hiệu quả.
raghu

40

Lý do đơn giản là:

  • collect() chỉ có thể làm việc với các đối tượng kết quả có thể thay đổi .
  • reduce()được thiết kế để làm việc với các đối tượng kết quả bất biến .

" reduce()Với bất di bất dịch" dụ

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()Với có thể thay đổi" dụ

Ví dụ nếu bạn muốn tự tính toán một khoản tiền sử dụng collect()nó có thể không làm việc với BigDecimalnhưng chỉ với MutableInttừ org.apache.commons.lang.mutableví dụ. Xem:

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

Này hoạt động vì ắc container.add(employee.getSalary().intValue()); là không được phép trả lại một đối tượng mới với kết quả nhưng để thay đổi trạng thái của thể thay đổi containerkiểu MutableInt.

Nếu bạn muốn sử dụng BigDecimalthay vì containerbạn không thể sử dụng collect()phương pháp như container.add(employee.getSalary());sẽ không thay đổi containerBigDecimalnó là bất biến. (Ngoài việc này BigDecimal::newsẽ không hoạt động vì BigDecimalkhông có nhà xây dựng trống)


2
Lưu ý rằng bạn đang sử dụng hàm Integertạo ( new Integer(6)), không dùng trong các phiên bản Java sau này.
MC Hoàng đế

1
Bắt tốt @MCEmaoh! Tôi đã đổi nó thànhInteger.valueOf(6)
Sandro

@Sandro - Tôi bối rối. Tại sao bạn nói rằng coll () chỉ hoạt động với các đối tượng có thể thay đổi? Tôi đã sử dụng nó để nối chuỗi. Chuỗi allNames = staff.stream () .map (Employee :: getNameString) .collect (Collector.joining (",")) .toString ();
MasterJoe2

1
@ MasterJoe2 Thật đơn giản. Nói tóm lại - việc thực hiện vẫn sử dụng StringBuildercái có thể thay đổi. Xem: hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/share/ trộm
Sandro

30

Việc giảm bình thường có nghĩa là kết hợp hai giá trị bất biến như int, double, v.v. và tạo ra một giá trị mới; đó là một sự giảm thiểu bất biến . Ngược lại, phương pháp thu thập được thiết kế để biến đổi một thùng chứa để tích lũy kết quả mà nó được cho là tạo ra.

Để minh họa vấn đề, giả sử bạn muốn đạt được Collectors.toList()bằng cách sử dụng một cách giảm đơn giản như

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

Điều này là tương đương với Collectors.toList(). Tuy nhiên, trong trường hợp này bạn đột biến List<Integer>. Như chúng ta biết ArrayListlà không an toàn luồng, cũng không an toàn để thêm / xóa giá trị khỏi nó trong khi lặp lại, do đó bạn sẽ có ngoại lệ đồng thời ArrayIndexOutOfBoundsExceptionhoặc bất kỳ loại ngoại lệ nào (đặc biệt là khi chạy song song) khi bạn cập nhật danh sách hoặc trình kết hợp cố gắng hợp nhất các danh sách vì bạn đang thay đổi danh sách bằng cách tích lũy (thêm) các số nguyên vào danh sách đó. Nếu bạn muốn làm cho chủ đề này an toàn, bạn cần phải vượt qua một danh sách mới mỗi lần sẽ làm giảm hiệu suất.

Ngược lại, các Collectors.toList()công trình trong một thời trang tương tự. Tuy nhiên, nó đảm bảo an toàn luồng khi bạn tích lũy các giá trị vào danh sách. Từ tài liệu cho collectphương pháp :

Thực hiện thao tác giảm có thể thay đổi trên các phần tử của luồng này bằng Collector. Nếu luồng song song và Collector đồng thời và luồng không được sắp xếp hoặc collector không được sắp xếp, thì việc giảm đồng thời sẽ được thực hiện. Khi được thực hiện song song, nhiều kết quả trung gian có thể được khởi tạo, điền và hợp nhất để duy trì sự cô lập của các cấu trúc dữ liệu có thể thay đổi. Do đó, ngay cả khi được thực thi song song với các cấu trúc dữ liệu không an toàn luồng (như ArrayList), không cần đồng bộ hóa bổ sung để giảm song song.

Để trả lời câu hỏi của bạn:

Khi nào bạn sẽ sử dụng collect()vs reduce()?

nếu bạn có giá trị bất biến như ints, doubles, Stringssau đó giảm bình thường làm việc tốt. Tuy nhiên, nếu bạn phải reducenói các giá trị của mình List(cấu trúc dữ liệu có thể thay đổi) thì bạn cần sử dụng collectphương pháp giảm đột biến với phương thức.


Trong đoạn mã tôi nghĩ vấn đề là nó sẽ lấy danh tính (trong trường hợp này là một phiên bản duy nhất của ArrayList) và giả sử nó là "bất biến" để họ có thể bắt đầu các xluồng, mỗi "thêm vào danh tính" sau đó kết hợp với nhau. Ví dụ tốt.
rogerdpack

tại sao chúng ta sẽ có ngoại lệ sửa đổi đồng thời, các luồng gọi sẽ chỉ truy xuất luồng nối tiếp và điều đó có nghĩa là nó sẽ được xử lý bởi một luồng và chức năng kết hợp hoàn toàn không được gọi?
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); } }tôi đã thử và không nhận được ngoại lệ CCm
amarnath

@amarnathharish sự cố xảy ra khi bạn cố chạy song song và nhiều luồng cố gắng truy cập vào cùng một danh sách
george

11

Đặt luồng là một <- b <- c <- d

Trong giảm

bạn sẽ có ((a # b) # c) # d

trong đó # là hoạt động thú vị mà bạn muốn làm.

Trong bộ sưu tập,

người sưu tầm của bạn sẽ có một số loại cấu trúc thu thập K.

K tiêu thụ a. K sau đó tiêu thụ b. K sau đó tiêu thụ c. K sau đó tiêu thụ d.

Cuối cùng, bạn hỏi K kết quả cuối cùng là gì.

K sau đó đưa nó cho bạn.


2

Chúng rất khác nhau về dấu chân bộ nhớ tiềm năng trong thời gian chạy. Trong khi collect()thu thập và đưa tất cả dữ liệu vào bộ sưu tập,reduce() rõ ràng yêu cầu bạn chỉ định cách giảm dữ liệu đã thực hiện qua luồng.

Ví dụ: nếu bạn muốn đọc một số dữ liệu từ một tệp, xử lý nó và đưa nó vào một số cơ sở dữ liệu, bạn có thể kết thúc với mã luồng java tương tự như sau:

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

Trong trường hợp này, chúng tôi sử dụng collect()để buộc java truyền dữ liệu qua và làm cho nó lưu kết quả vào cơ sở dữ liệu. Không có collect()dữ liệu thì không bao giờ được đọc và không bao giờ được lưu trữ.

Mã này vui vẻ tạo ra java.lang.OutOfMemoryError: Java heap spacelỗi thời gian chạy, nếu kích thước tệp đủ lớn hoặc kích thước heap đủ thấp. Lý do rõ ràng là nó cố gắng xếp chồng tất cả dữ liệu đã tạo ra thông qua luồng (và trên thực tế, đã được lưu trữ trong cơ sở dữ liệu) vào bộ sưu tập kết quả và điều này làm nổ tung cả đống.

Tuy nhiên, nếu bạn thay thế collect()bằng reduce()- nó sẽ không còn là vấn đề nữa vì sau này sẽ giảm và loại bỏ tất cả dữ liệu đã thực hiện.

Trong ví dụ được trình bày, chỉ cần thay thế collect()bằng một cái gì đó bằng reduce:

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

Thậm chí bạn không cần quan tâm để thực hiện phép tính phụ thuộc vào resultvì Java không phải là ngôn ngữ thuần túy (lập trình chức năng) và không thể tối ưu hóa dữ liệu không được sử dụng ở cuối luồng vì các tác dụng phụ có thể xảy ra .


3
Nếu bạn không quan tâm đến kết quả lưu db của mình, bạn nên sử dụng forEach ... bạn không cần sử dụng giảm. Trừ khi điều này là cho mục đích minh họa.
DaveEdelstein

2

Đây là ví dụ mã

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 (tổng hợp);

Đây là kết quả thực hiện:

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

Hàm giảm xử lý hai tham số, tham số đầu tiên là giá trị trả về trước đó trong luồng, tham số thứ hai là giá trị tính toán hiện tại trong luồng, nó tổng giá trị đầu tiên và giá trị hiện tại làm giá trị đầu tiên trong phép tính tiếp theo.


0

Theo các tài liệu

Các bộ thu giảm () hữu ích nhất khi được sử dụng trong việc giảm đa cấp, hạ lưu của groupingBy hoặc phân vùngBy. Để thực hiện giảm đơn giản trên luồng, thay vào đó, hãy sử dụng Stream.reduce (BinaryOperator).

Vì vậy, về cơ bản, bạn reducing()chỉ sử dụng khi bị bắt buộc trong một bộ sưu tập. Đây là một ví dụ khác :

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

Theo hướng dẫn này giảm đôi khi kém hiệu quả

Các hoạt động giảm luôn trả về một giá trị mới. Tuy nhiên, hàm tích lũy cũng trả về một giá trị mới mỗi khi nó xử lý một phần tử của luồng. Giả sử rằng bạn muốn giảm các thành phần của luồng thành một đối tượng phức tạp hơn, chẳng hạn như bộ sưu tập. Điều này có thể cản trở hiệu suất của ứng dụng của bạn. Nếu hoạt động rút gọn của bạn liên quan đến việc thêm các phần tử vào một bộ sưu tập, thì mỗi khi hàm tích lũy của bạn xử lý một phần tử, nó sẽ tạo ra một bộ sưu tập mới bao gồm phần tử, không hiệu quả. Thay vào đó, sẽ hiệu quả hơn khi bạn cập nhật một bộ sưu tập hiện có. Bạn có thể làm điều này với phương thức Stream.collect, phần tiếp theo mô tả ...

Vì vậy, danh tính được "tái sử dụng" trong kịch bản rút gọn, vì vậy sẽ hiệu quả hơn một chút .reducenếu có thể.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.