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

113

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

例如:

myStream.stream().filter(...).forEach(item -> { ... ; runCount++});
System.out.println("The lambda ran " + runCount + "times");
问题在于runCount需要是final,因此它不能是一个int。它也不能是Integer,因为它是不可变的。
我可以将其作为类级别变量(即字段)进行声明,但我只需要在这段代码中使用它。
我知道有各种方法,只是好奇你偏爱哪种解决方案?你会使用AtomicInteger还是数组引用或其他方式?

7
不,不是这样。 - Rohit Jain
4
你需要在这里使用AtomicInteger - Rohit Jain
12个回答

83

为了讨论方便,让我稍微调整一下您的示例格式:

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

如果你确实需要在lambda内增加计数器,通常的做法是将计数器设置为AtomicIntegerAtomicLong,然后调用其中一个增量方法。
你可以使用单个元素的intlong数组,但如果流以并行方式运行,则会出现竞态条件。
但请注意,流以forEach结束,这意味着没有返回值。你可以将forEach更改为peek,通过它传递项目,然后对它们进行计数:
long runCount = myStream.stream()
    .filter(...)
    .peek(item -> { 
        foo();
        bar();
    })
    .count();
System.out.println("The lambda ran " + runCount + " times");

这已经有所改善,但仍然有些奇怪。原因是forEachpeek只能通过副作用来完成它们的工作。Java 8新出现的函数式风格是避免副作用。我们通过在Stream中提取计数器增量并将其转化为count操作来实现了一部分。其他典型的副作用包括向集合添加元素。通常可以通过使用收集器来替换它们。但如果不知道您要执行的实际任务,我就不能提供更具体的建议。

9
需要注意的是,一旦count实现开始为SIZED流使用快捷方式,peek方法就会停止工作。这可能永远不会成为filter流的问题,但如果有人稍后更改代码,则可能会带来很大的意外。 - Holger
26
声明 final AtomicInteger i = new AtomicInteger(1);,然后在 lambda 表达式中使用 i.getAndAdd(1)。停下来回想一下以前 int i=1; ... i++ 的写法有多好用。 - aliopi
4
如果Java在数字类(包括AtomicInteger)上实现诸如“Incrementable”之类的接口,并将“++”等运算符声明为看起来时髦的函数,那么我们就不需要操作符重载,仍然能够编写非常易读的代码。 - SeverityOne

49

作为同步麻烦的 AtomicInteger 的替代方案,可以使用一个整数数组。只要不将另一个数组分配给该数组的引用(这就是关键),它就可以被用作最终变量,而字段的值可以任意更改。

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

如果您想确保引用不会被分配给另一个数组,可以将iarr声明为final变量。但正如@pisaruk所指出的那样,在并行处理中这种方法是行不通的。 - themathmagician
3
我认为,对于直接在集合上使用简单的foreach(不使用流),这是一个足够好的方法。谢谢! - Sabir Khan
1
这是最简单的解决方案,只要你不并行运行事物。 - David DeMar

19

对我来说,这个方法很有效,希望能有人觉得有用:

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

getAndIncrement()的Java文档说明:

原子性地增加当前值,具有由VarHandle.getAndAdd指定的内存效果。等同于getAndAdd(1)。


我有:AtomicInteger rowCount = new AtomicInteger(0); items.stream() .map(x -> (String.format("%3s. %-45s - count: %s", rowCount.getAndIncrement(), x.getName(), x.getSize()))) .forEach(System.out::println); - Sasha Bond

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

19
请[编辑]并提供更多信息。仅包含代码和“试试这个”类型的答案是不被鼓励的,因为它们缺乏可搜索的内容,并且没有解释为什么应该“试试这个”。我们在这里努力成为一个知识资源。 - Mogsdad
7
你的回答让我感到困惑。你有两个变量都命名为runCount。我怀疑你本意只想要其中一个,但是是哪一个呢? - Ole V.V.
1
我发现runCount.getAndIncrement()更合适。非常好的答案! - kospol
5
AtomicInteger 对我很有帮助,但是我需要使用 new AtomicInteger(0) 进行初始化。 - Stefan Höltker
3
1)这段代码无法编译:流没有返回长整型的终端操作。 2)即使有,'runCount' 的值始终为'1':
  • 流没有终端操作,因此peek() lambda参数将永远不会被调用。
  • System.out行在显示之前增加了运行计数。
- Cédric

12

你不应该使用AtomicInteger,除非你确实有很好的理由使用它。而使用AtomicInteger的原因可能只是允许并发访问或类似的情况。

至于你的问题:

Holder可以用来在lambda中持有和递增它。之后,您可以通过调用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");

1
JDK中有几个Holder类。这个似乎是javax.xml.ws.Holder - Brad Cupit
3
真的吗?为什么? - lkahtz
2
我同意 - 如果我知道在lambda/stream中没有进行任何并发操作,为什么要使用设计用于处理并发的AtomicInteger - 这可能会引入锁等问题。也就是说,很多年前JDK引入了一组新的集合及其迭代器,它们不执行任何锁定操作 - 为什么要给某些场景增加性能降低的功能,例如锁定,而这些场景并不需要锁定。 - Volksman
2
Stream.forEach 明确是不确定性的,因此使用 Holder 可能会或可能不会取决于底层流。 - michid
2
@GeroldBroser 它在调用线程方面也是不确定的:“对于任何给定的元素,操作可能在库选择的任何时间和任何线程中执行”。这可能导致竞态条件,使结果无效。 - michid
显示剩余4条评论

5

如果您不想创建一个字段,因为您只需要在本地使用它,则可以将其存储在匿名类中:

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

奇怪,我知道。但是它确实避免了临时变量甚至在本地范围内存在的情况。

2
这里到底发生了什么事,需要更多的解释,谢谢。 - Alexander Mills
2
@MrCholo 这是一个初始化块。它在构造函数之前运行。 - shmosel
2
@MrCholo 不是构造函数,而是实例初始化程序。 - shmosel
2
@MrCholo 一个匿名类不能有显式声明的构造函数。 - shmosel
2
@OlivierGrégoire 我承认一开始看起来很混乱,但我不确定你为什么认为这会影响性能。这里的大多数解决方案都涉及对象创建。 - shmosel
显示剩余7条评论

5
另一种选择是使用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");

Linkservice: org.apache.commons.lang.mutable.MutableInt - Gerold Broser

3

如果您希望计数仅在某些情况下递增(例如操作成功时),可以使用以下方法之一,使用mapToInt()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");

正如Stuart Marks所指出的那样,这仍然有些奇怪,因为它并没有完全避免副作用(取决于foo()bar()在做什么)。
另一种在lambda中增加一个可在外部访问的变量的方法是使用类变量:
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++;
        });
    }
}

在这个例子中,一个方法中使用类变量作为计数器可能没有意义,所以我建议除非有充分的理由,否则不要这样做。如果可能的话,保持类变量为final可以有助于线程安全等方面(有关使用final的讨论,请参见http://www.javapractices.com/topic/TopicAction.do?Id=23)。
为了更好地了解为什么lambda起作用的方式是这样的,https://www.infoq.com/articles/Java-8-Lambdas-A-Peek-Under-the-Hood提供了详细的解释。

else 缺少一个 '}',而 if...else 可以缩短为 return bar() ? 1 : 0; - Gerold Broser

3
对我来说,这是最优雅的方式。
long count = list.stream()
  .peek(/* do your stuff here */)
  .count();

JDK 9和10中存在一个错误,导致上述解决方案无法正常工作,但您可以按照以下方式进行解决。 https://bugs.openjdk.java.net/browse/JDK-8198356
long count = list.stream()
  .peek(/* do your stuff here */)
  .collect(Collectors.counting());

我喜欢这个解决方案的“风格”,因为它纯粹是功能性的,不依赖于共享的可变状态。然而,请注意,评论和OpenJDK票证的解决说明明确指出观察到的行为并不是一个错误,而是一种性能优化。这是因为count()终端操作可以在不调用管道中的peek()部分的情况下确定其结果;因为peek()无法改变实际计数。 - sxc731

2

reduce函数也可以使用,可以像这样使用它

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接