Koleksiyoncunun Javadoc'u, bir akışın öğelerinin yeni bir Listeye nasıl toplanacağını gösterir. Sonuçları mevcut bir ArrayList'e ekleyen bir astar var mı?
Koleksiyoncunun Javadoc'u, bir akışın öğelerinin yeni bir Listeye nasıl toplanacağını gösterir. Sonuçları mevcut bir ArrayList'e ekleyen bir astar var mı?
Yanıtlar:
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.
toCollection
yö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.
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()
.
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.
forEach(existing::add)
bir cevaba olasılık olarak dahil ettim . Ben de eklemeliydim forEachOrdered
…
forEachOrdered
Bunun yerine kullandığınız herhangi bir sebep var mı forEach
?
forEachOrdered
için çalışan sıralı ve paralel akışları. Buna karşılık, forEach
iletilen işlev nesnesini paralel akışlar için eşzamanlı olarak yürütebilir. Bu durumda, işlev nesnesi, örneğin a Vector<Integer>
.
target::add
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 list
değ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ı.
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));
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.
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.
Collection
”