Java 8: Preferred way to count iterations of a lam

2019-03-11 13:43发布

问题:

I face the same problem often. I need to count the runs of a lambda for use outside the lambda. E.g.:

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

The issue is that runCount needs to be final, so it cannot be an int. It cannot be an Integer because that's immutable. I could make it class level variable (i.e. a field) but I'll only need it in this block of code. I know there are various ways, I'm just curious what is your preferred solution for this? Do you use an AtomicInteger or an array reference or some other way?

回答1:

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.



回答2:

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


回答3:

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


回答4:

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.



回答5:

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.