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

Posted

技术标签:

【中文标题】Java 8:计算 lambda 迭代的首选方法?【英文标题】:Java 8: preferred way to count iterations of a lambda? 【发布时间】:2015-05-01 16:23:26 【问题描述】:

我经常遇到同样的问题。我需要计算 lambda 的运行次数,以便在 lambda 之外使用。

例如:

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

问题是 runCount 必须是 final,所以它不能是 int。它不能是Integer,因为它是不可变的。我可以将其设为类级别变量(即字段),但我只需要在这段代码中使用它。

我知道有多种方法,我只是想知道您的首选解决方案是什么? 您是使用AtomicInteger 还是数组引用或其他方式?

【问题讨论】:

@Sliver2009 不,不是。 @Florian 你必须在这里使用AtomicInteger 【参考方案1】:

为了便于讨论,让我重新格式化您的示例:

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 新兴的函数式风格是为了避免副作用。我们通过将计数器的增量提取到流上的count 操作中做了一点。其他典型的副作用是将项目添加到集合中。通常这些可以通过使用收集器来替换。但在不知道您要做什么实际工作的情况下,我无法提出更具体的建议。

【讨论】:

应该注意的是,一旦count 实现开始使用SIZED 流的快捷方式,peek 就会停止工作。对于filtered 流,这可能永远不会成为问题,但如果有人稍后更改代码,可能会产生巨大的惊喜…… 声明 final AtomicInteger i = new AtomicInteger(1); 并在您的 lambda 中某处使用 i.getAndAdd(1)。停下来,记住int i=1; ... i++ 曾经多么美好。 如果 Java 将在数字类(包括 AtomicInteger)上实现诸如 Incrementable 之类的接口,并将诸如 ++ 之类的运算符声明为看起来很花哨的函数,我们就不需要运算符重载和仍然有非常可读的代码。【参考方案2】:

作为同步麻烦 AtomicInteger 的替代方法,可以使用 整数数组 代替。只要 对数组的引用没有分配另一个数组(这就是重点),它可以用作 final 变量,而 values strong> 的字段可以任意改变

    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 声明为最终变量。但正如@pisaruk 指出的那样,这不会并行工作。 我认为,对于简单的foreach 直接在集合上(没有流),这是一个足够好的方法。谢谢!! 这是最简单的解决方案,只要您不并行运行。【参考方案3】:
AtomicInteger runCount = 0L;
long runCount = myStream.stream()
    .filter(...)
    .peek(item ->  
        foo();
        bar();
        runCount.incrementAndGet();
    );
System.out.println("The lambda ran " + runCount.incrementAndGet() + "times");

【讨论】:

请edit 提供更多信息。纯代码和“试试这个”的答案是discouraged,因为它们不包含可搜索的内容,也没有解释为什么有人应该“试试这个”。我们在这里努力成为知识的资源。 你的回答让我很困惑。你有两个变量都命名为runCount。我怀疑你打算只拥有其中一个,但哪一个? 我发现 runCount.getAndIncrement() 更合适。很好的答案! AtomicInteger帮助了我,但我会用new AtomicInteger(0)初始化它 1) 此代码无法编译:流没有返回 long 的终端操作 2) 即使可以,'runCount' 值将始终为 '1':- 流没有终端操作,因此永远不会调用 peek() lambda 参数 - System.out 行在显示之前增加运行计数【参考方案4】:

不应该使用 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");

【讨论】:

JDK 中有几个 Holder 类。这个好像是javax.xml.ws.Holder 真的吗?为什么? 我同意 - 如果我知道我没有在 lamda/stream 中执行任何并发操作,为什么我要使用旨在满足并发性的 AtomicInteger - 这可能会引入锁定等,即多年前,JDK 引入了一组新的集合及其不执行任何锁定的迭代器的原因 - 为什么要负担一些降低性能的能力,例如在许多情况下不需要锁定时锁定。 Stream.forEach 是明确的不确定性,因此使用 Holder 可能会或可能不会工作,具体取决于底层流。 @GeroldBroser 关于调用线程也是不确定的:“对于任何给定的元素,可以在库选择的任何时间和任何线程中执行操作”。这可能会导致竞争条件导致结果无效。【参考方案5】:

对我来说,这成功了,希望有人觉得它有用:

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); ```【参考方案6】:

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

【讨论】:

链接服务:org.apache.commons.lang.mutable.MutableInt【参考方案7】:

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

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

很奇怪,我知道。但它确实将临时变量排除在局部范围之外。

【讨论】:

这里到底发生了什么,需要更多解释一下 @MrCholo 这是一个initializer block。它在构造函数之前运行。 @MrCholo 不,它是一个实例初始化器。 @MrCholo 匿名类不能有显式声明的构造函数。 @OlivierGrégoire 我承认这乍一看令人困惑,但我不确定您为什么认为存在性能问题。这里的大多数解决方案都涉及对象创建。【参考方案8】:

另一种方法(如果您希望计数仅在某些情况下增加,例如操作成功时很有用)是这样的,使用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;【参考方案9】:

reduce也可以,你可以这样使用

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

【讨论】:

【参考方案10】:

对我来说,这是最优雅的方式。

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

JDK 9,10 中存在一个错误,导致上述解决方案无法正常工作,但您可以按如下方式解决它。 https://bugs.openjdk.java.net/browse/JDK-8198356

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

【讨论】:

【参考方案11】:
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

【讨论】:

【参考方案12】:

也可以使用enum。特别是如果您在一次迭代中有多个计数器:

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
    

【讨论】:

以上是关于Java 8:计算 lambda 迭代的首选方法?的主要内容,如果未能解决你的问题,请参考以下文章

在 Java 8 中迭代枚举

从 java 8 流中的列表中过滤值

Java 8 lambda表达式

Java 8 lambda表达式

java 8方法引用到lambda [duplicate]

JAVA 8 方法引用 - Method References