Java AtomicInteger 与流的一个元素数组

Java AtomincInteger vs one element array for streams

int[] arr = new int[]{0};
l.stream().forEach(x -> {if (x > 10 && x < 15) { arr[0] += 1;}});

lList<Integer>。这里我使用一个元素 arr 数组来存储在流中更改的值。另一种解决方案是使用 AtomicInteger class 的实例。但是我不明白这两种方法在内存使用、时间复杂度、安全性方面有什么区别...

请注意:我并没有尝试在这段特定的代码中使用 AtomicInteger(或数组)。此代码仅用作示例。谢谢!

您应该始终使用 AtomicInteger:

  • 性能影响可以忽略不计。从技术上讲,new int[1] 是 'faster',但它们的大小相同,或者,数组在堆中实际上 更大 (但不太可能;取决于您的 OS 体系结构,通常它们最终大小相同),并且数组 不会 花费任何周期来保证适当的并发保护,但实际上只有两个选择:[A ] 需要并发保护(因为它是在另一个线程中 运行s 的 lambda),因此 int 数组是一个非启动器;这会导致很难找到错误,非常可怕,或者 [B] 它们不是必需的,热点引擎很可能会解决这个问题并完全消除这个成本。即使没有,在任何情况下,没有争用时并发保护的开销都很低。

  • 可读性更强。只是略微如此,但 new int[1]new AtomicInteger() 更奇怪,我会说。 AtomicInteger 至少建议:我想要一个可变的 int,我会从其他上下文中弄乱它。

  • 比较方便。 System.out.println-ing 一个原子整数打印值。 sysouting 数组打印垃圾。

  • AtomicInteger 中的便捷方法可能相关。也许 compareAndSet 有用。

但是为什么呢?

Lambda 在以下 3 件事上是不透明的:

  • 已检查的异常(您不能在 lambda 中抛出已检查的异常,即使您的 lambda 周围的上下文捕获了它)。
  • 可变局部变量(您不能触及,更不用说更改在 lambda 之外声明的任何变量,除非它是(有效地)final)。
  • 控制流程。您不能在 lambda 内部使用 breakcontinuereturn 并让它表现得好像它不是:您不能中断或继续位于您外部的循环lambda 并且你不能 return 在你的 lambda 之外形成方法(你只能 return 来自 lambda 本身)。

当lambda 运行s 'in context'时,这些都是非常糟糕的事情,但它们都是非常好的事情 当 lambda 不在上下文中时 运行。

这是一个例子:

new TreeSet<String>((a, b) -> a - b);

我在这里创建了一个 TreeSet(这是一个自动排序元素的集合)。要制作一个,您传入代码来确定任何 2 个元素中的一个是 'the higher one',TreeSet 会处理其他所有事情。该 TreeSet 可以在您的方法中存活(只需将其存储在一个字段中或将其传递给最终将其存储在一个字段中的方法),甚至可以逃脱您的线程(让另一个线程读取该字段)。这意味着当调用该代码(此代码中的a - b)时,我们可能会在另一个线程中使用 'surrounds' 您的 new TreeSet 语句创建该 TreeSet 后的 5 天已经很久了。

在这种情况下,所有这些透明胶片都毫无意义:

break 回到早已完成且系统甚至不知道它是什么的循环是什么意思?

那个 catch 块使用了早已不复存在的上下文,例如局部变量或参数。它无法生存,所以如果你的 a - b 抛出一些经过检查的东西,那么你将 new TreeSet<> 包裹在 try/catch 块中的事实是没有意义的。

访问一个不再存在的变量是什么意思?就此而言,如果它仍然存在但 lambda 运行 位于单独的线程中,我们现在是否开始制作局部变量 volatile 并在堆上而不是堆栈上声明它们以防万一?

当然,如果你的 lambda 运行s 在上下文中,你将 lambda 传递给某个方法,那个方法 'uses it or loses it':运行你的 lambda 一定次数然后忘记总而言之,然后那些缺乏透明度的东西真的很烦人

很烦你不能这样做:

public List<String> toLines(List<Path> files) throws IOException {
  var allLines = files.stream()
    .filter(x -> x.toString().endsWith(".txt"))
    .flatMap(x -> Files.readAllLines().stream())
    .toList();
}  

上述代码失败的唯一原因是 Files.readAllLines() 抛出 IOException。我们声明我们从 throws 开始,但这行不通。您必须通过尝试以某种方式将该异常从 lambda 中转移出来或以其他方式解决它来拼凑这段代码,使其变得糟糕(正确的答案不是在这里完全使用流 API,用一个正常的 for 循环!)。

虽然尝试绕过 lambda 中的检查异常通常是不值得的,但您可以解决想要与外部上下文共享变量的问题:

int sum = 0;
listOfInts.forEach(x -> sum += x);

以上不起作用 - sum 来自外部范围,因此必须有效地最终,但事实并非如此。没有特别的原因它不能工作,但 java 不会让你。这里的正确答案是使用 int sum = listOfInts.mapToInt(Integer::intValue).sum(); 代替,但您并不总能找到一个可以满足您要求的终端操作。有时您需要绕过它。

这就是 new int[1]AtomicInteger 的用武之地。这些是引用 - 引用是最终的,因此您可以在 lambda 中使用它们。但是引用指向一个对象,你可以随意改变它,因此,你可以使用这个 'trick' 到 'share' 一个变量:

AtomicInteger sum = new AtomicInteger();
listOfInts.forEach(x -> sum.add(x));

确实有效。

知道哪种方法是最好的很重要,@rzwitserloot 的解释对此进行了非常详细的介绍。在你的具体例子中,你可以通过这样做来避免这个问题。

List<Integer> list = List.of(1,2,11,12,15,11,11,9,10,2,3);

int count = list.stream().filter(x->x > 10 && x < 15).reduce(0, (a,b)->a+1);
// or
int count = list.stream().filter(x->x > 10 && x < 15).mapToInt(x->1).sum();

两者都return值4

在第一个示例中,reduce 设置初始值 0,然后向其添加 1b 在语法上是必需的,但未使用)。要对实际元素求和而不是 1,请在 reduce 方法中将 1 替换为 b

在第二个示例中,值在流中被替换为 1,然后求和。由于对象流不存在方法 sum(),因此需要将 1 映射到 int 以创建 IntStream。要对此处的实际元素求和,请使用 mapToInt(x->x)

按照评论中的建议,你也可以这样做

long count = list.stream().filter(x->x > 10 && x < 15).count();

count() return 很长,所以如果您想要的话,它必须向下转换为 int