Java8 akışının öğelerini varolan bir listeye ekleme


Yanıtlar:


198

NOT: nosid'in cevabı , kullanarak mevcut bir koleksiyona nasıl ekleneceğini gösterir forEachOrdered(). Bu, mevcut koleksiyonları mutasyona uğratmak için yararlı ve etkili bir tekniktir. Cevabım Collector, var olan bir koleksiyonu değiştirmek için neden a kullanmamanız gerektiğini belirtir.

Kısa cevap hayır , en azından genel olarak değil Collector, mevcut bir koleksiyonu değiştirmek için a kullanmamalısınız .

Bunun nedeni, toplayıcıların iplik güvenli olmayan koleksiyonlarda bile paralelliği destekleyecek şekilde tasarlanmasıdır. Bunu yapmanın yolu, her bir iş parçacığının kendi ara sonuç koleksiyonunda bağımsız olarak çalışmasını sağlamaktır. Her iş parçacığının kendi koleksiyonunu alma biçimi, her seferinde yeni bir koleksiyon Collector.supplier()döndürmek için gerekli olanı çağırmaktır .

Bu ara sonuç koleksiyonları, daha sonra, tek bir sonuç toplama olana kadar, yine iplik sınırlı bir şekilde birleştirilir. Bu collect()operasyonun nihai sonucudur .

Balder ve asurilerin birkaç cevabı, Collectors.toCollection()yeni bir liste yerine mevcut bir listeyi döndüren bir tedarikçiyi kullanmayı ve geçmeyi önerdi . Bu, tedarikçi üzerindeki gereksinimi ihlal eder, yani her seferinde yeni, boş bir koleksiyon döndürür.

Bu, yanıtlarındaki örneklerin gösterdiği gibi basit durumlar için işe yarayacaktır. Ancak, özellikle akış paralel olarak çalıştırılırsa başarısız olur. (Kütüphanenin gelecekteki bir sürümü, ardışık durumda bile başarısız olmasına neden olacak, öngörülemeyen bir şekilde değişebilir.)

Basit bir örnek verelim:

List<String> destList = new ArrayList<>(Arrays.asList("foo"));
List<String> newList = Arrays.asList("0", "1", "2", "3", "4", "5");
newList.parallelStream()
       .collect(Collectors.toCollection(() -> destList));
System.out.println(destList);

Bu programı çalıştırdığımda, sık sık bir ArrayIndexOutOfBoundsException. Bunun nedeni ArrayList, iş parçacığı güvensiz bir veri yapısı olan birden çok iş parçacığının çalışmasıdır. Tamam, senkronize edelim:

List<String> destList =
    Collections.synchronizedList(new ArrayList<>(Arrays.asList("foo")));

Bu artık bir istisna dışında başarısız olmayacak. Ancak beklenen sonuç yerine:

[foo, 0, 1, 2, 3]

bunun gibi garip sonuçlar verir:

[foo, 2, 3, foo, 2, 3, 1, 0, foo, 2, 3, foo, 2, 3, 1, 0, foo, 2, 3, foo, 2, 3, 1, 0, foo, 2, 3, foo, 2, 3, 1, 0]

Bu, yukarıda tarif ettiğim iplik sınırlı biriktirme / birleştirme işlemlerinin sonucudur. Paralel bir akışla, her bir iplik tedarikçiyi ara birikim için kendi koleksiyonunu almaya çağırır. Aynı koleksiyonu döndüren bir tedarikçi iletirseniz, her iş parçacığı sonuçlarını bu koleksiyona ekler. İleti dizileri arasında herhangi bir sipariş olmadığından, sonuçlar bazı rastgele sırada eklenir.

Daha sonra, bu ara koleksiyonlar birleştirildiğinde, bu temel olarak listeyi kendisiyle birleştirir. Listeler kullanılarak birleştirilir List.addAll(); bu, işlem sırasında kaynak koleksiyonu değiştirilirse sonuçların tanımsız olduğunu söyler. Bu durumda, ArrayList.addAll()bir dizi kopyalama işlemi yapar, bu yüzden sanırım biri ne tür bir tür kendini çoğaltarak biter. (Diğer Liste uygulamalarının tamamen farklı davranışlara sahip olabileceğini unutmayın.) Her neyse, bu, hedefteki garip sonuçları ve yinelenen öğeleri açıklar.

"Akışımı sırayla çalıştırdığınızdan emin olacağım" diyebilir ve devam edip böyle bir kod yazabilirsiniz.

stream.collect(Collectors.toCollection(() -> existingList))

neyse. Bunu yapmamanızı tavsiye ederim. Akışı kontrol ederseniz, paralel olarak çalışmayacağını garanti edebilirsiniz. Koleksiyonlar yerine akışların dağıtıldığı yerde bir programlama tarzının ortaya çıkmasını bekliyorum. Biri size bir akış verirse ve bu kodu kullanırsanız akış paralel olursa başarısız olur. Daha da kötüsü, birisi sana sıralı akışı el olabilir ve bu kod Sonra biraz zaman keyfi miktar sonra sistemde başka kod neden olacaktır paralel akışı kullanmak değişebilir, bir süre cezası çalışan tüm testler, vs. geçecek senin kodu kırmak.

Tamam, o zaman sequential()bu kodu kullanmadan önce herhangi bir akışta aramayı unutmayın.

stream.sequential().collect(Collectors.toCollection(() -> existingList))

Tabii ki, bunu her seferinde yapmayı hatırlayacaksın, değil mi? :-) Diyelim ki öyle. Ardından, performans ekibi, özenle hazırlanmış tüm paralel uygulamalarının neden herhangi bir hızlanma sağlamadığını merak edecek. Ve bir kez daha onlar için aşağı iz edeceğiz senin sırayla çalıştırmak için tüm akışı zorluyor kodu.

Yapma.


Harika bir açıklama! - bunu açıkladığın için teşekkürler. Cevabımı, olası paralel akışlarla asla yapmamayı önermek için düzenleyeceğim.
Balder

3
Soru şuysa, bir akışın öğelerini mevcut bir listeye eklemek için bir satır varsa, kısa cevap evettir . Cevabımı gör. Ancak, katılıyorum, mevcut bir listeyle birlikte Collectors.toCollection () kullanmanın yanlış yol olduğunu.
nosid

Doğru. Sanırım hepimiz koleksiyoncular düşünüyoruz.
Stuart Marks

Mükemmel cevap! Açıkça tavsiye etseniz bile sıralı çözümü kullanmak için çok cazipim çünkü belirtildiği gibi iyi çalışmalıdır. Ancak javadoc'un, toCollectionyöntemin tedarikçi argümanı beni her zaman beni ikna etmemeye ikna etmek için yeni ve boş bir koleksiyon döndürmesini gerektiriyor . Gerçekten çekirdek Java sınıflarının javadoc sözleşmesini kırmak istiyorum.
yakınlaştırma

1
@AlexCurvers Akışın yan etkileri olmasını istiyorsanız, neredeyse kesinlikle kullanmak istersiniz forEachOrdered. Yan etkiler, mevcut öğelere zaten öğe olup olmadığına bakılmaksızın öğe eklemeyi içerir. Bir akışın öğelerinin yeni bir koleksiyona yerleştirilmesini istiyorsanız , collect(Collectors.toList())veya toSet()veya öğesini kullanın toCollection().
Stuart Marks

169

Görebildiğim kadarıyla, diğer tüm cevaplar mevcut bir akışa eleman eklemek için bir toplayıcı kullandı. Bununla birlikte, daha kısa bir çözüm var ve hem ardışık hem de paralel akışlar için çalışıyor. Sadece yöntemi kullanabilirsiniz forEachOrdered bir yöntem referansı ile kombinasyon halinde.

List<String> source = ...;
List<Integer> target = ...;

source.stream()
      .map(String::length)
      .forEachOrdered(target::add);

Tek kısıtlama, bu kaynak ve hedefin farklı listeler olmasıdır, çünkü bir akışın işlendiği sürece kaynakta değişiklik yapmanıza izin verilmez.

Bu çözümün hem ardışık hem de paralel akışlar için çalıştığını unutmayın. Ancak, eşzamanlılıktan faydalanmamaktadır. ForEachOrdered öğesine iletilen yöntem başvurusu her zaman sırayla yürütülür.


6
+1 Bu kadar çok insanın, bir tane olduğunda hiçbir ihtimal olmadığını iddia etmesi komik. Btw. İki ay önceforEach(existing::add) bir cevaba olasılık olarak dahil ettim . Ben de eklemeliydim forEachOrdered
Holger

5
forEachOrderedBunun yerine kullandığınız herhangi bir sebep var mı forEach?
Üye

6
@membersound: forEachOrderediçin çalışan sıralı ve paralel akışları. Buna karşılık, forEachiletilen işlev nesnesini paralel akışlar için eşzamanlı olarak yürütebilir. Bu durumda, işlev nesnesi, örneğin a Vector<Integer>.
nosid

@BrianGoetz: İtiraf etmeliyim ki, Stream.forEachOrdered dokümantasyonu biraz kesin değil. Ancak, herhangi iki çağrısı arasında daha önce bir ilişkinin olmadığı bu belirtimin makul bir yorumunu göremiyorum . Yöntemin hangi iş parçacıklarından çağrıldığına bakılmaksızın veri yarışı yoktur . Bunu bilmeni beklerdim. target::add
nosid

Bu benim için en yararlı cevap. Aslında, bir akıştan mevcut bir listeye öğe eklemek için pratik bir yol gösterir, bu da sorulan sorudur (yanıltıcı "toplamak" kelimesine rağmen)
Wheezil

12

Kısa cevap hayırdır (veya hayır olmalıdır). DÜZENLEME: evet, mümkün (aşağıdaki asurların cevabına bakınız), ama okumaya devam edin. EDIT2: ama yine de yapmamanız gereken başka bir sebep için Stuart Marks'ın cevabına bakın!

Daha uzun cevap:

Java 8'deki bu yapıların amacı , dile Fonksiyonel Programlama ile ilgili bazı kavramları tanıtmaktır ; Fonksiyonel Programlamada veri yapıları tipik olarak değiştirilmez, bunun yerine eski yapılardan harita, filtre, katlama / azaltma ve diğerleri gibi dönüşümler yoluyla yenileri oluşturulur.

Eğer varsa gereken eski listesinde değişiklik, sadece taze listeye eşleştirilmiş öğeleri toplamak:

final List<Integer> newList = list.stream()
                                  .filter(n -> n % 2 == 0)
                                  .collect(Collectors.toList());

ve sonra list.addAll(newList)tekrar yapın: eğer gerçekten yapmanız gerekiyorsa.

(veya eskisini ve yenisini birleştiren yeni bir liste oluşturun ve listdeğişkene geri atayın - bu FP'nin ruhunda biraz daha fazladır addAll)

API ile ilgili olarak: API izin vermesine rağmen (yine, asilliler cevabına bakın), en azından genel olarak, bunu yapmaktan kaçınmaya çalışmalısınız. (Java genellikle bir FP dili olmasa da) paradigma (FP) ile savaşmak ve onu öğrenmek yerine öğrenmeye çalışmak en iyisidir ve kesinlikle gerekliyse sadece "dirtier" taktiklere başvurmaktır.

Gerçekten uzun cevap: (örn. Bir FP giriş / kitabını gerçekten bulma ve okuma çabalarını dahil ederseniz)

Mevcut listeleri değiştirmenin genel olarak neden kötü bir fikir olduğunu ve yerel bir değişkeni değiştirmediğiniz ve algoritmanız kısa ve / veya önemsiz olmadığı sürece kodun sürdürülebilirliği sorunun kapsamı dışında kaldığını öğrenmek için —Fonksiyonel Programlamaya (yüzlerce) iyi bir giriş yapın ve okumaya başlayın. "Önizleme" açıklaması şuna benzer bir şey olacaktır: Verileri değiştirmemek (programınızın çoğu bölümünde) daha matematiksel olarak daha sağlam ve mantıklıdır ve beyniniz bir kez daha yüksek seviyeye ve daha az teknik (ve daha insan dostu) eski tarz zorunlu düşünceden uzağa geçişler) program mantığının tanımları.


@assylias: mantıksal olarak, yanlış değildi çünkü ya da bir kısmı vardı; neyse, bir not ekledim.
Erik Kaplun

1
Kısa cevap doğrudur. Önerilen tek gömlekler basit durumlarda başarılı olacak, ancak genel durumda başarısız olacaktır.
Stuart Marks

Uzun cevap çoğunlukla doğrudur, ancak API'nın tasarımı esas olarak paralellik ile ilgilidir ve fonksiyonel programlama ile daha azdır. Tabii ki FP hakkında paralelliğe uygun birçok şey var, bu yüzden bu iki kavram iyi bir şekilde hizalanmış.
Stuart Marks

@StuartMarks: İlginç: hangi durumlarda asillerin cevabında verilen çözüm bozulur? (ve paralellik hakkında iyi noktalar — Sanırım
FP'yi

@ErikAllik Bu konuyu kapsayan bir cevap ekledim.
Stuart Marks

11

Erik Allik zaten çok iyi nedenler verdi, neden muhtemelen bir akımın öğelerini mevcut bir Listeye toplamak istemeyeceksiniz.

Her neyse, gerçekten bu işlevselliğe ihtiyacınız varsa, aşağıdaki tek astarı kullanabilirsiniz.

Ama Stuart Marks onun cevabını açıklar sen gerektiğini asla akışları paralel akışlar olabilir eğer bunu - kullanımını kendi sorumluluğunuzdadır ...

list.stream().collect(Collectors.toCollection(() -> myExistingList));

ahh, bu bir utanç: P
Erik Kaplun

2
Akış paralel olarak çalıştırılırsa bu teknik korkunç bir şekilde başarısız olacaktır.
Stuart Marks

1
Başarısız olmadığından emin olmak için toplama sağlayıcının sorumluluğu olacaktır - örneğin, eşzamanlı bir koleksiyon sağlayarak.
Balder

2
Hayır, bu kod tedarikçinin uygun türde yeni, boş bir koleksiyon döndürmesi olan toCollection () gereksinimini ihlal eder. Hedef iş parçacığı açısından güvenli olsa bile, paralel durum için yapılan birleştirme yanlış sonuçlara neden olur.
Stuart Marks

1
@Balder Bunu açıklığa kavuşturması gereken bir cevap ekledim.
Stuart Marks

4

Sadece orijinal listeyi Collectors.toList()döndüren liste olmak zorunda .

İşte bir demo:

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class Reference {

  public static void main(String[] args) {
    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
    System.out.println(list);

    // Just collect even numbers and start referring the new list as the original one.
    list = list.stream()
               .filter(n -> n % 2 == 0)
               .collect(Collectors.toList());
    System.out.println(list);
  }
}

Yeni oluşturulan öğeleri orijinal listenize tek bir satırda nasıl ekleyebileceğiniz aşağıda açıklanmıştır.

List<Integer> list = ...;
// add even numbers from the list to the list again.
list.addAll(list.stream()
                .filter(n -> n % 2 == 0)
                .collect(Collectors.toList())
);

Bu Fonksiyonel Programlama Paradigmasının sağladığı şey budur.


Sadece yeniden atamak değil, mevcut bir listeye nasıl ekleneceğini / toplanacağını söylemek istedim.
codefx

1
Teknik olarak, Fonksiyonel Programlama paradigmasında bu tür şeyleri yapamazsınız; İşlevsel Programlama'da durum değiştirilmez, bunun yerine kalıcı veri yapılarında yeni durumlar oluşturulur, bu da eşzamanlılık amaçları için güvenli olmasını sağlar ve daha işlevseldir. Bahsettiğim yaklaşım, yapabileceğiniz şeydir ya da her öğeyi yinelediğiniz eski stil nesne yönelimli yaklaşıma başvurabilir ve öğeleri uygun gördüğünüz gibi saklayabilir veya kaldırabilirsiniz.
Aman Agnihotri

0

targetList = sourceList.stream (). flatmap (Liste :: akış) .collect (Collectors.toList ());


0

Eski listeyi ve yeni listeyi akış olarak birleştirir ve sonuçları hedef listesine kaydederim. Paralel olarak da iyi çalışır.

Stuart Marks tarafından verilen kabul edilen cevap örneğini kullanacağım:

List<String> destList = Arrays.asList("foo");
List<String> newList = Arrays.asList("0", "1", "2", "3", "4", "5");

destList = Stream.concat(destList.stream(), newList.stream()).parallel()
            .collect(Collectors.toList());
System.out.println(destList);

//output: [foo, 0, 1, 2, 3, 4, 5]

Umarım yardımcı 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.