Java 8: Lambda'nın yinelemelerini saymanın tercih edilen yolu?


84

Aynı problemle sık sık karşılaşıyorum. Lambda dışında kullanmak için bir lambda sayısını saymam gerekiyor .

Örneğin:

myStream.stream().filter(...).forEach(item -> { ... ; runCount++});
System.out.println("The lambda ran " + runCount + "times");

Sorun, runCount'un olması gerektiğidir final, bu nedenle bir int. O olamaz Integerçünkü bu değişmez . Bunu sınıf seviyesinde değişken (yani bir alan) yapabilirdim ama ona sadece bu kod bloğunda ihtiyacım olacak.

Bunun çeşitli yolları olduğunu biliyorum, bunun için tercih ettiğiniz çözümün ne olduğunu merak ediyorum.
Bir kullanıyor musunuz AtomicIntegerveya bir dizi başvurusu veya başka bir şekilde?


7
@ Sliver2009 Hayır değil.
Rohit Jain

4
@Florian Burayı kullanmalısın AtomicInteger.
Rohit Jain

Yanıtlar:


72

Tartışma uğruna örneğinizi biraz yeniden biçimlendirmeme izin verin:

long runCount = 0L;
myStream.stream()
    .filter(...)
    .forEach(item -> { 
        foo();
        bar();
        runCount++; // doesn't work
    });
System.out.println("The lambda ran " + runCount + " times");

Eğer varsa gerçekten bir lambda içinden bir sayacını artırmak gerekir, bunu yapmak için tipik bir yol sayacı bir hale getirmektir AtomicIntegerveya AtomicLongsonra üzerine artım yöntemlerinden birini arayın.

Tek öğe intveya longdizi kullanabilirsiniz, ancak akış paralel olarak çalıştırılırsa bu, yarış koşullarına sahip olacaktır.

Ancak akışın sona erdiğine dikkat edin forEach, bu da dönüş değeri olmadığı anlamına gelir. Öğeleri geçiren forEacha olarak değiştirebilir peekve ardından bunları sayabilirsiniz:

long runCount = myStream.stream()
    .filter(...)
    .peek(item -> { 
        foo();
        bar();
    })
    .count();
System.out.println("The lambda ran " + runCount + " times");

Bu biraz daha iyi, ama yine de biraz tuhaf. Nedeni şudur forEachve peekişlerini yalnızca yan etkiler yoluyla yapabilir. Java 8'in ortaya çıkan işlevsel stili, yan etkilerden kaçınmaktır. Sayaç artışını countakış üzerinde bir işleme çıkararak biraz yaptık . Diğer tipik yan etkiler koleksiyonlara ürün eklemektir. Genellikle bunlar kollektörler kullanılarak değiştirilebilir. Ama ne tür fiili işi yapmaya çalıştığınızı bilmeden daha spesifik bir şey öneremem.


9
Uygulamalar akışlar için kısayollar kullanmaya başladığında peekçalışmayı durdurduğuna dikkat edilmelidir . Bu, ed stream ile ilgili hiçbir zaman bir sorun olmayabilir, ancak biri kodu daha sonra değiştirirse büyük sürprizler yaratabilir ...countSIZEDfilter
Holger

16
final AtomicInteger i = new AtomicInteger(1);Lambda kullanımınızda bir yerlerde beyan edin i.getAndAdd(1). Dur ve int i=1; ... i++eskiden ne kadar güzel olduğunu hatırla .
aliopi

2
Java Incrementable, sayısal sınıflar gibi arabirimleri uygularsa AtomicIntegerve işleçleri ++süslü görünen işlevler olarak bildirirse, operatör aşırı yüklemesine ihtiyacımız olmazdı ve yine de çok okunabilir koda sahip oluruz.
Önem DerecesiOne

38

Zahmetli AtomicInteger senkronizasyonuna alternatif olarak bunun yerine tamsayı dizisi kullanılabilir. Diziye yapılan başvuru başka bir dizi atanmadığı sürece (ve nokta budur) , alanların değerleri keyfi olarak değişebilirken , son değişken olarak kullanılabilir .

    int[] iarr = {0}; // final not neccessary here if no other array is assigned
    stringList.forEach(item -> {
            iarr[0]++;
            // iarr = {1}; Error if iarr gets other array assigned
    });

Başvurunun başka bir dizi atanmadığından emin olmak istiyorsanız, iarr'ı son değişken olarak bildirebilirsiniz. Ancak @pisaruk'un da belirttiği gibi, bu paralel olarak çalışmayacak.
themathmagician

1
Bence, foreachdoğrudan toplama üzerinde (akış olmadan), bu yeterince iyi bir yaklaşım. Teşekkürler !!
Sabir Khan

İşleri paralel yürütmediğiniz sürece bu en basit çözümdür.
David DeMar

17
AtomicInteger runCount = 0L;
long runCount = myStream.stream()
    .filter(...)
    .peek(item -> { 
        foo();
        bar();
        runCount.incrementAndGet();
    });
System.out.println("The lambda ran " + runCount.incrementAndGet() + "times");

15
Lütfen daha fazla bilgi ile düzenleyin . Yalnızca kod ve "bunu dene" yanıtları, aranabilir içerik içermediğinden ve birisinin neden "bunu denemesi" gerektiğini açıklamadığından önerilmez . Burada bilgi kaynağı olmak için çaba sarf ediyoruz.
Mogsdad

7
Cevabınız kafamı karıştırıyor. Her ikisinin de adlandırılmış iki değişkeniniz var runCount. Sanırım bunlardan sadece birine sahip olmak istediniz, ama hangisi?
Ole VV

1
RunCount.getAndIncrement () öğesini daha uygun buldum. Mükemmel cevap!
kospol

2
AtomicIntegerBana yardım etti ama onu init olurnew AtomicInteger(0)
Stefan Höltker

1) Bu kod derlenmez: akışta uzun döndüren uçbirim işlemi yoktur 2) Olsa bile, 'runCount' değeri her zaman '1' olur: - akım uçbirim işlemi içermez, bu yüzden peek () lambda argümanı hiçbir zaman çağrılmayacak - System.out satırı, göstermeden önce çalıştırma sayısını artırır
Cédric

9

Sen olmamalıdır Kullanmak gerçekten iyi bir nedeniniz olmadıkça şeyler kullanmamalıdır, AtomicInteger kullanın. Ve AtomicInteger'ı kullanmanın nedeni sadece eşzamanlı erişimlere izin vermek olabilir.

Sorununuza gelince;

Tutucu, bir lambda içinde tutmak ve artırmak için kullanılabilir. Ve sonra runCount.value çağırarak elde edebilirsiniz.

Holder<Integer> runCount = new Holder<>(0);

myStream.stream()
    .filter(...)
    .forEach(item -> { 
        foo();
        bar();
        runCount.value++; // now it's work fine!
    });
System.out.println("The lambda ran " + runCount + " times");

JDK'da birkaç Holder sınıfı var. Bu öyle görünüyor javax.xml.ws.Holder.
Brad Cupit

1
Gerçekten mi? Ve neden?
lkahtz

1
Kabul ediyorum - lamda / akışta eşzamanlı işlem yapmadığımı biliyorsam, neden eşzamanlılığı sağlamak için tasarlanmış AtomicInteger'ı kullanmak isteyeyim ki bu, kilitlemeyi vb. Ortaya çıkarabilir, yani yıllar önce neden, JDK, yeni koleksiyon setini ve herhangi bir kilitleme yapmayan yineleyicilerini tanıttı - neden bir performans azaltma özelliğine sahip bir şey yüklensin, örneğin kilitleme birçok senaryo için gerekli olmadığında kilitleme.
Volksman

5

Benim için bu hile yaptı, umarım birisi faydalı bulmuştur:

AtomicInteger runCount = new AtomicInteger(0);
myStream.stream().filter(...).forEach(item -> runCount.getAndIncrement());
System.out.println("The lambda ran " + runCount.get() + "times");

getAndIncrement() Java dokümantasyonu durumları:

VarHandle.getAndAdd tarafından belirtildiği gibi bellek efektleriyle mevcut değeri atomik olarak artırır. GetAndAdd (1) ile eşdeğerdir.


3

Bunu yapmanın başka bir yolu (sayınızın yalnızca bazı durumlarda artmasını istiyorsanız yararlıdır, örneğin bir işlem başarılı olmuşsa), mapToInt()ve kullanarak şunun gibi bir şeydir sum():

int count = myStream.stream()
    .filter(...)
    .mapToInt(item -> { 
        foo();
        if (bar()){
           return 1;
        } else {
           return 0;
    })
    .sum();
System.out.println("The lambda ran " + count + "times");

Stuart Marks'ın da belirttiği gibi, bu hala biraz garip çünkü yan etkilerden tamamen kaçınmıyor (ne yaptığına foo()ve ne bar()yaptığına bağlı olarak ).

Ve bir lambda içindeki bir değişkeni dışarıdan erişilebilen artırmanın başka bir yolu da bir sınıf değişkeni kullanmaktır:

public class MyClass {
    private int myCount;

    // Constructor, other methods here

    void myMethod(){
        // does something to get myStream
        myCount = 0;
        myStream.stream()
            .filter(...)
            .forEach(item->{
               foo(); 
               myCount++;
        });
    }
}

Bu örnekte, bir yöntemde bir sayaç için bir sınıf değişkeni kullanmak muhtemelen mantıklı değildir, bu nedenle iyi bir neden olmadıkça buna karşı uyarırım. finalMümkünse sınıf değişkenlerini tutmak , iş parçacığı güvenliği, vb. Açısından yardımcı olabilir (kullanımla ilgili bir tartışma için bkz. Http://www.javapractices.com/topic/TopicAction.do?Id=23final ).

Lambdaların neden bu şekilde çalıştıklarına dair daha iyi bir fikir edinmek için https://www.infoq.com/articles/Java-8-Lambdas-A-Peek-Under-the-Hood ayrıntılı bir görünüme sahiptir.


3

Yalnızca yerel olarak ihtiyacınız olduğu için bir alan oluşturmak istemiyorsanız, onu anonim bir sınıfta saklayabilirsiniz:

int runCount = new Object() {
    int runCount = 0;
    {
        myStream.stream()
                .filter(...)
                .peek(x -> runCount++)
                .forEach(...);
    }
}.runCount;

Garip, biliyorum. Ancak geçici değişkeni yerel kapsamın dışında tutuyor.


2
burada neler oluyor, biraz daha fazla açıklamaya ihtiyacım var thx
Alexander Mills

Temel olarak, anonim bir sınıftaki bir alanı artırmak ve almaktır. Bana özellikle neyin karıştığını söylersen, açıklığa kavuşturmaya çalışabilirim.
shmosel

1
@MrCholo Bu bir başlatıcı blok . Yapıcıdan önce çalışır.
shmosel

1
@MrCholo Hayır, bu bir örnek başlatıcı.
shmosel

1
@MrCholo Anonim bir sınıfın açıkça bildirilmiş bir kurucusu olamaz.
shmosel

1

azalt da çalışır, böyle kullanabilirsin

myStream.stream().filter(...).reduce((item, sum) -> sum += item);

1

Diğer bir alternatif ise apache commons MutableInt kullanmaktır.

MutableInt cnt = new MutableInt(0);
myStream.stream()
    .filter(...)
    .forEach(item -> { 
        foo();
        bar();
        cnt.increment();
    });
System.out.println("The lambda ran " + cnt.getValue() + " times");

0
AtomicInteger runCount = new AtomicInteger(0);

elements.stream()
  //...
  .peek(runCount.incrementAndGet())
  .collect(Collectors.toList());

// runCount.get() should have the num of times lambda code was executed
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.