Java8: 计算 lambda 迭代次数的首选方法?

我经常面临同样的问题,我需要计算一个 LambdaLambda之外使用的运行次数。

例如:

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

问题是 RunCount需要是 final,所以它不能是 int。它不可能是 Integer,因为它是 永恒不变。我可以使它成为类级别变量(比如一个字段) ,但是我只需要在这段代码中使用它。

我知道有各种各样的方法,我只是好奇你对此的首选解决方案是什么?
您是否使用 AtomicInteger或数组引用或其他方式?

106181 次浏览

Let me reformat your example a bit for the sake of discussion:

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

If you really need to increment a counter from within a lambda, the typical way to do so is to make the counter an AtomicInteger or AtomicLong and then call one of the increment methods on it.

You could use a single-element int or long array, but that would have race conditions if the stream is run in parallel.

But notice that the stream ends in forEach, which means that there is no return value. You could change the forEach to a peek, which passes the items through, and then count them:

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

This is somewhat better, but still a bit odd. The reason is that forEach and peek can only do their work via side effects. The emerging functional style of Java 8 is to avoid side effects. We did a little of that by extracting the increment of the counter into a count operation on the stream. Other typical side effects are adding items to collections. Usually these can be replaced via use of collectors. But without knowing what actual work you're trying to do, I can't suggest anything more specific.

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

As an alternative to sync hassling AtomicInteger one could use an integer array instead. As long as the reference to the array does not get another array assigned (and that's the point) it can be used as a final variable while the values of the fields can change arbitrarily.

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

Another way of doing this (useful if you'd like your count to only be incremented in some cases, like if an operation was successful) is something like this, using mapToInt() and 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");

As Stuart Marks noted, this is still somewhat odd, because it's not completely avoiding side effects (depending on what foo() and bar() are doing).

And another way of incrementing a variable in a lambda that's accessible outside of it is to use a class variable:

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 this example, using a class variable for a counter in one method probably doesn't make sense, so I'd caution against it unless there's a good reason to. Keeping class variables final if possible can be helpful in terms of thread safety, etc (see http://www.javapractices.com/topic/TopicAction.do?Id=23 for a discussion on using final).

To get a better idea of why lambdas work the way they do, https://www.infoq.com/articles/Java-8-Lambdas-A-Peek-Under-the-Hood has a detailed look.

If you don't want to create a field because you only need it locally, you can store it in an anonymous class:

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

Weird, I know. But it does keep the temporary variable out of even local scope.

For me, this did the trick, hopefully someone finds it useful:

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

getAndIncrement() Java documentation states :

Atomically increments the current value, with memory effects as specified by VarHandle.getAndAdd. Equivalent to getAndAdd(1).

reduce also works,you can use it like this

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

You shouldn't use AtomicInteger, you shouldn't use things unless you have a really good reason to use. And the reason for using AtomicInteger might be only allowing concurrent accesses or such as.

When it comes to your problem;

Holder can be use for holding and incrementing it inside a lambda. And after you can get it by calling 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");
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

Another alternative is to use 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");

An enum can be used, too. Especially if you have more than one counter in an iteration:

import java.util.Arrays;


class LambdaCounter {


enum CountOf {


NO,
OK,
ERROR;


private int count;


// can be named inc(), instead of the Greek capital Delta,
// which stands for the math increment operator '∆' <https://en.wikipedia.org/wiki/%E2%88%86>
synchronized int Δ( final int... times ) {


if ( times.length <= 0 )
return ++count; // increase by 1


return count += Arrays.stream( times ).sum(); // increase by arguments
}


// can be named val[ue](), instead of the Greek capital Xi,
// which stands for the math identity operator '≡' <https://en.wikipedia.org/wiki/Triple_bar>
int Ξ() {
return count;
}
}


public static void main( final String[] args ) {


Arrays.stream( new int[] { 1, 2, 3, 4, 5, 6, 7 } )
.forEach( i -> {
CountOf.NO.Δ();
@SuppressWarnings( "unused" )
final int LHS_DUMMY =
i % 2 == 0
? CountOf.OK.Δ()
: CountOf.ERROR.Δ();
} );
System.out.printf( "No: %d, OK: %d, Error: %d, Error.inc(38): %d, Error.inc(4, 4): %d%n",
CountOf.NO.Ξ(), CountOf.OK.Ξ(), CountOf.ERROR.Ξ(), CountOf.ERROR.Δ( 38 ), CountOf.ERROR.Δ( 4, 4 ) );


// Output:
// No: 7, OK: 3, Error: 4, Error.inc(38): 42, Error.inc(4, 4): 50
}
}

For me, this is the most elegant way.

long count = list.stream()
.peek(/* do your stuff here */)
.long();

There is a bug in JDK 9,10 that prevents the above solution from working, but you can work around it as follows. https://bugs.openjdk.java.net/browse/JDK-8198356

long count = list.stream()
.peek(/* do your stuff here */)
.collect(Collectors.counting());