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?
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.
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
});
AtomicInteger runCount = 0L;
long runCount = myStream.stream()
.filter(...)
.peek(item -> {
foo();
bar();
runCount.incrementAndGet();
});
System.out.println("The lambda ran " + runCount.incrementAndGet() + "times");
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.
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).
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");
funktioniert auch reduzieren, man kann es so benutzen
myStream.stream().filter(...).reduce((item, sum) -> sum += item);
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.