Java 中的确定赋值

2024 年 9 月 10 日 | 阅读 15 分钟

每个局部变量和 final 空白字段在访问任何值时都将具有已分配的值。对值的访问将包括变量名或出现在表达式中的区域,除了赋值运算符 "=" 的左侧操作数。

要访问局部变量或 final 空白字段 x,必须在访问它之前明确为其赋值,否则将发生编译时错误。

同样,每个 final 空白变量将被赋值一次,并在对其进行赋值时未赋值。

因此,此类赋值的定义是仅当变量名出现在赋值运算符的左侧时才发生。

当对 final 空白变量进行赋值时,该变量在此之前不应被赋值,否则将出现编译时错误。

这种明确赋值的主要思想是,局部变量赋值或 final 空白字段的赋值发生在访问的每个可能执行路径上。

因此,分析将考虑表达式和语句的结构,并且将立即处理表达式中的运算符,例如 &&、!、||、? : 以及布尔值常量表达式。

对于像 &&、|| 和 ?: 这样的条件布尔运算符以及布尔值常量表达式,存在一个例外处理,并且表达式的值不会在流分析中考虑。

让我们通过一个例子来讨论这一点。

明确赋值将考虑表达式和语句的结构。

Java 编译器将决定在访问“k”之前已为其赋值,就像代码中方法调用的参数一样。

这是因为在表达式的值准确时会发生访问。该值仅在执行并更充分地评估了对“k”的赋值后才有效。表达式如下

同样,Java 编译器将在上述代码中识别出,变量“k”已在 while 语句中明确赋值,因为条件表达式“TRUE”永远不会取“FALSE”的值。break 语句将仅导致 while 语句正常编译,并且变量“k”在 break 语句之前已明确赋值。

而且,另一方面,

Java 编译器将拒绝上述代码,因为在上述情况下,如果我们考虑明确赋值规则,while 语句无法正确执行其主体。

另一个例子

Java 编译器将为上述代码生成编译时错误,即使“n”的值在编译时已知。根据规则,在编译时可以知道对值“k”的赋值总是会执行,并且更准确地说,它会执行。

Java 编译器必须遵循我们上面讨论的规则。只有常量表达式遵循规则,而在上面的例子中,常量表达式 n > 2 不是常量表达式。

我们将讨论另一个 Java 编译器将接受代码的示例

只要我们考虑“k”的明确赋值,因为上面的规则已被打破,我们就可以说“k”已经被赋值,无论标志的值是 true 还是 false。

规则将不接受上述变体,程序将收到编译时错误。

明确未赋值的示例

就“k”的明确未赋值而言,Java 编译器将接受代码,并且这些交叉的规则将允许它说“k”只赋值一次,无论它是 true 还是 false,这都没有关系。

规则将不接受变体,但当我们编译上述程序时,它会收到编译时错误。

为了指定明确赋值的步骤,这些规则将定义许多技术术语

  1. 它必须检查变量是否在任何表达式或语句之前被明确赋值。
  2. 它必须检查变量是否在任何表达式或语句之前被明确未赋值。
  3. 它必须检查变量是否在任何表达式或语句之后被明确赋值。
  4. 它必须检查变量是否在任何表达式或语句之后被明确未赋值。

在布尔值表达式的情况下,上述最后两个步骤被重定向为四个点

  1. 如果为 true,则变量在该表达式之后被明确赋值。
  2. 如果为 true,则变量在该表达式之后被明确未赋值。
  3. 如果为 false,则变量在该表达式之后被明确赋值。
  4. 如果为 false,则变量在该表达式之后被明确未赋值。

从上面可以看出,true 和 false 指的是表达式的值。

让我们举个例子。

只有当上述表达式为 true 时,局部变量“k”在求值上述表达式后才被明确赋值,而在表达式为 false 时则不然。如果“a”为 false,则对“k”的赋值未正确执行或评估。

让我们考虑语句“X 完成其功能通常时,V 在 X 之后被明确赋值”,“V”是局部变量,“X”是表达式或语句。该语句表示“V”在“X”之后被明确赋值,如果“X”通常完成。假设“X”以错误的方式完成,并且已定义了需要考虑的规则。

上述表达式的不同情况是“V 在 break 之后被明确赋值”,这始终为 true,因为我们知道 break 语句通常不会完成。确实,“V”没有为 break 语句赋值,该语句通常会完成它们。

  1. “V”被明确赋值但未明确未赋值。流分析规则将证明对“V”进行了赋值。
  2. “V”被明确未赋值但未明确赋值,流分析规则将证明未对“V”进行赋值。
  3. “V”未明确赋值;它未明确未赋值,并且规则不会证明对 V 进行了赋值。
  4. “V”被明确赋值也明确未赋值,并且这种情况不可能正常完成。

缩略语“iff”用作“当且仅当”以缩短规则。我们也可以使用另一个缩略语,例如,如果一个规则可能包含一个或多个“未赋值”的出现,那么它执行两个规则

  1. 一个带有“未赋值”的每个出现被替换为“明确赋值”。
  2. 另一个带有“未赋值”的每个出现被替换为“明确未赋值”。

让我们举个例子。

  1. “V”在空语句之后未赋值,当且仅当它在空语句之前未赋值。

上述陈述可以通过下面的两个规则来理解

  1. “V”在空语句之后赋值,当且仅当它在空语句之前赋值。
  2. “V”在空语句之后明确未赋值,当且仅当它在空语句之前明确未赋值。

明确赋值和表达式

布尔常量表达式

  1. 当值为 true 而值为 false 时,“V”在某个常量表达式之后未赋值。
  2. 当值为 false 而值为 true 时,“V”在某个常量表达式之后未赋值。
  3. 当值为 true 时,“V”在某个常量表达式之后未赋值,当且仅当“V”在该常量表达式之前未赋值。
  4. 当值为 false 时,“V”在某个常量表达式之后未赋值,当且仅当“V”在该常量表达式之前未赋值。
  5. 布尔值常量表达式“e”之后,“V”未赋值,当且仅当“V”在“e”为 true 时之后未赋值,并且“V”在“e”为 false 时之后未赋值。

以上所有等同于说,“V”在“e”之后未赋值或已赋值,当且仅当“V”在“e”之前未赋值或已赋值。

这是因为常量表达式的值为 true 且从不为 false,而常量表达式的值为 false 且从不为 true,并且满足上述两个规则。

条件与运算符(&&)

  1. 如果 a&&b 为 true,那么“V”未赋值或已赋值,当且仅当“V”在 b 为 true 时之后未赋值。
  2. 当 a&&b 为 false 时,“V”在该表达式之后未赋值或已赋值,当且仅当“a”为 false 时,“V”已赋值或未赋值,并且“b”为 false 时,“V”未赋值或已赋值。
  3. 当且仅当“V”在 a&&b 之前未赋值或已赋值时,“V”在“a”之前未赋值或已赋值。
  4. 当且仅当“V”在“a”为 true 时之后未赋值或已赋值时,“V”在“b”之前未赋值或已赋值。
  5. 当且仅当“V”在 a&&b 为 true 时之后未赋值或已赋值,并且“V”在 a&&b 为 false 时之后未赋值或已赋值时,“V”在 a&&b 之后[未]赋值。

条件或运算符(||)

  1. a||b 仅当“V”在 a 之后被赋值,并且“V”在 b 之后被赋值,并且当两者都为 true 时,a||b 才为 true。
  2. 当 a||b 为 false 时,“V”在该表达式之后未赋值或已赋值,当且仅当“V”在 b 为 false 时之后未赋值或已赋值。
  3. 当且仅当“V”在 a||b 之前未赋值或已赋值时,“V”在“a”之前未赋值或已赋值。
  4. 当且仅当“V”在“a”为 false 时之后未赋值或已赋值时,“V”在“b”之前已赋值或未赋值。
  5. 当且仅当“V”在 a||b 为 true 时之后被赋值或未赋值,并且“V”在 a||b 为 false 时之后未赋值或已赋值时,“V”在 a||b 之后被赋值或未赋值。

逻辑非运算符(!)

  1. 假设“V”在“a”为 false 时之后被赋值或未赋值,最后,“V”在 !a 之后被赋值。
  2. 如果“V”在“a”为 true 时之后被赋值或未赋值,那么“V”在“! a”之后被赋值或未赋值;它变成 false。
  3. 当且仅当“V”在“! a”之前未赋值或已赋值时,“V”在“a”之前未赋值或已赋值。
  4. 当且仅当“V”在“!a”为 true 时之后未赋值或已赋值,并且“V”在“!a”为 true 时之后未赋值或已赋值时,“V”在“! a”之后未赋值。

条件运算符(?:)

让我们假设 b 和 c 是布尔值表达式。

  1. 当且仅当“V”在 b 为 true 时之后未赋值或已赋值,并且“V”在 c 为 true 时之后未赋值或已赋值时,“V”在 a? b:c 为 true 时之后未赋值或已赋值。
  2. 当 a ? b:c 为 false 时,“V”在该表达式之后未赋值或已赋值,当且仅当“V”在 b 为 false 时之后未赋值或已赋值,并且“V”在 c 为 false 时之后未赋值或已赋值。
  3. 当且仅当“V”在 a ? b:c 之前[未]赋值时,“V”在“a”之前[未]赋值。
  4. 当且仅当“V”在“a”为 true 时之后未赋值或已赋值时,“V”在“b”之前已赋值或未赋值。
  5. 当且仅当“V”在“a”为 false 时之后未赋值或已赋值时,“V”在“c”之前已赋值或未赋值。
  6. 当且仅当“V”在 a ? b:c 为 true 时之后未赋值或已赋值,并且“V”在 a ?? b:c 为 false 时之后未赋值时,“V”在 a ? b:c 之后未赋值或已赋值。

布尔类型表达式

让我们假设“e”是一个布尔类型的表达式,而不是逻辑非表达式,也不是布尔常量表达式 ! a,不是条件与表达式 a&&b,不是条件或表达式 a||b,也不是条件表达式 a ? b:c。

  1. 当且仅当“V”在“e”之后未赋值或已赋值时,“V”在“e”之后未赋值或已赋值。
  2. 当且仅当“V”在“e”之后未赋值或已赋值时,“V”在“e”之后未赋值或已赋值。

赋值表达式

让我们考虑赋值表达式 a=b, a+=b, a%=b, a|=b, a^=b, a*=b, a-=b, a>>=b, a<<=b 等等。

  1. 当且仅当“a”是“V”或“V”在“b”之后被明确赋值时,“V”在该赋值表达式之后被明确赋值。
  2. 当且仅当“a”不是“V”,并且“V”在“b”之后被明确未赋值时,“V”在该赋值表达式之后被明确未赋值。
  3. 当且仅当“V”在赋值表达式之前未赋值或已赋值时,“V”在“a”之前未赋值或已赋值。
  4. 当且仅当“V”在“a”之后被赋值或未赋值时,“V”在“b”之前未赋值或已赋值。

注意:如果 a 是“V”并且在复合赋值如 a&=b 之前“V”没有被明确赋值,那么将发生编译时错误,并且上面讨论的第一个明确赋值规则将包括析取“a is V”,并且对于复合赋值表达式,它不像简单赋值那样,因此“V”在该代码点之后被明确赋值。

当我们包含析取“a is V”时,它不会影响确保程序可接受或导致编译时错误的二元决策。它会影响代码中有多少个不同的点被视为错误,因此在实践中,它将提高错误报告的质量。因此,对第一个明确未赋值规则中包含合取“a is V”的类似评论也适用,如上所述。

运算符 ++ 和 --

  1. 当且仅当 a 是 V 或 V 在操作数表达式之后被明确赋值时,“V”在 ++a, --a, a++, 或 a-- 之后被明确赋值。
  2. 当且仅当 a 不是 V 并且 V 在操作数表达式之后被明确未赋值时,“V”在 ++a, --a, a++, 或 a-- 之后被明确未赋值。
  3. 当且仅当“V”在 ++a, --a, a++, 或 a-- 之前未赋值或已赋值时,“V”在“a”之前未赋值或已赋值。

其他表达式

让我们假设表达式不是布尔常量表达式,也不是前缀增量表达式 ++a,不是前缀减量表达式 --a,不是后缀增量 a++,不是后缀减量表达式 a--,不是逻辑非表达式 ! a,不是条件与表达式 a&&b,不是条件或表达式 a||b,不是条件表达式 a? b:c,不是赋值表达式,也不是 lambda 表达式,那么我们必须遵循以下规则

规则 1:如果表达式没有子表达式,则“V”在该表达式之后未赋值或已赋值,当且仅当“V”在该表达式之前未赋值或已赋值。

在这种情况下,它适用于字面量、名称、this(限定和非限定)、没有参数的非限定类实例创建表达式、具有不包含表达式的初始化程序的数组创建表达式、超类字段访问表达式、没有参数的非限定和类型限定方法调用表达式、没有参数的超类方法调用表达式,以及超类和类型限定方法引用表达式。

规则 2:如果表达式有子表达式,“V”在该表达式之后未赋值或已赋值,当且仅当“V”在其最右侧的直接子表达式之后未赋值或已赋值。

有一个微妙的推理过程可以得出结论,即变量“V”可以在方法调用表达式之后被确认为明确未赋值。它将被单独对待,不做任何限定;这种断言并不总是正确的,因为被调用的方法会执行赋值。必须记住,对于 Java 编程语言,明确赋值的概念仅适用于 final 空白变量。

如果“V”是一个空白的 final 局部变量,那么只有声明其所属的方法才会对“V”执行赋值。如果“V”是一个 final 空白字段,那么只有包含“V”声明的类的构造函数或初始化程序才能对“V”执行赋值。最后,显式构造函数调用被特殊处理,即使它们在语法上与包含方法调用的表达式相同。它们不是表达式语句,最后,本节中的规则不适用于显式构造函数调用。

假设一个表达式是 lambda 表达式,并且将应用以下规则

  1. 当且仅当“V”在表达式之前未赋值或已赋值时,“V”在该表达式之后未赋值或已赋值。
  2. 当且仅当“V”在 lambda 表达式之前被明确赋值时,“V”在 lambda 表达式主体(表达式或块)之前被明确赋值。

没有规则规定“V”在 lambda 主体之前被明确未赋值。在 lambda 主体之前被明确未赋值的变量将在之后被赋值。因此,我们无法检查变量在执行主体时是否应被赋值。

对于表达式“x”的任何直接子表达式“y”(其中“x”不是 lambda 表达式),“V”在“y”之前未赋值或已赋值,当且仅当以下任一条件成立。

  1. 只有当局部变量声明语句包含至少一个变量初始化程序时,“V”才在该声明语句之后被明确赋值,当且仅当在局部变量声明语句中的最后一个变量初始化程序或声明器中的最后一个变量初始化程序(该声明器声明“V”)存在的情况下,“V”才被明确设置。
  2. 只有当包含至少一个变量初始化程序的局部变量声明语句中的最后一个变量初始化程序被明确未赋值,并且声明中的最后一个变量初始化程序不是声明“V”的声明器时,“V”在该局部变量声明语句之后才会被明确未赋值。
  3. 当且仅当“V”在局部变量声明语句之前未赋值或已赋值时,“V”在局部变量声明语句中的第一个变量初始化程序之前未赋值。
  4. 只有当“V”在“e”左侧的变量初始化程序之后被明确赋值,并且在声明“V”的声明器中时,“V”才在局部变量声明语句中除第一个变量初始化程序以外的任何变量初始化程序“e”之前被明确赋值。
  5. 只有当“V”在“e”左侧的变量初始化程序之后被明确未赋值,并且不在声明“V”的声明器中时,“V”才在局部变量声明语句中的任何变量初始化程序“e”(除第一个之外)之前被明确未赋值。

带标签的语句

  1. 当且仅当“V”在“S”之后未赋值或已赋值,“V”在存在于标签语句 L: S 中的每个 break 语句之前被赋值时,“V”在带标签的语句 L: S 之后未赋值或已赋值。
  2. 当且仅当“V”在 L: S 之前未赋值或已赋值时,“V”在“S”之前未赋值或已赋值。

表达式语句

  1. 当且仅当“V”在“e”之后未赋值或已赋值时,“V”在表达式语句“e”之后未赋值或已赋值。
  2. 当且仅当“V”在“e”之前未赋值或已赋值时,“V”在“e”之前未赋值或已赋值。

if 语句

对于语句 if (e) S,有以下规则适用:

  1. 当且仅当“V”在“S”之后未赋值或已赋值,并且“V”在“e”为 false 时之后未赋值或已赋值时,“V”在 if (e) S 之后才被赋值或未赋值。
  2. 当且仅当“V”在 if (e) S 之前未赋值或已赋值时,“V”在“e”之前未赋值或已赋值。
  3. 当且仅当“V”在“e”为 true 时之后未赋值或已赋值时,“V”在“S”之前被赋值或未赋值。

对于语句 if (e) S else T,有以下规则适用:

  1. 当且仅当“V”在“S”之后未赋值或已赋值,并且“V”在 T 之后被赋值或未赋值时,“V”在 if (e) S else T 之后未赋值或已赋值。
  2. 当且仅当“V”在 if (e) S else T 之前未赋值或已赋值时,“V”在“e”之前未赋值或已赋值。
  3. 当且仅当“V”在“e”为 true 时之后未赋值或已赋值时,“V”在“S”之前未赋值或已赋值。
  4. 当且仅当“V”在“e”为 false 时之后未赋值或已赋值时,“V”在 T 之前未赋值或已赋值。

Assert 语句

对于语句 assert e1 和语句 assert e1: e2,有以下规则适用。

  1. 当且仅当“V”在 assert 语句之前未赋值或已赋值时,“V”在 e1 之前未赋值或已赋值。
  2. 当且仅当“V”在 assert 语句之前被明确赋值时,“V”在 assert 语句之后被明确赋值或未赋值。
  3. 当且仅当“V”在 assert 语句之前被明确未赋值,并且“V”在 e1 为 true 时之后被明确未赋值时,“V”在 assert 语句之后被明确未赋值。

对于语句 assert e1: e2,有一个规则适用:

  1. 当且仅当“V”在 e1 为 false 时之后未赋值或已赋值时,“V”在 e2 之前未赋值或已赋值。

Switch 语句

当且仅当以下所有语句都为 true 时,“V”在 switch 语句之后被赋值或未赋值

  1. switch 块中存在 default 标签,或者“V”在 switch 表达式之后未赋值或已赋值。
  2. switch 块中没有以块语句组开头的 switch 标签(即,在结束 switch 块的闭合花括号“}”之前没有 switch 标签),或者“V”在 switch 表达式之后未赋值或已赋值。