Java 8 Akışları - Toplama ve Azaltma


143

collect()Vs ne zaman kullanılır reduce()? Bir şekilde ya da bu şekilde gitmenin kesinlikle daha iyi olduğu zaman iyi, somut örnekleri var mı?

Javadoc, () toplanmasının değiştirilebilir bir azalma olduğunu belirtir .

Değişken bir azalma olduğu göz önüne alındığında, bunun, performans için zararlı olabilecek senkronizasyon (dahili olarak) gerektirdiğini varsayıyorum. Büyük olasılıkla reduce(), indirgeme işleminin her adımından sonra geri dönüş için yeni bir veri yapısı oluşturmak zorunda kalmanın maliyetiyle daha kolay paralelleştirilebilir.

Yukarıdaki ifadeler ancak tahmin ve burada bir chime uzmanı isterim.


1
Bağlandığınız sayfanın geri kalanı bunu açıklıyor: reduce () ile olduğu gibi, toplama işlemini bu soyut şekilde ifade etmenin bir yararı, doğrudan paralelleştirmeye yatkın olmasıdır: kısmi sonuçları paralel olarak biriktirebilir ve daha sonra bunları birleştirebiliriz biriktirme ve birleştirme işlevleri uygun gereksinimleri karşılar.
JB Nizet

1
ayrıca bkz. Angelika Langer tarafından "Java 8'de Akışlar: Azalt ve Topla" - youtube.com/watch?v=oWlWEKNM5Aw
MasterJoe

Yanıtlar:


115

reduce" katlama " işlemidir, akıştaki her bir öğeye ikili işleç uygular; burada işleç için ilk bağımsız değişken önceki uygulamanın dönüş değeri ve ikinci bağımsız değişken geçerli akış öğesidir.

collect"koleksiyon" oluşturulduğu ve her öğenin bu koleksiyona "eklendiği" bir toplama işlemidir. Akışın farklı bölümlerindeki koleksiyonlar birlikte eklenir.

Bağladığınız belge iki farklı yaklaşıma sahip olmanızın nedenini verir:

Bir dize akışı alıp bunları tek bir uzun dize halinde birleştirmek istersek, bunu normal bir azalma ile başarabiliriz:

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

İstenilen sonucu elde ederiz ve hatta paralel olarak çalışır. Ancak, performanstan memnun olmayabiliriz! Böyle bir uygulama çok sayıda dize kopyalama yapar ve çalışma süresi karakter sayısında O (n ^ 2) olur. Daha performanslı bir yaklaşım, sonuçları dizeleri biriktirmek için değiştirilebilir bir kap olan bir StringBuilder'de biriktirmek olacaktır. Değişken indirgemeyi normal indirgeme ile paralel hale getirmek için aynı tekniği kullanabiliriz.

Buradaki nokta şudur: Paralellik her iki durumda da aynıdır, ancak reducebiz fonksiyonu akış elemanlarının kendilerine uygularız. Bu collectdurumda, işlevi değiştirilebilir bir kaba uygularız.


1
Toplama için durum buysa: "Sonuçları bir StringBuilder'da biriktirmek için daha performanslı bir yaklaşım olur" o zaman neden azaltmak gerekir?
jimhooker2002

2
@ Jimhooker2002 tekrar okur. Örneğin, ürünü hesaplıyorsanız, azaltma işlevi basitçe bölünmüş akışlara paralel olarak uygulanabilir ve daha sonra sonunda birleştirilebilir. Azaltma işlemi her zaman türün akış olarak sonucunu verir. Toplama, sonuçları değişken bir kaba toplamak istediğinizde, yani sonuç akıştan farklı bir tür olduğunda kullanılır. Bunun avantajı, her bir ayrık akım için kabın tek bir örneğinin kullanılabilmesidir, ancak kapların sonunda birleştirilmesi gereken dezavantajdır.
Örümcek Boris

1
@ ürün örneğinde jimhooker2002, intolduğu değişmez bilgileri hemen ödemeli operasyonu kullanamaması için. Bir AtomicIntegerveya bazı özel kullanmak gibi kirli bir kesmek yapabilirsiniz, IntWrapperama neden? Bir katlama işlemi, bir toplama işleminden basitçe farklıdır.
Örümcek Boris

17
reduceAkışın öğelerinden farklı türde nesneler döndürebileceğiniz başka bir yöntem de vardır .
damluar

1
u azaltma yerine toplama kullanacak bir başka durum, azaltma işleminin bir koleksiyona öğe eklemeyi içermesidir, akümülatör işleviniz her öğeyi işlediğinde, verimsiz olan öğeyi içeren yeni bir koleksiyon oluşturur.
raghu

40

Nedeni basitçe:

  • collect() Sadece çalışabilir ile kesilebilir sonuç nesneler.
  • reduce()olduğu çalışmaları için tasarlanmış olan değişmez sonuç nesneler.

" reduce()değişmez" örneği

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()değiştirilebilir" örneği

Örneğin el kullanarak bir toplamını hesaplamak istiyorsanız collect()onunla çalışma olamaz BigDecimalama sadece birlikte MutableIntgelen org.apache.commons.lang.mutableörnek. Görmek:

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

Bu, akümülatörün container.add(employee.getSalary().intValue()); sonuçla yeni bir nesne döndürmesi değil container, türün mutable durumunu değiştirmesi gerektiği için çalışır MutableInt.

Kullanmak isterseniz BigDecimalyerine containerkullanamadı size collect()olarak yöntemini container.add(employee.getSalary());değiştirmek olmaz containerçünkü BigDecimalo sabittir. (Bunun dışında boş bir kurucu BigDecimal::newolmadığı için çalışmaz BigDecimal)


2
Daha sonraki Java sürümlerinde kullanımdan kaldırılmış bir Integeryapıcı ( new Integer(6)) kullandığınızı unutmayın .
MC İmparatoru

1
İyi yakalama @MCEmperor! Bunu Integer.valueOf(6)
Sandro olarak

@Sandro - Kafam karıştı. Neden collect () işlevinin yalnızca değişken nesnelerle çalıştığını söylüyorsunuz? Dizeleri birleştirmek için kullandım. Dize allNames = çalışanları.stream () .map (Employee :: getNameString) .collect (Collectors.joining (",")) .toString ();
MasterJoe

1
@ MasterJoe2 Çok basit. Kısacası - uygulama hala StringBuilderdeğiştirilebilir olanı kullanıyor . Bakınız: hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/share/…
Sandro

30

Normal indirgeme, int, double, vb. Gibi iki değişmez değeri birleştirmek ve yeni bir değer üretmek içindir; bir var değişmez azalma. Buna karşılık, toplama yöntemi, üretmesi beklenen sonucu biriktirmek için bir kabı mutasyona uğratacak şekilde tasarlanmıştır .

Sorunu göstermek için, Collectors.toList()basit bir azaltma kullanarak elde etmek istediğinizi varsayalım.

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

Bu eşdeğerdir Collectors.toList(). Ancak, bu durumda mutasyona uğrarsınız List<Integer>. Bildiğimiz gibi ArrayList, iş parçacığı için güvenli değil veya yineleme sırasında değer eklemek / çıkarmak güvenli değildir, böylece ArrayIndexOutOfBoundsExceptionlisteyi veya birleştiriciyi güncellediğinizde eşzamanlı istisna veya herhangi bir istisna (özellikle paralel çalıştırıldığında) elde edersiniz. tamsayıları toplayarak (ekleyerek) listeyi değiştirdiğiniz için listeleri birleştirmeye çalışır. Bu iş parçacığını güvenli hale getirmek istiyorsanız, her seferinde performansı düşürecek yeni bir liste geçirmeniz gerekir.

Buna karşılık, Collectors.toList()benzer şekilde çalışır. Ancak, değerleri listede biriktirdiğinizde iplik güvenliğini garanti eder. Gönderen belgelerine collectyöntemiyle :

Bir Toplayıcı kullanarak bu akışın elemanları üzerinde değiştirilebilir bir azaltma işlemi gerçekleştirir. Akış paralelse ve Toplayıcı eşzamanlıysa ve ya akış sıralanmamışsa ya da toplayıcı sıralanmamışsa, eşzamanlı bir azalma gerçekleştirilir. Paralel olarak yürütüldüğünde, değiştirilebilir ara yapıların izolasyonunu sürdürmek için çoklu ara sonuçlar başlatılabilir, doldurulabilir ve birleştirilebilir. Bu nedenle, iş parçacığı için güvenli olmayan veri yapılarına (ArrayList gibi) paralel olarak yürütüldüğünde bile, paralel azaltma için ek senkronizasyon gerekmez.

Sorunuzu cevaplamak için:

collect()Vs ne zaman kullanılır reduce()?

Aşağıdaki gibi değişmez değerleri varsa ints, doubles, Stringso zaman normal bir azalma sadece para cezası çalışır. Bununla birlikte, reducedeğerlerinizi bir List(değiştirilebilir veri yapısı) olarak söylemek zorundaysanız , collectyöntemle değiştirilebilir azaltmayı kullanmanız gerekir .


Kod snippet'inde sorunun kimliğini (bu durumda bir ArrayList'in tek bir örneğini) alacağını ve "değişmez" olduğunu varsayarak x, her biri "kimliğe ekleyerek" sonra bir araya getirdikleri konuları değiştirebileceğini düşünüyorum. İyi örnek.
rogerdpack

neden eşzamanlı modifikasyon istisnası alacağız, akışları çağırmak sadece seri akışı yeniden başlatacak ve bu da tek iplikle işleneceği ve birleştirici işlevinin hiç çağrılmadığı anlamına geliyor?
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); } }denedim ve CCm istisna alamadım
amarnath harish

@ amarnathharish paralel olarak çalıştırmayı denediğinizde oluşur ve birden fazla iş parçacığı aynı listeye erişmeye çalışın
george

11

Akış <- b <- c <- d olsun

Azaltmada,

sahip olacaksınız ((a # b) # c) # d

burada # yapmak istediğiniz ilginç işlemdir.

Koleksiyonda,

Koleksiyoncunuzda bir çeşit toplama yapısı K olacaktır.

K tüketir a. K sonra b tüketir. Daha sonra K tüketir c. K sonra d tüketir.

Sonunda K'ya nihai sonucun ne olduğunu soruyorsun.

K sonra sana verir.


2

Çalışma zamanı sırasında potansiyel bellek ayak izinde çok farklıdırlar. Tüm verileri collect()toplar ve koleksiyona koyarken , sizden akış yoluyla veriyi nasıl azaltacağınızı açıkça belirtmenizi ister.reduce()

Örneğin, bir dosyadan bazı verileri okumak, işlemek ve bir veritabanına koymak istiyorsanız, aşağıdakine benzer java akış koduyla karşılaşabilirsiniz:

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

Bu durumda, collect()java'yı veri akışı yapmaya zorlamak ve sonucu veritabanına kaydetmesini sağlamak için kullanırız. collect()Veri olmadan asla okunmaz ve saklanmaz.

java.lang.OutOfMemoryError: Java heap spaceDosya boyutu yeterince büyükse veya yığın boyutu yeterince küçükse, bu kod mutlu bir şekilde çalışma zamanı hatası oluşturur . Bunun açık nedeni, veriyi akış yoluyla yapan (ve aslında veritabanında zaten saklanmış olan) tüm verileri elde edilen koleksiyona yığılmasıdır ve bu da yığınları havaya uçurur.

Bununla birlikte, - collect()ile değiştirirseniz , reduce()sorun artık olmaz çünkü ikincisi bunu yapan tüm verileri azaltır ve atar.

Sunulan örnekte, sadece değiştirmek collect()ile bir şey ile reduce:

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

resultJava'nın saf bir FP (işlevsel programlama) dili olmadığı ve olası yan etkiler nedeniyle akışın altında kullanılmayan verileri optimize edemediği için, hesaplamanın bağımlı olmasına dikkat etmeniz bile gerekmez . .


3
Eğer db save sonuçları umurumda değil, forEach kullanmalısınız ... azaltmak kullanmanız gerekmez. Bu açıklama amaçlı olmadığı sürece.
DaveEdelstein

2

İşte kod örneği

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

Yürütme sonucu şöyledir:

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

Fonksiyon parametresini iki parametreyi azaltın, ilk parametre akımdaki önceki dönüş değeri, ikinci parametre akımdaki geçerli hesaplama değeridir, ilk değeri ve akım değerini bir sonraki caculation'daki ilk değer olarak toplar.


0

Dokümanlara göre

İndirgeyici () toplayıcılar, groupingBy veya partitioningBy'nin aşağı akış yönünde çok seviyeli bir indirgeme işleminde kullanıldığında en kullanışlıdır. Bir akışta basit bir azalma gerçekleştirmek için bunun yerine Stream.reduce (BinaryOperator) öğesini kullanın.

Temelde reducing()sadece bir koleksiyon içinde zorlandığında kullanacaksınız . İşte başka bir örnek :

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

Bu öğreticiye göre azaltmak bazen daha az verimli

Küçültme işlemi her zaman yeni bir değer döndürür. Bununla birlikte, akümülatör işlevi, bir akışın bir öğesini her işlediğinde yeni bir değer döndürür. Bir akışın öğelerini koleksiyon gibi daha karmaşık bir nesneye azaltmak istediğinizi varsayalım. Bu, uygulamanızın performansını engelleyebilir. Azaltma işleminiz bir koleksiyona öğe eklemeyi içeriyorsa, akümülatör işleviniz bir öğeyi her işlediğinde, verimsiz olan öğeyi içeren yeni bir koleksiyon oluşturur. Bunun yerine mevcut bir koleksiyonu güncellemeniz daha verimli olur. Bunu, bir sonraki bölümde açıklanan Stream.collect yöntemiyle yapabilirsiniz ...

Bu nedenle, kimlik azaltma senaryosunda "yeniden kullanılır", .reducemümkünse daha az etkili olur .

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.