单线程应用程序中的 AtomicInteger 和 lambda 表达式

AtomicInteger & lambda expressions in single-threaded app

我需要修改 JButton 的 ActionListener 中 lambda 表达式内的局部变量,由于我无法直接修改它,所以我遇到了 AtomicInteger 类型。

我实现了它并且工作得很好,但我不确定这是否是一个好的做法,或者它是否是解决这种情况的正确方法。

我的代码如下:

newAnchorageButton.addActionListener(e -> {
    AtomicInteger anchored = new AtomicInteger();
        
    anchored.set(0);
    
    cbSets.forEach(cbSet ->
        cbSet.forEach(cb -> {
            if (cb.isSelected())
                anchored.incrementAndGet();
        })
    );
        
    // more code where I use the 'anchored' variable...
}

我不确定这是否是解决此问题的正确方法,因为我读到 AtomicInteger 主要用于与并发相关的应用程序,并且该程序是单线程的,但同时我可以'找不到其他方法来解决这个问题。

我可以简单地使用两个嵌套的 for 循环来遍历这些数组,但我正在尝试根据 sonarlint vscode 扩展尽可能地降低方法的认知复杂性,并将那些留给-loops 理论上增加了方法的复杂性,因此增加了它的可读性和可维护性。

用 lambda 表达式替换 for 循环降低了认知的复杂性,但也许我不应该太关注它。

虽然在 single-threaded 代码中足够安全,但最好以函数式方式计算它们,如下所示:

long anchored = cbSets.stream()     // get a stream of the sets
    .flatMap(List::stream)          // flatten to list of cb's
    .filter(JCheckBox::isSelected)  // only selected ones
    .count();                       // count them

我们没有改变累加器,而是将扁平化流限制为仅我们感兴趣的流并要求计数。

不过,更一般地说,总是可以在没有可变变量的情况下总结或聚合值。考虑:

record Country(int population) { }

countries.stream()
    .mapToInt(Country::population)
    .reduce(0, Math::addExact)

注意:我们从不改变任何值;相反,我们将每个连续值与前一个值组合起来,产生一个新值。可以使用 sum() 但我更喜欢 reduce(0, Math::addExact) 以避免溢出的可能性。

and leaving those for-loops theoretically increases the method complexity and therefore its readability and maintainability.

这明显是马桶。 x.forEach(foo -> bar)for (var foo : x) bar; 不同 'cognitively simpler' - 您可以将每个 AST 节点直接从一个映射到另一个。

如果用于定义复杂性的定义得出的结论是一个比另一个复杂得多,那么唯一正确的结论是 该定义很愚蠢,应该修正或放弃.

为了使其实用:是的,引入 AtomicInteger,虽然在性能方面不会产生丝毫差异,确实 使代码 way比较复杂。 AtomicInteger 在代码中的简单存在表明并发在这里是相关的。它不是,所以你必须添加评论来解释你为什么要使用它。评论是邪恶的。 (他们暗示代码本身不会说话,并且无法以任何方式对其进行测试)。他们通常是最不邪恶的,但他们仍然是邪恶的。

保持 lambda-based 代码在认知上容易遵循的一般 'trick' 是拥抱管道:

  • 您编写了一些 'forms' 流的代码。这可以像 list.stream() 一样简单,但有时您会进行一些流连接或对集合的集合进行平面映射。
  • 您有一个对流中的单个元素进行操作的操作管道,不引用整体或任何相邻元素。
  • 最后,你减少(使用 collectreducemax - 一些终止符)这样减少方法 returns 你需要什么。

上述模型(另一个答案恰好遵循它)往往会产生与 'old style' 代码一样 readable/complex 的代码,并且很少(但有时!)更具可读性,并且明显不那么复杂。偏离它,结果几乎总是复杂得多 - 一个明显的失败者。

并非 java 中的所有 for 循环都符合上述模型。如果它不合适,那么试图将那个特定的方钉强行插入圆孔将需要很多努力,并且几乎总是会导致代码明显更糟:要么慢一个数量级,要么认知复杂得多。

这也意味着它几乎从不 'worth' 将基于 non-stream 的代码重写为基于流的代码;根据某些个人喜好,它充其量只是提高了一个百分点的可读性,并没有得到普遍认可的重大改进。

关闭那个愚蠢的 linter 规则。它认为上述 'less' 复杂,并且它显然确定 for (var foo : x) bar; 是 'more complicated' 而不是 x.forEach(foo -> bar) 的事实足以证明它的伤害远大于它的帮助。

我有以下内容要添加到其他两个答案中:

您的代码中的两个一般良好做法存在问题:

  1. Lambda 不应超过 3-4 行
  2. 除了某些精确的情况,流操作的 lambda 应该是无状态的。

对于 #1,考虑将 lambda 的代码提取到一个私有方法中,例如,当它变得太长时。 您可能会提高可读性,也可能会更好地将 UI 与业务逻辑分开。

对于 #2,您可能并不担心,因为您目前正在单线程中工作,但是流可以并行化,并且它们可能并不总是按照您认为的那样执行。 出于这个原因,在流管道操作中保持代码无状态总是更好。否则你可能会感到惊讶。

更一般地说,流非常好,非常简洁,但有时对好的旧循环做同样的事情会更好。 不要犹豫,回到经典循环。

当 Sonar 告诉你复杂度太高时,实际上,你应该尝试分解你的代码:拆分成更小的方法,改进你的对象模型等。