Java 8'de türü dönüştüren azaltma yöntemi için neden bir birleştirici gereklidir?


142

combinerAkışlar reduceyönteminde yerine getirdiği rolü tam olarak anlamakta zorlanıyorum .

Örneğin, aşağıdaki kod derlenmez:

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

Derleme hatası: (bağımsız değişken uyuşmazlığı; int, java.lang.String biçimine dönüştürülemez)

ancak bu kod derlenir:

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

Birleştirici yönteminin paralel akışlarda kullanıldığını anlıyorum - bu yüzden örneğimde iki ara birikmiş int ekliyor.

Ama ilk örneğin neden birleştirici olmadan derlemediğini veya birleştiricinin dize int'e dönüşümünü nasıl çözdüğünü anlamıyorum çünkü sadece iki int ekliyor.

Herkes buna ışık tutabilir mi?



2
aha, paralel akışlar için ... Sızdıran soyutlama diyorum!
Andy

Yanıtlar:


77

Kullanmaya reduceçalıştığınız iki ve üç bağımsız değişken sürümü, için aynı türü kabul etmez accumulator.

İki argüman reduceşu şekilde tanımlanır :

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

Sizin durumunuzda, T String'dir, bu nedenle BinaryOperator<T>iki String argümanı kabul etmeli ve bir String döndürmelidir. Ama bir int ve bir String geçirirsiniz, bu da - derleme hatasıyla sonuçlanır argument mismatch; int cannot be converted to java.lang.String. Aslında, ben bir String bekleniyor (T) çünkü kimlik değeri olarak 0 geçen burada da yanlış olduğunu düşünüyorum.

Ayrıca, bu azaltma sürümünün bir Ts akışını işlediğini ve bir T döndürdüğünü unutmayın, bu nedenle bir dize akışını int'e azaltmak için kullanamazsınız.

Üç argüman reduceşu şekilde tanımlanır :

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

Sizin durumunuzda U Tamsayı ve T Dize'dir, bu nedenle bu yöntem Dize akışını bir Tamsayıya indirecektir.

İçin BiFunction<U,? super T,U>akümülatör size durumda Tamsayı ve Dize iki farklı tipte (U ve? Süper T) parametrelerini geçebilir. Buna ek olarak, U kimlik değeri sizin durumunuzda bir Tamsayı kabul eder, bu yüzden 0'ı geçmek iyidir.

İstediğinizi başarmanın başka bir yolu:

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

Burada akış türü, dönüş türüyle eşleşir reduce, bu nedenle iki parametre sürümünü kullanabilirsiniz reduce.

Tabii ki kullanmak zorunda değilsiniz reduce:

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

8
Son kodunda bir ikinci seçenek olarak, aynı zamanda kullanabilirsiniz mapToInt(String::length)üzerinde mapToInt(s -> s.length())bir diğeri üzerinde daha iyi olurdu eğer emin, ama okunabilmesi için eski tercih ederim.
skiwi

20
Birçoğu bu cevabı neden combinergerekli olduğunu, neden sahip olmanın accumulatoryeterli olmadığını anlamıyorlar . Bu durumda: Birleştirici sadece paralel akışlar için, ipliklerin "birikmiş" sonuçlarını birleştirmek için gereklidir.
ddekany

1
Cevabınızı özellikle yararlı bulmuyorum - çünkü birleştiricinin ne yapması gerektiğini ve onsuz nasıl çalışabileceğimi hiç açıklamıyorsunuz! Benim durumumda, T tipini U'ya düşürmek istiyorum, ancak bunun paralel olarak yapılmasının hiçbir yolu yok. Bu mümkün değil. Sisteme paralellik istemediğimi / ihtiyacım olmadığını nasıl anlarsınız ve böylece birleştiriciyi dışarıda bırakırsınız?
Zordid

@Zordid the Streams API, birleştiriciyi geçmeden T türünü bir U'ya azaltma seçeneği içermez.
Eran

216

Eran cevabı iki-arg ve üç arg sürümleri arasındaki farkların açıklamasını reduceeski azalttığı içinde Stream<T>hiç Tikincisi azaltır oysa Stream<T>etmek U. Azaltılması Ancak, aslında ek birleştirici fonksiyonu olan ihtiyacı açıklama yapmadı Stream<T>etmek U.

Akış API'sinin tasarım ilkelerinden biri, API'nin sıralı ve paralel akışlar arasında farklılık göstermemesi veya başka bir yol koymasıyla, belirli bir API'nin bir akışın sırayla veya paralel olarak düzgün çalışmasını engellememesidir. Lambdalarınız doğru özelliklere (çağrışımsal, etkileşmeyen vb.) Sahipse, sırayla veya paralel olarak çalışan bir akış aynı sonuçları vermelidir.

İlk önce indirgemenin iki arg versiyonunu düşünelim:

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

Sıralı uygulama basittir. Kimlik değeri Ibir sonuç vermek için sıfırıncı akım elemanı ile "birikir". Bu sonuç, ikinci akış elemanı ile biriken başka bir sonuç vermek üzere birinci akış elemanı ile birikir ve bu böyle devam eder. Son eleman toplandıktan sonra nihai sonuç döndürülür.

Paralel uygulama, akışı segmentlere bölerek başlar. Her bölüm, yukarıda tarif ettiğim sıralı bir şekilde kendi ipliği ile işlenir. Şimdi, N iş parçacığımız varsa, N ara sonucumuz var. Bunların bir sonuca indirgenmesi gerekir. Her ara sonuç T tipinde olduğundan ve birkaç tane var, bu N ara sonuçları tek bir sonuca indirmek için aynı akümülatör işlevini kullanabiliriz.

Şimdi azaltan varsayımsal bir iki-arg azaltma işlemini düşünelim Stream<T>için U. Diğer dillerde, buna "katlama" veya "katlama sollama" işlemi denir . Bunun Java'da mevcut olmadığını unutmayın.

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

(Kimlik değerinin IU türünde olduğuna dikkat edin .)

Sıralı versiyonu , ara değerlerin T tipi yerine U foldLefttipinde olması reduceharicinde , sıralı versiyonuna benzer. Ama aksi halde aynıdır. (Varsayımsal bir foldRightişlem, işlemlerin soldan sağa yerine sağdan sola gerçekleştirilmesi dışında benzer olacaktır.)

Şimdi foldLeft. Akışı segmentlere bölerek başlayalım. Daha sonra her N iş parçacığının kendi segmentindeki T değerlerini U tipi N ara değerlerine indirmesini sağlayabiliriz. Şimdi ne olacak? U tipi N değerlerinden U tipi tek bir sonuca nasıl ulaşabiliriz?

Eksik olduğunu başka fonksiyondur birleştirir tip U. sonucunun tek tip U çoklu ara sonuçlarını biz yeterli olduğunu birine birleştiren iki U değerleri bire düştü değerlerin herhangi sayısını azaltmak için bir işlevi varsa - tıpkı yukarıdaki orijinal azalma. Bu nedenle, farklı bir tipin sonucunu veren azaltma işlemi iki işleve ihtiyaç duyar:

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

Veya Java sözdizimini kullanarak:

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

Özetle, farklı bir sonuç türü paralel bir azalma yapmak için iki fonksiyonları gerekir: bir biriken ara U değerleri T elemanları, ve bir ikinci bu biçerdöver tek bir U sonucu içine ara ürün, U-değeri. Türleri değiştirmezsek, akümülatör işlevinin birleştirici işleviyle aynı olduğu ortaya çıkar. Bu yüzden aynı tipte azaltmanın sadece akümülatör işlevi vardır ve farklı bir türe indirgemek için ayrı akümülatör ve birleştirici işlevleri gerekir.

Son olarak, Java, sunmaz foldLeftve foldRightişlem yapmaz, çünkü doğası gereği ardışık olan işlemlerin belirli bir sırasını ima ederler. Bu, sıralı ve paralel çalışmayı eşit olarak destekleyen API'ler sağlamada yukarıda belirtilen tasarım ilkesiyle çelişmektedir.


7
Peki bir ihtiyacınız varsa ne yapabilirsiniz foldLeftçünkü hesaplama önceki sonuca bağlıdır ve paralelleştirilemez?
amoebe

5
@ amoebe kullanarak kendi foldLeft uygulayabilirsiniz forEachOrdered. Bununla birlikte, ara durum yakalanan bir değişkende tutulmalıdır.
Stuart Marks

@StuartMarks teşekkürler, jOOλ kullandım. Düzgün bir uygulaması varfoldLeft .
amoebe

1
Bu yanıtı seviyorum! Yanılıyorsam beni düzeltin: Bu OP'nin çalışan örneğinin (ikincisi) neden birleştiriciyi hiçbir zaman çalıştırmazsa akış sıralaması olarak açıklamayacağını açıklar.
Luigi Cortese

2
Neredeyse her şeyi açıklıyor ... hariç: bu neden sıralı olarak azaltmayı hariç tutmalı? Benim durumumda, paralel olarak yapmak imkansızdır, çünkü indirgemem, önceki sonucun ara sonucundaki her bir işlevi çağırarak işlev listesini U'ya indirir. Bu hiç paralel olarak yapılamaz ve birleştiriciyi tanımlamanın bir yolu yoktur. Bunu başarmak için hangi yöntemi kullanabilirim?
Zordid

116

Kavramları netleştirmek için karalamalar ve okları sevdiğim için ... başlayalım!

Dizeden Dizeye (sıralı akış)

4 dizeye sahip olduğunuzu varsayalım: Amacınız bu dizeleri bir dizede birleştirmektir. Temel olarak bir tür ile başlar ve aynı türle bitirirsiniz.

Bunu ile yapabilirsiniz

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

ve bu, olanları görselleştirmenize yardımcı olur:

resim açıklamasını buraya girin

Akümülatör işlevi, adım adım (kırmızı) akışınızdaki öğeleri son azaltılmış (yeşil) değere dönüştürür. Akümülatör işlevi basitçe bir Stringnesneyi diğerine dönüştürür String.

String'den int'e (paralel akış)

Aynı 4 dizeye sahip olduğunuzu varsayalım: yeni hedefiniz uzunluklarını toplamak ve akışınızı paralelleştirmek istiyorsunuz.

İhtiyacınız olan şey şudur:

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

ve bu olanların şeması

resim açıklamasını buraya girin

Burada akümülatör işlevi (a BiFunction) Stringverilerinizi bir intverilere dönüştürmenizi sağlar . Akım paralel olarak, her biri birbirinden bağımsız olarak hazırlanan ve aynı derecede kısmi (turuncu) sonuç üreten iki (kırmızı) parçaya bölünür. Kısmi intsonuçları nihai (yeşil) olanla birleştirmek için bir kural sağlamak için bir birleştirici tanımlamak gerekir int.

String'den int'e (sıralı akış)

Akışınızı paralel hale getirmek istemiyorsanız ne olur? Yine de, bir birleştiricinin sağlanması gerekiyor, ancak kısmi sonuçların üretilmeyeceği göz önüne alındığında asla çağrılmayacak.


7
Bunun için teşekkürler. Okumam bile gerekmiyordu. Keşke onlar sadece bir ucube katlama fonksiyonu ekledi olurdu.
Lodewijk Bogaards

1
@LodewijkBogaards yardımcı oldu sevindim! JavaDoc burada oldukça şifreli
Luigi Cortese

@LuigiCortese Paralel akışta elemanları her zaman çiftlere böler mi?
TheLogicGuy

1
Açık ve yararlı cevabınızı takdir ediyorum. Söylediklerinizin bir kısmını tekrarlamak istiyorum: "Zaten bir birleştiricinin sağlanması gerekiyor, ama asla çağrılmayacak." Bu, sayısız kez "kodunuzu daha özlü ve daha kolay okunabilir hale getirdiğinden" emin olduğum Java Yeni Brave Yeni Dünya fonksiyonel programlama bölümünün bir parçasıdır. Umarım (parmak tırnak) örnekleri bunun gibi özlü netliğin az ve çok arasında kalmasını sağlar.
dnuttle

Sekiz dizeleri ile azaltmak göstermek daha iyi olacak ...
Ekaterina Ivanova iceja.net

0

Paralel olarak çalıştırılamadığından bir birleştirici olmadan iki farklı tür alan bir azaltma sürümü yoktur (bunun neden bir gereklilik olduğundan emin değilsiniz). Aslında akümülatör ilişkisel olmalıdır oldukça fazla işe yaramaz çünkü bu arayüzü yapar:

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

Şununla aynı sonuçları verir:

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

Bu maphile özellikle bağlı accumulatorve combinerişleri oldukça yavaşlatabilir.
Tagir Valeev

Veya, accumulatorilk parametreyi bırakarak basitleştirebileceğiniz için önemli ölçüde hızlandırın .
quiz123

Paralel azaltma mümkündür, hesaplamanıza bağlıdır. Sizin durumunuzda, birleştiricinin karmaşıklığının farkında olmalısınız, aynı zamanda diğer örneklere karşı kimlik konusunda da akümülatör olmalısınız.
LoganMzz
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.