变量可能未被初始化,但在构造函数中进行了设置。

7
所以我已经尝试搜索答案,但是对于这个错误信息,我找到的答案与我的问题无关。
这里是问题所在。 为什么这段代码:
Case 1)
public class A {
    private final String A;
    private final String B;
    private final String C = A + B;

    public A(String A, String B) {
        this.A = A;
        this.B = B;
    }
}

对于行 private final String C = A + B;,它显示以下错误:

java: variable A might not have been initialized
java: variable B might not have been initialized

这个非常有效:

但是这个非常有效:

情况2)

public class K {
    private final String K;
    private final String L;
    private final String M = kPlusL();

    public K(final String K, final String L) {
        this.K = K;
        this.L = L;
    }

    private String kPlusL() {
        return K + L;
    }
}

这也非常有效:
情况3)
public class O {
    protected final String O;
    protected final String P;

    public O(final String O, final String P) {
        this.O = O;
        this.P = P;
    }
}

public class Q extends O {
    private final String Q = O + P;

    Q (final String O, final String P) {
        super(O, P);
    }
}

有人可以解释一下为什么吗?我正在使用IntelliJ IDEA和Java 1.8.0_151。
这三种情况都在做同样的事情(将两个字符串拼接在一起),但第一个直接拼接,第二个和第三个是“间接”拼接。

6
信不信由你,你使用的集成开发环境对这一切都没有任何影响。 - Stultuske
2
提示:考虑字段初始化程序运行的时间和构造函数体运行的时间。 - Jon Skeet
CoronA - 不会像我预期的那样吗?但是只有在Q或M被初始化后我才能访问它们。从未在此之前。它会将两个字符串拼接在一起 - 是否如预期? - Tomáš Tököly
Smutje - 感谢您的回答!问题是,如果我在情况1)中从A和B变量中删除“final”,它也可以编译。 - Tomáš Tököly
Raedwald - 谢谢!那就是我在寻找的答案!首先执行的是字段初始化器,然后才是构造函数体。 - Tomáš Tököly
显示剩余4条评论
6个回答

4
当您尝试在第一个案例中初始化 C 时,此时 AB 尚未初始化,因此private final String C = A + B; 将失败。

尝试在构造函数内初始化 C

public class A {
    private final String A;
    private final String B;
    private final String C;

    public A(String A, String B) {
        this.A = A;
        this.B = B;
        this.C = A + B;
    }
}

这里是来自JLS的相关部分:

对于每次访问局部变量或空白final字段x,x在访问之前必须被明确赋值,否则会出现编译时错误。


1
问题不在于如何修复它,而是为什么编译器禁止第一种方式,但允许第二种和第三种方式。这是一种需要用JLS中的引用来回答为什么编译器会这样做的问题(或者将其作为错误报告)。 - Kayaman
@QBrute确实,这并没有回答问题(可能只是非常部分地...)https://dev59.com/AKjja4cB1Zd3GeqP81BV#48322068 - Eugene

2
情况2中,您使用kPlusL()设置M的值,这将在连接期间将null转换为字符串。因此它将具有值"nullnull"。
情况3中,您正在继承一个类,因此在子类实例化之前将调用超类构造函数。因此,在Q中分配OP的值。

你试过这段代码吗?在第二种情况下,M会被初始化为“nullnull”(因为它调用了kPlusL,而此时类还没有完全初始化)。 - daniu
你是对的,daniu。由于字符串连接,它将返回“nullnull”。 - jeet427
daniu - 我刚试了一下,它确实返回了 "nullnull"。如果我在情况1中从A和B变量中删除“final”,我也会得到“nullnull”。谢谢! - Tomáš Tököly

1

的确,JLS的部分是相关的(因为您已经得到了它们,不会再次提供链接),但字节码更加有趣。

对于第一个例子(稍作修改):

private String A;
private String B;
private final String C = A + B;

public FirstExample(String A, String B) {
    this.A = A;
    this.B = B;
}

System.out.println(new FirstExample("a", "b").C);

这将打印出nullnull,如果你查看生成的字节码(仅相关部分),就会明白这是有道理的。顺便说一句,根据JLS的规定,实例字段在构造函数体的其余部分之前被初始化是正确的。
getfield // Field A:Ljava/lang/String;
getfield // Field B:Ljava/lang/String;
// this is just concat the two Strings with java-9
invokedynamic // InvokeDynamic #0:makeConcatWithConstants
....
putfield // Field C:Ljava/lang/String;  
putfield // Field A:Ljava/lang/String;
putfield // Field B:Ljava/lang/String;

重点是实例变量在构造函数的其余部分之前进行初始化(基本上是CAB之前)。现在加上finalAB为什么不能编译就有意义了。
第二个例子更有趣。这些被称为前向引用,并且被规范允许(实际上可以说它们没有被禁止)。例如:
String x = y;
String y  = "a";

"

不允许,但另一方面这是允许的:

"
String x = getIt();
String y  = "a";
public String getIt() {
    return y;
}

缺点是第一次y会被初始化为null而不是a;因此,x将为空。
最后一个例子再次符合JLS。超类构造函数首先运行,因此初始化这些字段;只有在继承的(已经具有构造函数中的值)字段的帮助下,Q变量才被初始化,从而产生预期的值。

1

你能解释一下为什么“绕过前向引用规则”的机制(即情况2)有效吗? - daniu
“绕过前向引用规则”只是说明编译器无法(或不想)检测到这种情况。我认为这是因为这样的数据流分析并不简单,或者有时您希望出现这种行为。 - gtosto
我不知道...我希望禁止任何初始化调用实例方法是合理的。 - daniu
@daniu 我同意,对于final字段来说这根本没有意义。对于非final字段可能有些道理,但是允许这样做对于final字段来说是愚蠢的。 - Eugene

0

因为首先它定义了A和B,并且定义和初始化C的值为A和B,然后进入构造函数并初始化A和B。因此,可以看出在调用构造函数之前没有A和B的值。 请看下面的代码:

public class A { 
private final String A; // = Null
private final String B; // = Null
private final String C = A + B; // Error Cause There is no value for A and B

public A(String A, String B) {
    this.A = A;
    this.B = B;
} 
} 

只需将C = A + B放置在构造函数内部即可。


0

由于前两种情况已经很明确并得到了回答,我想把重点放在第三种情况上,即当您拥有继承时。根据JLS中指定的初始化顺序:

在新创建的对象引用返回结果之前,使用以下过程处理指定的构造函数以初始化新对象:
1. 为此构造函数调用新创建的参数变量分配构造函数的参数。 2. 如果此构造函数以同一类中另一个构造函数(使用this)的显式构造函数调用(§8.8.7.1)开头,则使用相同的五个步骤递归地评估参数并处理该构造函数调用。如果该构造函数调用异常完成,则出于同样的原因,此过程也会异常完成;否则,请继续执行第5步。 3. 此构造函数不以同一类中另一个构造函数(使用this)的显式构造函数调用开头。如果此构造函数是Object以外的类的构造函数,则此构造函数将以超类构造函数的显式或隐式调用(使用super)开头。使用相同的五个步骤递归地评估参数并处理该超类构造函数调用。如果该构造函数调用异常完成,则出于同样的原因,此过程也会异常完成。否则,请继续执行第4步。 4. 执行此类的实例初始化程序和实例变量初始化程序,按照它们在类的源代码中以文本方式出现的从左到右的顺序将实例变量初始化程序的值分配给相应的实例变量。如果执行任何这些初始化程序导致异常,则不会处理更多的初始化程序,并且此过程将以相同的异常异常完成。否则,请继续执行第5步。 5. 执行此构造函数的其余部分。如果该执行异常完成,则出于同样的原因,此过程也会异常完成。否则,此过程正常完成。

例子12.5-1. 实例创建的评估,给出的链接清楚地解释了初始化顺序。

因此,根据您的第三个示例(查看3和4点),执行顺序将是:

  1. O 中的实例变量初始值设定项(由于您没有指定任何初始值设定项,它们将被初始化为 null)
  2. O 构造函数——public O(final String O, final String P),它将使用给定的值分配 OP
  3. Q 中的实例变量初始值设定项——private final String Q = O + P;,它将 O 和 P 进行拼接,并将结果赋值给 Q
  4. Q 构造函数——public Q(final String O, final String P) 结束 因此,通过完成类 Q 构造函数,对象已经被正确地初始化。

我仍然认为情况3比情况2更加明确。为什么允许初始化器引用实例方法,如果不能保证其正常工作?在构造函数中也无法在super调用内部调用它们。 - daniu
1
请查看此链接:https://docs.oracle.com/javase/tutorial/java/javaOO/initial.html 请注意,只有final方法可以在实例变量初始化器中使用,这对于子类可能想要重用初始化方法特别有用。希望这能解决您的疑虑。 最好不要在这些方法中使用其他未初始化的实例变量,因为它们可能导致不一致的状态。 - Nagendra Varma
@daniu 你说得对,实际格式应该是这个,这不违反前向引用规则... https://dev59.com/AKjja4cB1Zd3GeqP81BV#48322068 - Eugene

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接