Java 8: preferowany sposób zliczania iteracji lambda?

84

Często mam ten sam problem. Muszę policzyć przebiegi lambda do użycia poza lambdą .

Na przykład:

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

Problem polega na tym, że runCount musi być final, więc nie może to być int. Nie może to być, Integerponieważ jest niezmienne . Mógłbym uczynić to zmienną na poziomie klasy (tj. Pole), ale będę potrzebować jej tylko w tym bloku kodu.

Wiem, że są różne sposoby, jestem po prostu ciekawy, jakie jest Twoje preferowane rozwiązanie?
Czy używasz AtomicIntegerodwołania do tablicy lub w inny sposób?

informatik01
źródło
7
@ Sliver2009 Nie, nie jest.
Rohit Jain
4
@Florian Musisz użyć AtomicIntegertutaj.
Rohit Jain

Odpowiedzi:

72

Pozwólcie, że przeformatuję nieco twój przykład na potrzeby dyskusji:

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

Jeśli naprawdę potrzebujesz zwiększyć licznik z poziomu lambdy, typowym sposobem na to jest ustawienie licznika na AtomicIntegeror, AtomicLonga następnie wywołanie jednej z metod inkrementacji.

Możesz użyć pojedynczego elementu intlub longtablicy, ale miałoby to warunki wyścigu, gdyby strumień był uruchamiany równolegle.

Ale zauważ, że strumień kończy się na forEach, co oznacza, że ​​nie ma wartości zwracanej. Możesz zmienić na forEacha peek, który przekazuje elementy, a następnie je policzyć:

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

To jest trochę lepsze, ale wciąż trochę dziwne. Powodem jest to forEachi peekmogą wykonywać swoją pracę tylko poprzez skutki uboczne. Wyłaniający się styl funkcjonalny Java 8 polega na unikaniu skutków ubocznych. Zrobiliśmy trochę tego, wyodrębniając przyrost licznika do countoperacji na strumieniu. Inne typowe skutki uboczne to dodawanie elementów do kolekcji. Zwykle można je wymienić za pomocą kolektorów. Ale nie wiedząc, jaką rzeczywistą pracę próbujesz wykonać, nie mogę zasugerować nic bardziej szczegółowego.

Stuart Marks
źródło
9
Należy zauważyć, że peekprzestaje działać, gdy countimplementacje zaczną używać skrótów dla SIZEDstrumieni. To może nigdy nie stanowić problemu z filtered streamem, ale może stworzyć wielkie niespodzianki, jeśli ktoś zmieni kod w późniejszym czasie…
Holger
16
Zadeklaruj final AtomicInteger i = new AtomicInteger(1);i gdzieś w swojej lambdzie użyj i.getAndAdd(1). Zatrzymaj się i pamiętaj, jak miło było int i=1; ... i++kiedyś
aliopi
2
Gdyby Java implementowała interfejsy, takie jak Incrementablew klasach numerycznych, w tym AtomicIntegeri deklarowała operatory, takie jak ++funkcje wyglądające na fantazyjne, nie potrzebowalibyśmy przeciążania operatorów i nadal mielibyśmy bardzo czytelny kod.
SeverityOne
38

Jako alternatywę dla kłopotliwej synchronizacji AtomicInteger można zamiast tego użyć tablicy liczb całkowitych . Dopóki odwołanie do tablicy nie ma przypisanej innej tablicy (i o to chodzi), może być użyte jako zmienna końcowa , podczas gdy wartości pól mogą się dowolnie zmieniać .

    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
    });
Duke Spray
źródło
Jeśli chcesz mieć pewność, że odwołanie nie otrzyma innej tablicy, możesz zadeklarować iarr jako zmienną końcową. Ale jak wskazuje @pisaruk, nie będzie to działać równolegle.
themathmagician
1
Myślę, że w przypadku prostego foreachbezpośredniego odbioru (bez strumieni) jest to wystarczająco dobre podejście. dzięki !!
Sabir Khan
To najprostsze rozwiązanie, o ile nie prowadzisz rzeczy równolegle.
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");
Leandruz
źródło
15
Podobać się zmienił z dodatkowymi informacjami. Odpowiedzi zawierające tylko kod i „wypróbuj to” są odradzane , ponieważ nie zawierają treści, które można przeszukiwać, i nie wyjaśniają, dlaczego ktoś powinien „spróbować tego”. Dokładamy wszelkich starań, aby być źródłem wiedzy.
Mogsdad
7
Twoja odpowiedź mnie dezorientuje. Masz dwie zmienne, obie nazwane runCount. Podejrzewam, że chciałeś mieć tylko jeden z nich, ale który?
Ole VV
1
Uważam, że bardziej odpowiednia jest metoda runCount.getAndIncrement (). Świetna odpowiedź!
kospol
2
AtomicIntegerMi pomógł, ale chciałbym init, tonew AtomicInteger(0)
Stefan Höltker
1) Ten kod nie skompiluje się: strumień nie ma operacji terminalowej, która zwraca long 2) Nawet gdyby tak było, wartość 'runCount' zawsze wynosiłaby '1': - strumień nie ma operacji terminalowej, więc peek () argument lambda nigdy nie zostanie wywołany - Linia System.out zwiększa liczbę uruchomień przed jej wyświetleniem
Cédric
9

Nie powinieneś używać AtomicInteger, nie powinieneś używać rzeczy, chyba że masz naprawdę dobry powód, aby ich używać. Przyczyną korzystania z AtomicInteger może być tylko zezwolenie na równoczesny dostęp lub takie jak.

Jeśli chodzi o twój problem;

Uchwyt może służyć do przechowywania i zwiększania wartości wewnątrz lambda. A potem możesz to uzyskać, wywołując runCount.value

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");
Ahmet Orhan
źródło
W JDK jest kilka klas Holder. Ten wydaje się być javax.xml.ws.Holder.
Brad Cupit
1
Naprawdę? I dlaczego?
lkahtz
1
Zgadzam się - jeśli wiem, że nie wykonuję żadnych współbieżnych operacji w lamdzie / strumieniu, dlaczego miałbym chcieć użyć AtomicInteger, który został zaprojektowany do obsługi współbieżności - który może wprowadzić blokowanie itp., Czyli powód, dla którego wiele lat temu JDK wprowadził nowy zestaw kolekcji i ich iteratorów, które nie wykonywały żadnego blokowania - po co obciążać coś zdolnością zmniejszającą wydajność, np. blokowanie, gdy blokowanie nie jest wymagane w wielu scenariuszach.
Volksman,
5

Dla mnie to załatwiło sprawę, mam nadzieję, że ktoś uzna to za przydatne:

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

getAndIncrement() Dokumentacja Java stwierdza:

Atomowo zwiększa bieżącą wartość z efektami pamięci określonymi przez VarHandle.getAndAdd. Odpowiednik getAndAdd (1).

José Ripoll
źródło
3

Innym sposobem zrobienia tego (przydatnym, jeśli chcesz, aby liczba była zwiększana tylko w niektórych przypadkach, na przykład w przypadku pomyślnej operacji), jest coś takiego, jak użycie mapToInt()i 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");

Jak zauważył Stuart Marks, jest to nadal nieco dziwne, ponieważ nie pozwala całkowicie uniknąć skutków ubocznych (w zależności od tego, co foo()i bar()robisz).

Innym sposobem inkrementacji zmiennej w lambdzie, która jest dostępna poza nią, jest użycie zmiennej klasy:

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++;
        });
    }
}

W tym przykładzie użycie zmiennej klasy jako licznika w jednej metodzie prawdopodobnie nie ma sensu, więc ostrzegam przed tym, chyba że jest ku temu dobry powód. Utrzymywanie zmiennych klas, finaljeśli to możliwe, może być pomocne pod względem bezpieczeństwa wątków itp. (Zobacz http://www.javapractices.com/topic/TopicAction.do?Id=23, aby zapoznać się z dyskusją na temat używania final).

Aby lepiej zrozumieć, dlaczego lambdy działają tak, jak działają, https://www.infoq.com/articles/Java-8-Lambdas-A-Peek-Under-the-Hood ma szczegółowy wygląd.

OGŁOSZENIE
źródło
3

Jeśli nie chcesz tworzyć pola, ponieważ potrzebujesz go tylko lokalnie, możesz przechowywać je w anonimowej klasie:

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

Wiem, dziwne. Ale utrzymuje zmienną tymczasową poza zasięgiem lokalnym.

shmosel
źródło
2
co tu się dzieje, potrzebuję trochę więcej wyjaśnień dzięki
Alexander Mills
Zasadniczo jest to zwiększanie i pobieranie pola w klasie anonimowej. Jeśli powiesz mi, co Cię szczególnie zdezorientowało, spróbuję to wyjaśnić.
shmosel
1
@MrCholo To blok inicjalizujący . Działa przed konstruktorem.
shmosel
1
@MrCholo Nie, to inicjator instancji.
shmosel
1
@MrCholo Anonimowa klasa nie może mieć jawnie zadeklarowanego konstruktora.
shmosel
1

redukuje również działa, możesz go używać w ten sposób

myStream.stream().filter(...).reduce((item, sum) -> sum += item);
yao.qingdong
źródło
1

Inną alternatywą jest użycie apache commons MutableInt.

MutableInt cnt = new MutableInt(0);
myStream.stream()
    .filter(...)
    .forEach(item -> { 
        foo();
        bar();
        cnt.increment();
    });
System.out.println("The lambda ran " + cnt.getValue() + " times");
Vojtěch Fried
źródło
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
tsunllly
źródło