Tại sao một bộ kết hợp cần thiết cho phương thức rút gọn chuyển đổi loại trong java 8


141

Tôi gặp khó khăn trong việc hiểu đầy đủ vai trò của phương thức combinerStreams reduce.

Ví dụ: đoạn mã sau không biên dịch:

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

Lỗi biên dịch cho biết: (đối số không khớp; int không thể chuyển đổi thành java.lang.String)

nhưng mã này không biên dịch:

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

Tôi hiểu rằng phương thức combiner được sử dụng trong các luồng song song - vì vậy trong ví dụ của tôi, nó được thêm vào hai ints tích lũy trung gian.

Nhưng tôi không hiểu tại sao ví dụ đầu tiên không biên dịch mà không có bộ kết hợp hoặc cách bộ kết hợp đang giải quyết chuyển đổi chuỗi thành int vì nó chỉ thêm hai int.

bất cứ ai có thể làm sáng tỏ về điều này?



2
aha, đó là cho các luồng song song ... Tôi gọi là trừu tượng rò rỉ!
Andy

Câu trả lời:


77

Hai và ba phiên bản đối số reducemà bạn đã cố sử dụng không chấp nhận cùng loại cho accumulator.

Hai đối số reduceđược định nghĩa là :

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

Trong trường hợp của bạn, T là Chuỗi, vì vậy BinaryOperator<T>nên chấp nhận hai đối số Chuỗi và trả về Chuỗi. Nhưng bạn truyền cho nó một int và String, dẫn đến lỗi biên dịch mà bạn gặp phải - argument mismatch; int cannot be converted to java.lang.String. Trên thực tế, tôi nghĩ rằng việc chuyển 0 làm giá trị nhận dạng cũng sai ở đây, vì một Chuỗi được mong đợi (T).

Cũng lưu ý rằng phiên bản rút gọn này xử lý luồng Ts và trả về T, vì vậy bạn không thể sử dụng nó để giảm luồng Chuỗi thành int.

Ba đối số reduceđược định nghĩa là :

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

Trong trường hợp của bạn, U là Integer và T là String, vì vậy phương thức này sẽ giảm một luồng String thành Integer.

Đối với bộ BiFunction<U,? super T,U>tích lũy, bạn có thể truyền tham số của hai loại khác nhau (U và? Super T), trong trường hợp của bạn là Integer và String. Ngoài ra, giá trị nhận dạng U chấp nhận một Số nguyên trong trường hợp của bạn, vì vậy việc chuyển nó 0 là ổn.

Một cách khác để đạt được những gì bạn muốn:

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

Ở đây loại luồng phù hợp với loại trả về reduce, vì vậy bạn có thể sử dụng hai phiên bản tham số của reduce.

Tất nhiên bạn không phải sử dụng reducetất cả:

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

8
Là một tùy chọn thứ hai trong mã cuối cùng của bạn, bạn cũng có thể sử dụng mapToInt(String::length)hơn mapToInt(s -> s.length()), không chắc chắn liệu cái này có tốt hơn cái kia không, nhưng tôi thích cái trước vì dễ đọc hơn.
skiwi

19
Nhiều người sẽ tìm thấy câu trả lời này vì họ không hiểu tại sao combinercần thiết, tại sao không có accumulatorlà đủ. Trong trường hợp đó: Bộ kết hợp chỉ cần cho các luồng song song, để kết hợp các kết quả "tích lũy" của các luồng.
ddekany

1
Tôi không thấy câu trả lời của bạn đặc biệt hữu ích - bởi vì bạn hoàn toàn không giải thích những gì bộ kết hợp nên làm và làm thế nào tôi có thể làm việc mà không có nó! Trong trường hợp của tôi, tôi muốn giảm loại T thành U nhưng không có cách nào có thể thực hiện song song cả. Nó chỉ đơn giản là không thể. Làm thế nào để bạn nói với hệ thống tôi không muốn / cần song song và do đó bỏ qua bộ kết hợp?
Zordid

@Zordid API Streams không bao gồm tùy chọn giảm loại T thành U mà không thông qua bộ kết hợp.
Eran

216

Câu trả lời của Eran đã mô tả sự khác biệt giữa các phiên bản hai-arg và ba-arg reducetrong đó phiên bản trước giảm Stream<T>xuống Ttrong khi phiên bản sau giảm Stream<T>xuống U. Tuy nhiên, nó không thực sự giải thích sự cần thiết của chức năng kết hợp bổ sung khi giảm Stream<T>xuống U.

Một trong những nguyên tắc thiết kế của API luồng là API không nên khác nhau giữa các luồng tuần tự và song song hoặc đặt một cách khác, một API cụ thể không nên ngăn luồng chạy chính xác theo tuần tự hoặc song song. Nếu lambdas của bạn có các thuộc tính phù hợp (kết hợp, không can thiệp, v.v.), một luồng chạy tuần tự hoặc song song sẽ cho kết quả tương tự.

Trước tiên chúng ta hãy xem xét phiên bản giảm hai đối số:

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

Việc thực hiện tuần tự là đơn giản. Giá trị danh tính Iđược "tích lũy" với phần tử luồng zeroth để đưa ra kết quả. Kết quả này được tích lũy với phần tử luồng đầu tiên để đưa ra kết quả khác, lần lượt được tích lũy với phần tử luồng thứ hai, v.v. Sau khi phần tử cuối cùng được tích lũy, kết quả cuối cùng được trả về.

Việc thực hiện song song bắt đầu bằng cách chia luồng thành các phân đoạn. Mỗi phân đoạn được xử lý bởi luồng riêng của nó theo kiểu tuần tự mà tôi đã mô tả ở trên. Bây giờ, nếu chúng ta có N luồng, chúng ta có N kết quả trung gian. Những điều này cần phải được giảm xuống một kết quả. Vì mỗi kết quả trung gian thuộc loại T và chúng tôi có một số kết quả, chúng tôi có thể sử dụng cùng một hàm tích lũy để giảm N kết quả trung gian đó xuống một kết quả duy nhất.

Bây giờ hãy xem xét một hoạt động giảm hai đối số giả thuyết mà giảm Stream<T>xuống U. Trong các ngôn ngữ khác, đây được gọi là thao tác "gấp" hoặc "gập sang trái" vì vậy đó là những gì tôi sẽ gọi nó ở đây. Lưu ý điều này không tồn tại trong Java.

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

(Lưu ý rằng giá trị nhận dạng Ithuộc loại U.)

Phiên bản tuần tự foldLeftgiống như phiên bản tuần tự reducengoại trừ các giá trị trung gian thuộc loại U thay vì loại T. Nhưng mặt khác thì giống nhau. (Một foldRightthao tác giả định sẽ tương tự ngoại trừ các thao tác sẽ được thực hiện từ phải sang trái thay vì từ trái sang phải.)

Bây giờ hãy xem xét phiên bản song song của foldLeft. Hãy bắt đầu bằng cách chia luồng thành các phân đoạn. Sau đó chúng ta có thể có mỗi luồng N giảm các giá trị T trong phân đoạn của nó thành N giá trị trung gian của loại U. Bây giờ thì sao? Làm thế nào để chúng ta nhận được từ N giá trị của loại U xuống một kết quả duy nhất của loại U?

Điều còn thiếu là một hàm khác kết hợp nhiều kết quả trung gian của loại U thành một kết quả duy nhất của loại U. Nếu chúng ta có một hàm kết hợp hai giá trị U thành một, điều đó đủ để giảm bất kỳ số lượng giá trị nào xuống còn một - giống như việc giảm ban đầu ở trên. Do đó, hoạt động rút gọn mang lại kết quả của một loại khác nhau cần hai chức năng:

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

Hoặc, sử dụng cú pháp Java:

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

Tóm lại, để thực hiện giảm song song thành một loại kết quả khác nhau, chúng ta cần hai hàm: một hàm tích lũy các phần tử T thành các giá trị U trung gian và thứ hai kết hợp các giá trị U trung gian thành một kết quả U duy nhất. Nếu chúng ta không chuyển loại, thì hóa ra hàm tích lũy giống như hàm combiner. Đó là lý do tại sao việc giảm xuống cùng loại chỉ có chức năng tích lũy và việc giảm xuống một loại khác đòi hỏi các chức năng tích lũy và kết hợp riêng biệt.

Cuối cùng, Java không cung cấp foldLeftfoldRighthoạt động vì chúng ngụ ý một thứ tự cụ thể của các hoạt động vốn là tuần tự. Điều này đụng độ với nguyên tắc thiết kế đã nêu ở trên về việc cung cấp các API hỗ trợ hoạt động tuần tự và song song như nhau.


7
Vì vậy, bạn có thể làm gì nếu bạn cần một foldLeftvì tính toán phụ thuộc vào kết quả trước đó và không thể song song?
amoebe

5
@amoebe Bạn có thể thực hiện FoldLeft của riêng mình bằng cách sử dụng forEachOrdered. Tuy nhiên, trạng thái trung gian phải được giữ trong một biến bị bắt.
Stuart Marks

@StuartMarks cảm ơn, cuối cùng tôi đã sử dụng jOOλ. Họ có một thực hiệnfoldLeft gọn gàng .
amoebe

1
Yêu câu trả lời này! Sửa lỗi cho tôi nếu tôi sai: điều này giải thích tại sao ví dụ chạy của OP (ví dụ thứ hai) sẽ không bao giờ gọi trình kết hợp, khi chạy, là chuỗi tuần tự.
Luigi Cortese

2
Nó giải thích hầu hết mọi thứ ... ngoại trừ: tại sao điều này nên loại trừ giảm theo tuần tự. Trong trường hợp của tôi, điều đó không thể thực hiện song song vì việc giảm của tôi làm giảm danh sách các hàm thành U bằng cách gọi từng hàm trên kết quả trung gian của kết quả trước đó. Điều này hoàn toàn không thể được thực hiện song song và không có cách nào để mô tả một chiếc lược. Tôi có thể sử dụng phương pháp nào để thực hiện điều này?
Zordid

115

Vì tôi thích hình tượng trưng và mũi tên để làm rõ các khái niệm ... hãy bắt đầu!

Từ Chuỗi đến Chuỗi (luồng liên tiếp)

Giả sử có 4 chuỗi: mục tiêu của bạn là nối các chuỗi đó thành một chuỗi. Về cơ bản, bạn bắt đầu với một loại và kết thúc với cùng một loại.

Bạn có thể đạt được điều này với

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

và điều này giúp bạn hình dung những gì đang xảy ra:

nhập mô tả hình ảnh ở đây

Hàm tích lũy chuyển đổi, từng bước một, các phần tử trong luồng (màu đỏ) của bạn thành giá trị giảm (màu xanh lá cây) cuối cùng. Hàm tích lũy chỉ đơn giản là biến đổi một Stringđối tượng thành một đối tượng khác String.

Từ chuỗi đến int (luồng song song)

Giả sử có cùng 4 chuỗi: mục tiêu mới của bạn là tính tổng độ dài của chúng và bạn muốn song song hóa luồng của mình.

Những gì bạn cần là một cái gì đó như thế này:

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

và đây là một kế hoạch của những gì đang xảy ra

nhập mô tả hình ảnh ở đây

Ở đây, hàm tích lũy (a BiFunction) cho phép bạn chuyển đổi Stringdữ liệu của mình thành intdữ liệu. Là luồng song song, nó được chia thành hai phần (màu đỏ), mỗi phần được xây dựng độc lập với nhau và tạo ra nhiều kết quả (một phần) màu cam. Xác định một bộ kết hợp là cần thiết để cung cấp quy tắc hợp nhất một phần intkết quả vào kết quả cuối cùng (màu xanh lá cây) int.

Từ Chuỗi đến int (luồng tuần tự)

Điều gì xảy ra nếu bạn không muốn song song hóa luồng của mình? Chà, dù sao cũng cần phải cung cấp một bộ kết hợp, nhưng nó sẽ không bao giờ được gọi, vì sẽ không có kết quả một phần nào được tạo ra.


7
Cảm ơn vì điều đó. Tôi thậm chí không cần đọc. Tôi ước họ vừa thêm một chức năng gấp kỳ dị.
Lodewijk Bogaards

1
@LodewijkBogaards rất vui vì nó đã giúp! JavaDoc ở đây thực sự khá khó hiểu
Luigi Cortese

@LuigiCortese Trong luồng song song, nó có luôn chia các phần tử cho các cặp không?
TheLogicGuy

1
Tôi đánh giá cao câu trả lời rõ ràng và hữu ích của bạn. Tôi muốn nhắc lại một chút về những gì bạn nói: "Chà, dù sao thì cũng cần phải cung cấp một cái lược, nhưng nó sẽ không bao giờ được gọi." Đây là một phần của chương trình chức năng Java thế giới mới dũng cảm, tôi đã được đảm bảo vô số lần, "làm cho mã của bạn ngắn gọn hơn và dễ đọc hơn". Chúng ta hãy hy vọng rằng các ví dụ về (trích dẫn ngón tay) ngắn gọn rõ ràng như điều này vẫn còn ít và xa giữa.
dnuttle

Sẽ tốt hơn nhiều khi minh họa giảm với tám chuỗi ...
Ekaterina Ivanova iceja.net

0

Không có giảm phiên bản đó có hai loại khác nhau mà không có một bộ kết hợp vì nó không thể được thực hiện song song (không chắc chắn lý do tại sao đây là một yêu cầu). Việc tích lũy phải kết hợp làm cho giao diện này trở nên vô dụng kể từ:

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

Tạo ra kết quả tương tự như:

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

mapThủ thuật như vậy tùy thuộc vào cụ thể accumulatorcombinercó thể làm chậm mọi thứ khá nhiều.
Tagir Valeev

Hoặc, tăng tốc đáng kể vì bây giờ bạn có thể đơn giản hóa accumulatorbằng cách bỏ tham số đầu tiên.
quiz123

Giảm song song là có thể, nó phụ thuộc vào tính toán của bạn. Trong trường hợp của bạn, bạn phải nhận thức được sự phức tạp của bộ kết hợp mà còn tích lũy trên danh tính so với các trường hợp khác.
LoganMzz
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.