it-swarm.com.de

Java 8: Bevorzugter Weg, Iterationen eines Lambda zu zählen?

Ich habe oft das gleiche Problem. Ich muss die Läufe eines Lambda für den Einsatz außerhalb des Lambda zählen. Z.B.:

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

Das Problem ist, dass runCount endgültig sein muss, es kann also kein int sein. Es kann keine Ganzzahl sein, da dies unveränderlich ist. Ich könnte es Klassenvariable machen (d. H. Ein Feld), aber ich brauche es nur in diesem Codeblock ... Ich weiß, dass es verschiedene Möglichkeiten gibt. Verwenden Sie einen AtomicInteger oder eine Arrayreferenz oder auf andere Weise? 

53
user4159962

Lassen Sie mich aus Gründen der Diskussion ein wenig umformatieren:

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

Wenn Sie really einen Zähler von einem Lambda aus inkrementieren müssen, ist es normalerweise üblich, den Zähler als AtomicInteger oder AtomicLong zu definieren und dann eine der Inkrementierungsmethoden aufzurufen.

Sie können ein int- oder ein long-Array mit einem einzelnen Element verwenden. Dies hätte jedoch Race-Bedingungen, wenn der Stream parallel ausgeführt wird.

Beachten Sie jedoch, dass der Stream in forEach endet, was bedeutet, dass es keinen Rückgabewert gibt. Sie könnten die forEach in eine peek umwandeln, die die Elemente weiterleitet, und sie dann zählen:

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

Das ist etwas besser, aber immer noch etwas seltsam. Der Grund ist, dass forEach und peek nur über Nebeneffekte arbeiten können. Der aufkommende Funktionsstil von Java 8 besteht darin, Nebenwirkungen zu vermeiden. Wir haben ein wenig davon gemacht, indem wir das Inkrement des Zählers in eine count-Operation im Stream extrahiert haben. Andere typische Nebenwirkungen sind das Hinzufügen von Elementen zu Sammlungen. In der Regel können diese über Kollektoren ausgetauscht werden. Aber ohne zu wissen, welche Arbeit Sie tatsächlich machen wollen, kann ich nichts genaueres vorschlagen.

52
Stuart Marks

Als Alternative zur Synchronisierung mit AtomicInteger kann stattdessen ein Integer-Array verwendet werden. Solange der Referenz auf das Array kein anderes Array erhält (und das ist der Punkt), kann sie als final - Variable verwendet werden, während sich die Werte der Felder ändern können willkürlich.

    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
    });
24
Duke Spray
AtomicInteger runCount = 0L;
long runCount = myStream.stream()
    .filter(...)
    .peek(item -> { 
        foo();
        bar();
        runCount.incrementAndGet();
    });
System.out.println("The lambda ran " + runCount.incrementAndGet() + "times");
6
Leandruz

Eine andere Möglichkeit, dies zu tun (nützlich, wenn Sie möchten, dass Ihre Zählung in einigen Fällen nur inkrementiert wird, wenn beispielsweise eine Operation erfolgreich war), ist etwa Folgendes: Verwenden Sie mapToInt() und 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");

Wie Stuart Marks bemerkte, ist dies immer noch etwas seltsam, da Nebenwirkungen (abhängig von den Funktionen von foo() und bar()) nicht vollständig vermieden werden.

Eine andere Möglichkeit, eine Variable in einem Lambda zu erhöhen, auf die außerhalb von ihr zugegriffen werden kann, ist die Verwendung einer Klassenvariablen:

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

In diesem Beispiel macht die Verwendung einer Klassenvariablen für einen Zähler in einer Methode wahrscheinlich keinen Sinn. Daher würde ich davor warnen, es sei denn, es gibt einen guten Grund dafür. Wenn Variablenvariablen final beibehalten werden, kann dies im Hinblick auf die Thread-Sicherheit usw. hilfreich sein (siehe http://www.javapractices.com/topic/TopicAction.do?Id=23 für eine Diskussion zur Verwendung von final). 

Um eine bessere Vorstellung davon zu bekommen, warum Lambdas so arbeiten, wie sie funktionieren, hat https://www.infoq.com/articles/Java-8-Lambdas-A-Peek-Unter- the-Hood einen genaueren Blick.

3
A. D.

Für mich hat dies den Trick getan, hoffentlich findet es jemand nützlich:

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

die Dokumentation zu getAndIncrement () Java lautet:

Erhöht den aktuellen Wert atomar mit Speichereffekten, wie in VarHandle.getAndAdd angegeben. Entspricht getAndAdd (1).

2
Jose Ripoll

Sie sollten nicht verwenden AtomicInteger, Sie sollten keine Dinge verwenden, es sei denn, Sie haben einen wirklich guten Grund zu verwenden. Und der Grund für die Verwendung von AtomicInteger ist möglicherweise, dass nur gleichzeitige Zugriffe zugelassen werden.

Wenn es um dein Problem geht;

Der Halter kann zum Halten und Inkrementieren in einem Lambda verwendet werden. Und nachdem Sie es erhalten können, indem Sie runCount.value aufrufen

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");
1
Ahmet Orhan

funktioniert auch reduzieren, man kann es so benutzen

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

Wenn Sie ein Feld nicht erstellen möchten, weil Sie es nur lokal benötigen, können Sie es in einer anonymen Klasse speichern:

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

Komisch, ich weiß. Die temporäre Variable bleibt jedoch auch außerhalb des lokalen Bereichs.

0
shmosel