Java 8: Bevorzugte Methode zum Zählen von Iterationen eines Lambda?


82

Ich habe oft das gleiche Problem. Ich muss die Läufe eines Lambda für die Verwendung 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, daher kann es kein int sein. Es kann keine Ganzzahl sein, da dies unveränderlich ist. Ich könnte es auf Klassenebene variabel machen (dh ein Feld), aber ich werde es nur in diesem Codeblock brauchen. Ich weiß, dass es verschiedene Möglichkeiten gibt. Ich bin nur neugierig, was Ihre bevorzugte Lösung dafür ist. Verwenden Sie eine AtomicInteger- oder Array-Referenz oder eine andere Methode?


7
@ Sliver2009 Nein, ist es nicht.
Rohit Jain

4
@Florian Du musst AtomicIntegerhier verwenden.
Rohit Jain

Antworten:


70

Lassen Sie mich Ihr Beispiel zur Diskussion ein wenig neu formatieren:

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

Wenn Sie einen Zähler wirklich aus einem Lambda heraus inkrementieren müssen, besteht die typische Methode darin, den Zähler zu einem AtomicIntegeroder zu machen AtomicLongund dann eine der Inkrementierungsmethoden darauf aufzurufen.

Sie könnten ein einzelnes Element intoder longArray verwenden, dies hätte jedoch Race-Bedingungen, wenn der Stream parallel ausgeführt wird.

Beachten Sie jedoch, dass der Stream endet forEach, was bedeutet, dass es keinen Rückgabewert gibt. Sie können das forEachin a ändern peek, das die Elemente durchläuft, 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 forEachund peekkönnen ihre Arbeit nur über Nebenwirkungen erledigen. Der aufkommende Funktionsstil von Java 8 besteht darin, Nebenwirkungen zu vermeiden. Wir haben ein wenig davon getan, indem wir das Inkrement des Zählers in eine countOperation im Stream extrahiert haben . Andere typische Nebenwirkungen sind das Hinzufügen von Elementen zu Sammlungen. In der Regel können diese durch Verwendung von Kollektoren ersetzt werden. Aber ohne zu wissen, welche Arbeit Sie tatsächlich versuchen, kann ich nichts Spezifischeres vorschlagen.


8
Es sollte beachtet werden, dass dies nicht mehr peekfunktioniert, sobald countImplementierungen Verknüpfungen für SIZEDStreams verwenden. Das ist vielleicht nie ein Problem mit filtered stream, kann aber große Überraschungen bereiten, wenn jemand den Code zu einem späteren Zeitpunkt ändert…
Holger

14
Deklarieren final AtomicInteger i = new AtomicInteger(1);und irgendwo in Ihrem Lambda verwenden i.getAndAdd(1). Halte inne und erinnere dich, wie schön int i=1; ... i++früher war.
Aliopi

2
Wenn Java Schnittstellen wie beispielsweise Incrementablenumerische Klassen implementieren AtomicIntegerund Operatoren als ++ausgefallene Funktionen deklarieren würde, müssten die Operatoren nicht überladen werden und verfügen dennoch über gut lesbaren Code.
SeverityOne

37

Als Alternative zur Synchronisierung von AtomicInteger könnte stattdessen ein ganzzahliges Array verwendet werden. Solange dem Verweis auf das Array kein anderes Array zugewiesen wird (und das ist der Punkt), kann er als endgültige Variable verwendet werden, während sich die Werte der Felder beliebig ändern können.

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

Wenn Sie sicherstellen möchten, dass der Referenz kein anderes Array zugewiesen wird, können Sie iarr als endgültige Variable deklarieren. Aber wie @pisaruk betont, wird dies nicht parallel funktionieren.
Themathmagician

1
Ich denke, für einfache foreachdirekte Sammlung (ohne Streams) ist dies ein ausreichend guter Ansatz. Vielen Dank !!
Sabir Khan

Dies ist die einfachste Lösung, solange Sie die Dinge nicht parallel ausführen.
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
Bitte bearbeiten Sie mit weiteren Informationen. Von Nur-Code- und "Versuchen Sie dies" -Antworten wird abgeraten , da sie keinen durchsuchbaren Inhalt enthalten und nicht erklären, warum jemand "dies versuchen" sollte. Wir bemühen uns hier, eine Ressource für Wissen zu sein.
Mogsdad

7
Ihre Antwort verwirrt mich. Sie haben zwei Variablen benannt runCount. Ich vermute, Sie wollten nur einen von ihnen haben, aber welchen?
Ole VV

1
Ich fand runCount.getAndIncrement () besser geeignet. Gute Antwort!
Kospol

2
Das AtomicIntegerhat mir geholfen, aber ich würde es mitnew AtomicInteger(0)
Stefan Höltker

1) Dieser Code wird nicht kompiliert: Der Stream hat keine Terminaloperation, die eine lange zurückgibt. 2) Selbst wenn dies der Fall wäre, wäre der Wert von 'runCount' immer '1': - Der Stream hat keine Terminaloperation, also peek () Lambda-Argument wird niemals aufgerufen - Die Zeile System.out erhöht die Anzahl der Läufe, bevor sie angezeigt wird
Cédric

8

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

Wenn es um Ihr Problem geht;

Der Halter kann zum Halten und Inkrementieren in einem Lambda verwendet werden. Und nachdem Sie es erhalten haben, rufen Sie runCount.value auf

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

Es gibt einige Holder-Klassen im JDK. Dieser scheint zu sein javax.xml.ws.Holder.
Brad Cupit

1
"Ja wirklich?" Und warum?
lkahtz

1
Ich bin damit einverstanden - wenn ich weiß, dass ich keine gleichzeitigen Operationen im Lamda / Stream ausführe, warum sollte ich dann AtomicInteger verwenden, das für die Parallelität ausgelegt ist -, das möglicherweise Sperren usw. einführt, dh den Grund, warum vor vielen Jahren Das JDK führte die neuen Sammlungen und ihre Iteratoren ein, die keine Sperrung durchführten. Warum sollte etwas mit einer leistungsmindernden Funktion belastet werden, z. B. Sperren, wenn das Sperren in vielen Szenarien nicht erforderlich ist?
Volksman

4

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

In der Java-Dokumentation von getAndIncrement () heißt es:

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


3

Eine andere Möglichkeit, dies zu tun (nützlich, wenn Sie möchten, dass Ihre Anzahl nur in einigen Fällen erhöht wird, z. B. wenn eine Operation erfolgreich war), ist die Verwendung von 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 nicht vollständig vermieden werden (je nachdem, was foo()und was bar()getan wird).

Eine andere Möglichkeit, eine Variable in einem Lambda zu erhöhen, auf das außerhalb 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 ist die Verwendung einer Klassenvariablen für einen Zähler in einer Methode wahrscheinlich nicht sinnvoll. Daher würde ich davor warnen, es sei denn, es gibt einen guten Grund dafür. Das Beibehalten von Klassenvariablen, finalwenn möglich, kann im Hinblick auf die Thread-Sicherheit usw. hilfreich sein ( eine Diskussion zur Verwendung finden Sie unter http://www.javapractices.com/topic/TopicAction.do?Id=23final ).

Https://www.infoq.com/articles/Java-8-Lambdas-A-Peek-Under-the-Hood bietet einen detaillierten Überblick darüber, warum Lambdas so funktionieren, wie sie funktionieren .


3

Wenn Sie kein Feld 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;

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


2
was auf der Erde ist hier los, in der Notwendigkeit einer wenig mehr Erklärung thx
Alexander Mills

Grundsätzlich wird ein Feld in einer anonymen Klasse erhöht und abgerufen. Wenn Sie mir sagen, wovon Sie besonders verwirrt sind, kann ich versuchen, dies zu klären.
Shmosel

1
@ MrCholo Es ist ein Initialisierungsblock . Es läuft vor dem Konstruktor.
Shmosel

1
@ MrCholo Nein, es ist ein Instanzinitialisierer.
Shmosel

1
@ MrCholo Eine anonyme Klasse kann keinen explizit deklarierten Konstruktor haben.
Shmosel

1

Reduzieren funktioniert auch, Sie können es so verwenden

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

1

Eine andere Alternative ist die Verwendung von 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
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.