Bir Koleksiyonu veya Akışı iade etmeli miyim?


163

Üye listesine salt okunur bir görünüm döndüren bir yöntemim olduğunu varsayalım:

class Team {
    private List < Player > players = new ArrayList < > ();

    // ...

    public List < Player > getPlayers() {
        return Collections.unmodifiableList(players);
    }
}

Ayrıca, tüm müşterinin yaptığı listenin hemen üzerinde bir kez yinelendiğini varsayalım. Belki oyuncuları bir JList'e falan koymak. İstemci yok değil daha sonra incelenmek için listeye bir referans olarak saklamak!

Bu yaygın senaryo göz önüne alındığında, bunun yerine bir akış döndürmeli miyim?

public Stream < Player > getPlayers() {
    return players.stream();
}

Yoksa bir akışı Java'da deyimsel olmayan bir şekilde döndürmek mi? Akışlar, oluşturuldukları ifadenin içinde her zaman "sonlandırılacak" şekilde tasarlandı mı?


12
Bir deyim olarak bu konuda kesinlikle yanlış bir şey yoktur. Sonuçta players.stream(), arayana bir akış döndüren böyle bir yöntemdir. Asıl soru şu ki, gerçekten arayanı tek geçişle sınırlamak ve ayrıca CollectionAPI üzerinden koleksiyonunuza erişimini reddetmek istiyor musunuz? Belki arayan sadece addAllbaşka bir koleksiyona istiyor ?
Marko Topolnik

2
Her şey bağlıdır. Her zaman collection.stream () öğesinin yanı sıra Stream.collect () öğesini de yapabilirsiniz. Yani size ve bu işlevi kullanan arayan size kalmış.
Raja Anbazhagan

Yanıtlar:


222

Cevap, her zaman olduğu gibi "duruma bağlıdır" dır. Geri gönderilen koleksiyonun ne kadar büyük olacağına bağlıdır. Sonucun zaman içinde değişip değişmediğine ve döndürülen sonucun tutarlılığının ne kadar önemli olduğuna bağlıdır. Ve bu, kullanıcının yanıtı nasıl kullanacağına çok bağlıdır.

İlk olarak, bir Akıştan her zaman bir Koleksiyon alabileceğinizi veya bunun tersini yapabileceğinizi unutmayın:

// If API returns Collection, convert with stream()
getFoo().stream()...

// If API returns Stream, use collect()
Collection<T> c = getFooStream().collect(toList());

Yani soru şu ki arayanlar için daha faydalı.

Sonucunuz sonsuz olabilirse, tek bir seçenek vardır: Akış.

Sonucunuz çok büyükse, muhtemelen Stream'i tercih edersiniz, çünkü hepsini bir kerede gerçekleştirmede herhangi bir değer olmayabilir ve bunu yapmak önemli yığın basıncı yaratabilir.

Arayanın tüm yapacağı tekrarı yinelemekse (arama, filtreleme, toplama), Akış önceden seçilmeli ve bir koleksiyon gerçekleştirmeye gerek olmadığından (özellikle kullanıcı, bütün sonuç.) Bu çok yaygın bir durumdur.

Kullanıcının birden çok kez yineleyeceğini veya başka bir şekilde saklayacağını biliyor olsanız bile, bunun yerine bir Akış döndürmek isteyebilirsiniz, çünkü onu yerleştirmeyi seçtiğiniz Koleksiyonun (ör. ArrayList) istediklerinde ve arayanın yine de kopyalaması gerekir. bir akış döndürürseniz yapabilir collect(toCollection(factory))ve tam olarak istedikleri biçimde alabilirler.

Yukarıdaki "Stream'i Tercih Et" vakaları çoğunlukla Stream'in daha esnek olduğu gerçeğinden kaynaklanır; bir Koleksiyona materyalize etmenin maliyetlerini ve kısıtlamalarını ödemeden onu nasıl kullandığınıza geç bağlayabilirsiniz.

Bir Koleksiyonu iade etmeniz gereken tek durum, güçlü tutarlılık gereksinimleri olduğunda ve hareketli bir hedefin tutarlı bir anlık görüntüsünü oluşturmanız gerektiğidir. Ardından, öğeleri değişmeyecek bir koleksiyona koymak isteyeceksiniz.

Bu nedenle, çoğu zaman Stream'in doğru cevap olduğunu söyleyebilirim - daha esnektir, genellikle gereksiz materyalizasyon maliyetleri getirmez ve gerekirse kolayca seçtiğiniz Koleksiyona dönüştürülebilir. Ancak bazen, bir Koleksiyonu iade etmeniz gerekebilir (örneğin, güçlü tutarlılık gereksinimleri nedeniyle) veya Koleksiyonu iade etmek isteyebilirsiniz, çünkü kullanıcının onu nasıl kullanacağını bilirsiniz ve bunun onlar için en uygun şey olduğunu bilirsiniz.


6
Söylediğim gibi, özellikle güçlü tutarlılık gereksinimleriniz olduğunda, hareketli bir hedefin zamanında bir anlık görüntü döndürmek istediğinizde olduğu gibi, uçmayacağı birkaç durum var. Ancak, nasıl kullanılacağına dair spesifik bir şey bilmiyorsanız, Akış çoğu zaman daha genel bir seçim gibi görünür.
Brian Goetz

8
@Marko Sorunuzu bu kadar dar bir şekilde sınırlasanız bile, sonuca hala katılmıyorum. Belki de bir Akış oluşturmanın, koleksiyonu değiştirilemez bir sargı ile sarmaktan çok daha pahalı olduğunu varsayıyorsunuz? (Ve yapmasanız bile, sargıya aldığınız akış görünümü orijinalden aldığınızdan daha kötüdür; UnmodifiableList bölücüyü geçersiz kılmadığından (), tüm paralellikleri etkili bir şekilde kaybedersiniz.) Alt satır: dikkatli olun aşinalık yanlılığı; Koleksiyon'u yıllardır tanıyorsunuz ve bu yeni gelenlere güvensizlik getirmenizi sağlayabilir.
Brian Goetz

5
@MarkoTopolnik Tabii. Amacım, SSS olan genel API tasarım sorusunu ele almaktı. Maliyetle ilgili olarak, önceden materyalize bir koleksiyonunuz yoksa, iade edebileceğiniz veya paketleyebileceğiniz (OP yapar, ancak genellikle bir tane yoktur), alıcı yönteminde bir koleksiyonun gerçekleştirilmesinin bir akış döndürmekten ve izin vermekten daha ucuz olmadığını unutmayın. arayan bir tanesini gerçekleştirir (ve arayanın buna ihtiyacı yoksa veya ArrayList'e dönerseniz ancak arayan TreeSet istiyorsa erken materyalizasyon çok daha pahalı olabilir.) Ancak Akış yeni ve insanlar genellikle daha fazla $$$ bu.
Brian Goetz

4
@MarkoTopolnik Bellek içi çok önemli bir kullanım örneği olsa da, sıralı olmayan oluşturulan akışlar (ör. Stream.generate) gibi iyi paralelleştirme desteğine sahip başka durumlar da vardır. Bununla birlikte, Akışların zayıf bir uyum olduğu durumlarda, verilerin rasgele gecikmeyle ulaştığı reaktif kullanım durumu söz konusudur. Bunun için RxJava'yı öneririm.
Brian Goetz

4
@MarkoTopolnik Katılıyorum diye düşünmüyorum, belki de çabalarımızı biraz farklı bir şekilde odaklamamızı sevmiş olabilirsiniz. (Buna alışkınız; tüm insanları mutlu edemeyiz.) Akışlar için tasarım merkezi, bellek içi veri yapılarına odaklandı; RxJava tasarım merkezi harici olarak oluşturulan olaylara odaklanır. Her ikisi de iyi kütüphaneler; Ayrıca, tasarım merkezlerinden iyi olmayan vakalara uygulamaya çalıştığınızda her ikisi de çok iyi ücret almaz. Ancak bir çekiç, iğne ucu için korkunç bir araç olduğu için, çekiçle ilgili bir sorun olmadığını düşündürmez.
Brian Goetz

63

Brian Goetz'in mükemmel cevabına eklemek için birkaç puanım var .

"Getter" tarzı yöntem çağrısından bir Akış döndürmek oldukça yaygındır. Java 8 javadoc uygulamasındaki Akış kullanımı sayfasına bakın ve dışındaki paketler için "Akış döndüren yöntemler ..." konusuna bakın java.util.Stream. Bu yöntemler genellikle birden çok değeri veya bir şeyin toplanmasını temsil eden veya içerebilen sınıflar üzerindedir. Bu gibi durumlarda, API'ler genellikle koleksiyonlarını veya dizilerini döndürmüştür. Brian'ın cevabında belirttiği tüm nedenlerden dolayı, buraya Stream-return yöntemlerini eklemek çok esnektir. Bu sınıfların çoğunda koleksiyonlar veya dizi döndürme yöntemleri vardır, çünkü sınıflar Streams API'sinden önce gelir. Yeni bir API tasarlıyorsanız ve Akış döndüren yöntemler sağlamak mantıklıysa, koleksiyon döndüren yöntemler de eklemek gerekli olmayabilir.

Brian, değerleri bir koleksiyona "gerçekleştirmenin" maliyetinden bahsetti. Bu noktayı yükseltmek için burada aslında iki maliyet vardır: koleksiyonda değerlerin depolanma maliyeti (bellek tahsisi ve kopyalama) ve ayrıca değerleri ilk etapta yaratmanın maliyeti. İkinci maliyet, bir Akımın tembellik arama davranışından faydalanarak genellikle azaltılabilir veya önlenebilir. Bunun iyi bir örneği şu konumlardaki API'lerdir java.nio.file.Files:

static Stream<String>  lines(path)
static List<String>    readAllLines(path)

readAllLinesSonuç listesine depolamak için dosya içeriğinin tamamını yalnızca bellekte tutmakla kalmaz , aynı zamanda listeyi döndürmeden önce dosyayı sonuna kadar okumalıdır. linesHiç ya da değil - yöntem zaten gerekli sonra ne zamana kadar dosya okuma ve çizgi kırılmasını bırakarak neredeyse hemen bazı kurulum gerçekleştirdi sonra dönebilir. Arayanın yalnızca ilk on satırla ilgilenmesi durumunda, bu büyük bir faydadır:

try (Stream<String> lines = Files.lines(path)) {
    List<String> firstTen = lines.limit(10).collect(toList());
}

Elbette, arayan akışı yalnızca bir desenle eşleşen satırları döndürmek için filtreliyorsa önemli miktarda bellek alanı kaydedilebilir.

Ortaya çıkan bir deyim, akış döndüren yöntemleri, bir getönek olmadan temsil ettiği veya içerdiği şeylerin çoğulundan sonra adlandırmaktır . Ayrıca, stream()döndürülecek yalnızca bir değer kümesi olduğunda akış döndüren bir yöntem için makul bir ad olsa da, bazen birden çok değer türünde toplamalara sahip sınıflar vardır. Örneğin, hem nitelikleri hem de öğeleri içeren bir nesneniz olduğunu varsayalım. İki akış döndüren API sağlayabilirsiniz:

Stream<Attribute>  attributes();
Stream<Element>    elements();

3
Harika noktalar. Adlandırma deyiminin ortaya çıktığını nereden gördüğünüzü ve ne kadar çekişin (buharı) aldığını daha fazla söyleyebilir misiniz? Bir koleksiyona karşı bir akış elde ettiğinizi açıkça ortaya koyan bir adlandırma kuralı fikrini seviyorum - sık sık IDE'nin tamamlanmasının bana ne alabileceğimi söylemesi için "get" de tamamlanmasını bekliyorum.
Joshua Goldberg

1
Ayrıca o adlandırma deyim hakkında çok ilgileniyorum
seçilen

5
@JoshuaGoldberg JDK, bu adlandırma deyimini benimsemiş gibi görünse de, sadece değil. Şunu düşünün: Java 8'de CharSequence.chars () ve .codePoints (), BufferedReader.lines () ve Files.lines (), Java 9'da aşağıdakiler eklenmiştir: Process.children (), NetworkInterface.addresses ( ), Scanner.tokens (), Matcher.results (), java.xml.catalog.Catalog.catalogs (). Bu deyimi kullanmayan diğer akış döndürme yöntemleri eklendi - Scanner.findAll () akla geliyor - ancak çoğul isim deyimi JDK'da adil bir şekilde kullanılmaya başlanmış gibi görünüyor.
Stuart Marks

1

Akışlar, oluşturuldukları ifadenin içinde her zaman "sonlandırılacak" şekilde tasarlandı mı?

Çoğu örnekte bu şekilde kullanılırlar.

Not: Bir Akışı döndürmek, bir Yineleyici döndürmekten farklı değildir (çok daha etkileyici bir güçle kabul edilir)

IMHO en iyi çözüm, bunu neden yaptığınızı özetlemek ve koleksiyonu geri vermemektir.

Örneğin

public int playerCount();
public Player player(int n);

ya da onları saymak istiyorsan

public int countPlayersWho(Predicate<? super Player> test);

2
Bu cevabın problemi, yazarın müşterinin yapmak istediği her eylemi tahmin etmesini gerektirmesidir ve sınıftaki yöntem sayısını büyük ölçüde artıracaktır.
dkatzel

@dkatzel Son kullanıcıların yazar mı yoksa birlikte çalıştıkları kişiye mi bağlı olduğuna bağlıdır. Son kullanıcılar bilinemezse, daha genel bir çözüme ihtiyacınız vardır. Yine de temel alınan koleksiyona erişimi sınırlamak isteyebilirsiniz.
Peter Lawrey

1

Akış sonluysa ve döndürülen nesnelerde kontrol edilen bir istisna atacak beklenen / normal bir işlem varsa, her zaman bir Koleksiyon döndürürüm. Çünkü her bir nesne için bir kontrol istisnası atabilecek bir şey yapacaksanız, akıştan nefret edersiniz. Akarsu ile bir gerçek eksikliği orada zarif kontrol istisnaları ile başa çıkmak için yetersizlik.

Şimdi, belki de bu, kontrol edilen istisnalara ihtiyacınız olmadığının bir işaretidir, ki bu adil, ancak bazen kaçınılmazdır.


1

Koleksiyonların aksine, akışların ek özellikleri vardır . Herhangi bir yöntemle döndürülen bir akış şu şekilde olabilir:

  • sonlu veya sonsuz
  • paralel veya sıralı (uygulamanın diğer bölümlerini etkileyebilecek, varsayılan olarak dünya çapında paylaşılan iş parçacığı ile)
  • sipariş edilmiş veya sipariş edilmemiş

Bu farklılıklar koleksiyonlarda da mevcuttur, ancak bunlar açık sözleşmenin bir parçasıdır:

  • Tüm Koleksiyonlar boyutlara sahiptir, Yineleyici / Yinelenebilir sonsuz olabilir.
  • Koleksiyonlar açıkça sipariş edilmiş veya sipariş edilmemiş
  • Paralellik, koleksiyonun iş parçacığı güvenliğinin ötesinde önemsediği bir şey değildir.

Bir akışın tüketicisi olarak (bir yöntem geri dönüşünden veya bir yöntem parametresi olarak) bu tehlikeli ve kafa karıştırıcı bir durumdur. Algoritmalarının doğru bir şekilde çalıştığından emin olmak için, akış tüketicileri, algoritmanın akış özellikleri hakkında yanlış bir varsayım yapmadığından emin olmalıdır. Ve bu çok zor bir şey. Birim testinde, bu, tekrarlanacak tüm testlerinizi aynı akış içeriğiyle, ancak

  • (sonlu, sıralı, sıralı)
  • (sonlu, sıralı, paralel)
  • (sonlu, sıralı olmayan, sıralı) ...

Giriş akışı algoritmanızı bozan bir özelliğe sahipse IllegalArgumentException özel durumunu atan akışlar için koruma sağlar , çünkü özellikler gizlidir.

Bu, Akış'ı, yukarıdaki sorunlardan hiçbiri önemli olmadığında, yöntem imzasında yalnızca geçerli bir seçenek olarak bırakır; bu durum nadiren durumdur.

Düzenli bir sözleşmeyle (ve örtülü iş parçacığı havuzu işlemesi olmadan) yöntem imzalarında diğer veri türlerinin kullanılması, verilerin düzgünlük, boyut veya paralellik (ve iş parçacığı kullanımı) hakkında yanlış varsayımlarla yanlışlıkla işlenmesini imkansız hale getiren çok daha güvenlidir.


2
Sonsuz akışlarla ilgili endişeleriniz asılsızdır; soru "bir koleksiyon veya bir akış döndürmeli miyim" dir. Koleksiyon bir olasılıksa, sonuç tanım gereği sonludur. Öyleyse, arayanların bir koleksiyonu iade edebileceğiniz göz önüne alındığında, sonsuz bir yinelemeyi riske atacağı endişeleri asılsızdır. Bu cevaptaki geri kalan tavsiye sadece kötüdür. Stream'i aşırı kullanan biriyle karşılaştığınız gibi geliyor ve diğer yönde aşırı dönüyorsunuz. Anlaşılır, ancak kötü tavsiye.
Brian Goetz

0

Bence senaryonuza bağlı. TeamUygulamanızı yaparsanız Iterable<Player>, yeterli olabilir.

for (Player player : team) {
    System.out.println(player);
}

veya işlevsel bir tarzda:

team.forEach(System.out::println);

Ancak daha eksiksiz ve akıcı bir API istiyorsanız, bir akış iyi bir çözüm olabilir.


OP'nin yayınladığı kodda, oyuncu sayısının bir tahmin dışında neredeyse işe yaramaz olduğunu unutmayın ('1034 oyuncu şu anda oynuyor, başlamak için burayı tıklayın!') Bunun nedeni, değişebilir bir koleksiyonun değişmez bir görünümünü döndürüyorsunuz. , şimdi elde ettiğiniz sayı bundan sonraki üç mikrosaniyeye eşit olmayabilir. Bir Koleksiyonu döndürmek size sayıya ulaşmanın "kolay" bir yolunu verirken (ve gerçekten stream.count()de oldukça kolaydır), bu sayı hata ayıklama veya tahmin yapmaktan başka hiçbir şey için gerçekten çok anlamlı değildir.
Brian Goetz

0

Daha yüksek profilli katılımcıların bazıları büyük genel tavsiyeler verirken, kimsenin tam olarak belirtmediğine şaşırdım:

Zaten elinizde "somutlaştırılmış" varsa Collection(yani, çağrıdan önce oluşturulmuşsa - verilen örnekte olduğu gibi, üye alanı olduğu gibi), bunu a'ya dönüştürmenin bir anlamı yoktur Stream. Arayan kişi bunu kolayca yapabilir. Oysa, arayan kişi verileri orijinal biçiminde tüketmek istiyorsa Stream, orijinal yapının bir kopyasını yeniden gerçekleştirmek için onları gereksiz işler yapmaya zorlarsınız.


-1

Bir Stream fabrikası daha iyi bir seçim olabilir. Yalnızca Akış yoluyla koleksiyonları göstermenin en büyük kazancı, alan adı modelinizin veri yapısını daha iyi kapsama almasıdır. Etki alanı sınıflarınızın herhangi bir kullanımının, yalnızca bir Akışı açığa çıkararak Listenizin veya Setinizin iç işleyişini etkilemesi imkansızdır.

Ayrıca, etki alanı sınıfınızdaki kullanıcıları daha modern bir Java 8 stilinde kod yazmaya teşvik eder. Mevcut alıcılarınızı koruyarak ve yeni Akışla dönen alıcılar ekleyerek bu stile aşamalı olarak yeniden bakmak mümkündür. Zamanla, bir Liste veya Küme döndüren tüm alıcıları silene kadar eski kodunuzu yeniden yazabilirsiniz. Bu eski yeniden düzenleme, tüm eski kodu temizledikten sonra gerçekten iyi hissettiriyor!


7
bunun tam olarak alıntılanmasının bir nedeni var mı? bir kaynak var mı
Xerus

-5

Muhtemelen 2 yöntem olurdu, biri geri dönmek Collectionve diğeri koleksiyonu olarak döndürmek için Stream.

class Team
{
    private List<Player> players = new ArrayList<>();

// ...

    public List<Player> getPlayers()
    {
        return Collections.unmodifiableList(players);
    }

    public Stream<Player> getPlayerStream()
    {
        return players.stream();
    }

}

Bu her iki dünyanın en iyisi. İstemci Listeyi veya Akışı isteyip istemediğini seçebilir ve yalnızca bir Akış elde etmek için listenin değişmez bir kopyasını oluşturmak için ekstra nesne oluşturma işlemini yapmak zorunda değildir.

Bu ayrıca API'nıza yalnızca 1 yöntem daha ekler, böylece çok fazla yönteminiz olmaz


1
Çünkü bu iki seçenek arasında seçim yapmak istedi ve her birinin artılarını ve eksilerini istedi. Ayrıca herkese bu kavramları daha iyi anlamasını sağlar.
Libert Piou Piou

Lütfen yapma. API'leri hayal edin!
François Gautier
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.