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?


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.


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

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");

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");

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).


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.


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.


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

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");

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
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.