Java akışları gerçekten sadece hata ayıklama için bakmak mı?


137

Java akışları hakkında okuyorum ve devam ederken yeni şeyler keşfediyorum. Bulduğum yeni şeylerden biri peek()işlevdi. Gözden geçirdiğim hemen hemen her şey, akışlarınızın hata ayıklaması için kullanılması gerektiğini söylüyor.

Her bir Hesabın bir kullanıcı adı, şifre alanı ve bir login () ve loginIn () yöntemine sahip olduğu bir Akışım olsaydı ne olurdu.

Bende var

Consumer<Account> login = account -> account.login();

ve

Predicate<Account> loggedIn = account -> account.loggedIn();

Bu neden bu kadar kötü?

List<Account> accounts; //assume it's been setup
List<Account> loggedInAccount = 
accounts.stream()
    .peek(login)
    .filter(loggedIn)
    .collect(Collectors.toList());

Şimdi söyleyebildiğim kadarıyla bunun tam olarak ne yapması gerektiğini yapıyor. O;

  • Hesapların bir listesini alır
  • Her hesaba giriş yapmayı dener
  • Giriş yapmayan tüm hesapları filtreler
  • Giriş yapılmış hesapları yeni bir listede toplar

Böyle bir şey yapmanın dezavantajı nedir? Devam etmememin bir nedeni var mı? Son olarak, bu çözüm değilse ne olacak?

Bunun orijinal sürümü .filter () yöntemini aşağıdaki gibi kullanmıştır;

.filter(account -> {
        account.login();
        return account.loggedIn();
    })

38
Kendimi çok satırlı bir lambdaya ihtiyaç duyduğumda, çizgileri özel bir yönteme taşıyorum ve lambda yerine yöntem referansını geçiyorum.
VGR

1
Niyet nedir - tüm hesapları oturum açmaya çalışan ve onlar giriş yaptıysanız dayalı filtreyi onları (dogrudur olabilir)? Veya, onları giriş yapmak istiyoruz ardından onları giriş yaptıktan olup olmamasına göre filtreleme? Bunu bu sırayla soruyorum çünkü tam forEachtersine istediğiniz işlem olabilir peek. API'de olması kötüye kullanıma açık olmadığı anlamına gelmez (gibi Optional.of).
Makoto

8
Ayrıca kodunuzun sadece .peek(Account::login)ve olabileceğini unutmayın .filter(Account::loggedIn); sadece böyle bir yöntemi çağıran bir Tüketici ve Tahmin yazmak için hiçbir neden yoktur.
Joshua Taylor

2
Ayrıca akışı API dikkat açıkça yan etkilerini caydırıcı içinde davranışsal parametreler .
Didier L

6
Yararlı tüketicilerin her zaman yan etkileri vardır, bunlar elbette önerilmez. Bu aslında aynı bölümde belirtilmiştir: “ ve gibi az sayıda akış işlemi yalnızca yan etkilerle çalışabilir; bunlar dikkatli kullanılmalıdır. forEach()peek()”. Daha fazla söylemek, peekişlemin (hata ayıklama amaçları için tasarlanan) map()veya gibi başka bir işlem içinde aynı şeyi yaparak değiştirilmemesi gerektiğini hatırlatmaktı filter().
Didier L

Yanıtlar:


77

Bu konudaki temel paket:

Anında hedefinize ulaşsa bile API'yı istenmeyen bir şekilde kullanmayın. Bu yaklaşım gelecekte kırılabilir ve gelecekteki koruyucular için de belirsizdir.


Farklı operasyonlar oldukları için bunu birden fazla operasyona ayırmanın bir zararı yoktur. API'nın belirsiz ve istenmeyen bir şekilde kullanılmasında zarar vardır ; bu, Java'nın gelecekteki sürümlerinde bu özel davranış değiştirilirse sonuçları olabilir.

forEachBu işlemi kullanmak , bakıcıya her öğesinde amaçlanan bir yan etki accountsolduğunu ve onu değiştirebilecek bir işlem gerçekleştirdiğinizi açıklığa kavuşturacaktır .

Ayrıca peek, terminal işlemi çalışana kadar tüm koleksiyonda çalışmayan, ancak forEachgerçekten bir terminal işlemi olan bir ara işlem olması bakımından daha gelenekseldir . Bu yolla, kodunuzun davranışı ve akışı hakkında , bu bağlamda peekolduğu gibi davranıp davranmayacağına dair soru sormanın aksine güçlü argümanlar yapabilirsiniz forEach.

accounts.forEach(a -> a.login());
List<Account> loggedInAccounts = accounts.stream()
                                         .filter(Account::loggedIn)
                                         .collect(Collectors.toList());

3
Giriş işlemini bir önişleme adımında gerçekleştirirseniz, bir akışa ihtiyacınız yoktur. forEachKaynak koleksiyonunda doğru performans gösterebilirsiniz :accounts.forEach(a -> a.login());
Holger

1
@Holger: Mükemmel nokta. Bunu cevaba dahil ettim.
Makoto

2
@ Adam.J: Cevabım daha çok başlığınızda bulunan genel soruya odaklandı, yani bu yöntem gerçekten sadece hata ayıklama için, bu yöntemin yönlerini açıklayarak. Bu cevap, gerçek kullanım durumunuz ve bunun yerine nasıl yapılacağı ile daha çok kaynaşmıştır. Yani diyebilirsiniz ki, birlikte tam resmi sağlıyorlar. Birincisi, bunun amaçlanan kullanım olmamasının nedeni, ikincisi sonuç, istenmeyen bir kullanıma bağlı kalmamak ve bunun yerine ne yapılması gerektiğidir. İkincisinin sizin için daha pratik kullanımı olacaktır.
Holger

2
Tabii ki, login()yöntemin booleanbaşarı durumunu gösteren bir değer döndürmesi çok daha kolaydı ...
Holger

3
Ben de bunu hedefliyordum. A login()döndürürse boolean, bunu en temiz çözüm olan yüklem olarak kullanabilirsiniz. Hala bir yan etkisi vardır, ancak müdahale loginetmediği sürece sorun olmaz , yani birinin Accountişleminin diğerinin giriş işlemi üzerinde hiçbir etkisi yoktur Account.
Holger

111

Anlamanız gereken önemli şey, akışların terminal işlemi tarafından yönlendirilmesidir . Terminal işlemi, tüm elemanların işlenip işlenmeyeceğini veya herhangi bir işlemin yapılacağını belirler. Yani collect, oysa her bir öğeyi işleyen bir işlemdir findAnybunun, eşleşen eleman karşılaştı kez işleme öğeleri durdurabilir.

Ve count()öğeleri işlemeden akışın boyutunu belirleyebildiği zaman hiçbir öğeyi işlemeyebilir. Bu, Java 8'de yapılmayan ancak Java 9'da olacak bir optimizasyon olduğundan, Java 9'a geçtiğinizde ve count()tüm öğeleri işlemek için koda sahip olduğunuzda sürprizler olabilir . Bu aynı zamanda diğer uygulamaya bağlı ayrıntılara da bağlıdır, örneğin Java 9'da bile, referans uygulaması, limitbu tür bir öngörüyü engelleyen temel bir sınırlama bulunmamakla birlikte, sonsuz bir akış kaynağının boyutunu bir araya getiremez .

Yana peeksağlayan “her bir elemanın üzerinde işlem gerçekleştirmek elemanları ortaya çıkan akımdan tüketildiği kadar ”, bu elementlerin işlem zorunlu değildir ama uç çalışma gereksinimlerine bağlı olarak hareket gerçekleştirir. Bu, belirli bir işleme ihtiyacınız varsa, örneğin tüm öğelere bir eylem uygulamak istiyorsanız, bunu büyük bir dikkatle kullanmanız gerektiği anlamına gelir. Terminal işleminin tüm öğeleri işlemesi garanti edilirse çalışır, ancak o zaman bile, bir sonraki geliştiricinin terminal işlemini değiştirmediğinden emin olmalısınız (veya bu ince yönü unutursunuz).

Ayrıca, akışlar, paralel akışlar için bile belirli bir işlem birleşimi için karşılaşma düzenini korumayı garanti ederken, bu garantiler geçerli değildir peek. Bir listeye toplanırken, ortaya çıkan liste sıralı paralel akışlar için doğru sıraya sahip olacaktır, ancak peekeylem keyfi bir sırada ve eşzamanlı olarak çağrılabilir.

Dolayısıyla, yapabileceğiniz en yararlı şey peek, API belgelerinin tam olarak söylediği bir akış öğesinin işlenip işlenmediğini bulmaktır:

Bu yöntem esas olarak, bir boru hattındaki belirli bir noktayı geçtikçe öğeleri görmek istediğiniz hata ayıklamayı desteklemek için bulunur.


OP'nin kullanım durumunda gelecekteki veya mevcut herhangi bir sorun olacak mı? Kodu her zaman istediğini yapıyor mu?
ZhongYu

9
@ bayou.io: görebildiğim kadarıyla bu formda bir sorun yok . Ama açıklamaya çalıştığım gibi, bu şekilde kullanmak, bir veya iki yıl sonra koda «özellik talebi 9876» koduna dahil etmek için bile koda geri dönseniz bile - bu yönü hatırlamanız gerektiği anlamına geliyor…
Holger

1
"gözetleme eylemi keyfi bir sırada ve eşzamanlı olarak çağrılabilir". Bu ifade, gözetlemenin nasıl çalıştığına ilişkin kurallarına aykırı değil mi, örneğin "elementler tüketildiğinde"?
Jose Martinez

5
@Jose Martinez: " Sonuçta elde edilen akımdan elemanlar tüketilir" diyor , bu terminal işlemi değil, işlemdir, ancak terminal eylemi bile nihai sonuç tutarlı olduğu sürece düzensiz öğeleri tüketebilir. Ama aynı zamanda, API notunun ifadesi, “ bir boru hattında belirli bir noktadan geçtikçe öğeleri görün ” ifadesini tanımlamakta daha iyi bir iş çıkardığını düşünüyorum.
Holger

23

Belki de bir kural, "hata ayıklama" senaryosunun dışında bir göz kullanırsanız, bunu yalnızca sonlandırma ve ara filtreleme koşullarının ne olduğundan eminseniz yapmanız gerekir. Örneğin:

return list.stream().map(foo->foo.getBar())
                    .peek(bar->bar.publish("HELLO"))
                    .collect(Collectors.toList());

tek bir operasyonda tüm Foos'u Barlara dönüştürmek ve hepsine merhaba demek için geçerli bir durum gibi görünüyor.

Gibi bir şeyden daha verimli ve zarif görünüyor:

List<Bar> bars = list.stream().map(foo->foo.getBar()).collect(Collectors.toList());
bars.forEach(bar->bar.publish("HELLO"));
return bars;

ve bir koleksiyonu iki kez yinelemezsiniz.


4

Her şeyi bir terminal yöntemine geçirilen basit veya oluşturulmuş bir işleve doldurmak yerine, akış nesnelerini değiştirebilen veya küresel durumu (bunlara dayalı olarak) değiştirebilen kodu ademi merkezileştirmepeek yeteneği sağladığını söyleyebilirim .

Şimdi soru şu olabilir: Akış nesnelerini mutasyona uğratmalı veya işlevsel durumu java programlamadaki işlevlerden küresel durumu değiştirmeli miyiz?

2 Yukarıdaki sorulardan herhangi birine cevabınız evet (veya: Bazı durumlarda evet de) daha sonra peek()ise kesinlikle sadece hata ayıklama amacıyla , aynı nedenle forEach()hata ayıklama amacıyla değil sadece .

Benim için forEach()ve arasında peek()seçim yaparken, aşağıdakileri seçiyor: Akış nesnelerini değiştiren kod parçalarının birleştirilebilir bir öğeye eklenmesini mi yoksa doğrudan akışa eklemelerini ister miyim?

peek()Java9 yöntemleri ile daha iyi eşleşeceğini düşünüyorum . örneğin takeWhile(), zaten mutasyon geçirmiş bir nesneye dayalı olarak yinelemenin ne zaman durdurulacağına karar vermesi gerekebilir, bu nedenle onu eşleştirmek forEach()aynı etkiye sahip olmaz.

PS Hiçbir map()yere başvurmadım çünkü yeni nesneler üretmek yerine nesneleri (veya küresel durumu) değiştirmek istersek, tam olarak aynı şekilde çalışır peek().


3

Yukarıdaki cevapların çoğuna katılmama rağmen, peek kullanımının aslında en temiz yol gibi göründüğü bir durum var.

Kullanım durumunuza benzer şekilde, yalnızca etkin hesaplara filtre uygulamak ve ardından bu hesaplarda bir giriş yapmak istediğinizi varsayalım.

accounts.stream()
    .filter(Account::isActive)
    .peek(login)
    .collect(Collectors.toList());

Peek, koleksiyonu iki kez tekrarlamak zorunda kalmadan gereksiz çağrıdan kaçınmak için yararlıdır:

accounts.stream()
    .filter(Account::isActive)
    .map(account -> {
        account.login();
        return account;
    })
    .collect(Collectors.toList());

3
Tek yapmanız gereken bu giriş yöntemini doğru yapmaktır. Gerçekten gözetlemenin en temiz yol olduğunu görmüyorum. Kodunuzu okuyan biri, API'yı gerçekten kötüye kullandığınızı nasıl bilmelidir? İyi ve temiz kod, bir okuyucuyu kod hakkında varsayımlar yapmaya zorlamaz.
kaba713

1

İşlevsel çözüm, hesap nesnesini değiştirilemez hale getirmektir. Yani account.login () yeni bir hesap nesnesi döndürmelidir. Bu, harita işleminin peek yerine oturum açma için kullanılabileceği anlamına gelir.

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.