Java 编译器错误地将变量标记为可能已经分配给

问题描述

出于某种原因,java 编译器将较低的 str 变量标记为“可能已经分配给”,尽管这种情况是不可能的(至少我认为是这样)。
知道为什么吗?
我知道删除最终修饰符会解决它,但我想了解原因...

final List<Long> idList = List.of(1L,2L,3L);
final String str;
boolean found = false;
for (Long id : idList) {
   if (id == 2) {
      str = "id" + id;
      found = true;
      break;
   }
}

if (!found) {
   str = "none";
}

在 IntelliJ IDE 上使用 java 15

解决方法

编译器不会对您的代码进行深入分析来弄清楚。这将需要很长时间,而且(参见停机问题),完美在数学上是无法实现的。

因此,编译器有一个它应用的简单操作列表。

在这种情况下,str = 赋值位于 if 循环中,因此它可以运行 0 次、1 次或多次。它既不推断设置了 while 的属性(可以运行 0 次),也不允许触及最终变量(可以运行 2 次或更多)。

,

关于不能多次分配给最终变量的规则,写得更正式(JLS 4.12.4):

如果一个 final 变量被赋值给一个编译时错误,除非它在赋值之前绝对没有被赋值。

Java 语言规范花了整整 chapter 16 篇幅讨论什么算作“确定分配”和“确定未分配”。一个变量可以是“确定分配”或“确定未分配”,两者,或都不是

在这种情况下,如果您在代码中应用有关 for 循环的规则,您将看到只有一条规则涉及 for 循环后的明确 [un] 赋值:

  • V 是在 for 语句之后 [un] 赋值的,如果以下两个都为真:

    • 条件表达式不存在或 V 在条件表达式后 [un] 赋值为 false 时。

    • V 在 for 语句是 break 目标的每个 break 语句之前 [un] 赋值。

让我们尝试证明 str 在 for 循环之后肯定是未赋值的。

在一个迷人的 for 循环中,显然有一个条件表达式(检查迭代器是否为 hasNext)。条件不涉及 str,因此 str 像以前一样保持未分配状态。我们遇到了第一个要点。 str 在休息之前分配的,所以我们没有遇到第二个要点。

我们不能使用第 16 章中的规则证明在 for 循环之后 str 肯定是未赋值的。(实际上我们也不能证明它在 for 循环之后被赋值,但这无关紧要。) if (!found) 语句也没有进行任何赋值,因此这意味着 str notstr = "none"; 之前肯定是未赋值的。因此,根据 4.12.4 的编译器错误。

如果你仍然想使用final变量,尝试使用一个基本的for循环来获取找到元素的索引,然后将其赋值给str

final List<Long> idList = List.of(1L,2L,3L);
final String str;
int index = -1;
for (int i = 0; i < idList.size(); i++) {
  Long id = idList.get(i);
  if (id == 2) {
    index = i;
    break;
  }
}
if (index < 0) {
  str = "none";
} else {
  str = "id" + idList.get(index);
}

或者,使用流:

final String str = 
    idList.stream()
        .filter(x -> x == 2)
        .findFirst()
        .map(x -> "id" + x)
        .orElse("none");
,

鉴于 str 具有修饰符 final 的事实,编译器会查看它,在这种情况下,就好像 str 是一个 String 类型的变量,它将具有更改(给定它的变量)修饰符 final 可以更改的能力不接受更改。