在从类构造函数调用的方法中初始化一个final变量

9
今天我遇到了一个奇怪的问题,但我无法弄明白原因。
假设我们在Java中有一个典型类中的final变量。 我们可以立即初始化它或在类构造函数中初始化,如下所示:
public class MyClass {

    private final int foo;

    public MyClass() {
        foo = 0;
    }
}

但我不知道为什么我们不能在构造函数中调用方法并在该方法中初始化foo,就像这样:

public class MyClass {

    private final int foo;

    public MyClass() {
        bar();
    }

    void bar(){
        foo = 0;
    }
}

因为我认为我们仍处于构造函数流程中,它尚未完成。任何提示将不胜感激。


17
因为没有任何东西能够阻止你的 bar() 方法被调用超过一次。而 final 变量只能被赋值一次。 - user6073886
5
更不用说你可以扩展MyClass并重写bar()方法。那样一来,它根本就不会被分配了,而我们也不能允许这种情况发生。 - Kayaman
@OHGODSPIDERS 非常感谢,是的,我认为这就是原因,如果这不是评论,我会接受这个答案。 - Alireza
原因很简单: 您可以再次调用bar方法,它将尝试重新分配值给final成员。 您可以尝试将foo设置为static final,编译器不会允许您在构造函数中初始化它。因为构造函数每次创建类的实例时都会被调用,但是静态final成员即使通过不同的实例也无法重新分配。 - Srijan Mehrotra
@Echtniet 不,它们不同,因为构造函数还没有完成。 - Alireza
3个回答

11

首先,声明时赋值会被编译器复制到每个构造函数中。其次,您可以使用方法来初始化值,但需要return它才能生效。正如其他人所指出的那样,您需要确保这个值只设置一次。

public class MyClass {
    private final int foo = bar();

    private static int bar() {
        return 0;
    }
}

等价于

public class MyClass {
    private final int foo;

    public MyClass() {
        this.foo = bar();
    }

    private static int bar() {
        return 0;
    }
}

请注意barstatic的,否则你需要一个实例来调用它。


“在声明时赋值被复制”并不完全正确。对于final成员的内部赋值发生在任何构造函数之前,包括super()。当从父构造函数调用重写方法时(这是一种糟糕的设计,但确实会发生),这一点很重要。 - user3458

3
你只能初始化一次final变量。final变量有三种形式:
  • 类final变量
  • 实例final变量
  • 本地final变量。
对于类final变量,变量可以在声明或静态初始化器中初始化:
class Program {
static final int i1 = 10;
static final int i2;
static {
    i2 = 10;
}

例如,对于最终变量,变量可以在声明、实例初始化程序或构造函数中进行初始化:

class Program {
final int i1 = 10;
final int i2;
final int i3;
{
    i2 = 10;
}

Program() {
    i3 = 10;
}

对于本地最终变量,这些变量可以在声明时或其后的任何位置进行初始化。本地最终变量必须在使用之前进行初始化。

class Program {
void method() {
     final int i1 = 10;
     final int i2;
     System.out.println(i1);
     i2 = 10;
     System.out.println(i2);
     return ;
}

来源:参见链接参考链接


1

字段(或变量)上的final修饰符意味着编译器将确保以下两点:

  • 该字段在对象构造期间至少初始化一次,除非对象构造失败。
  • 该字段最多只能初始化一次。

对于您的代码,以上两点都无法得到保证:

  • 某个子类可能会覆盖bar方法。
  • 同一包中的其他类可能会再次调用bar方法。

使用私有方法而不是包私有方法可能很诱人。虽然它可以保证这两个条件(除非您试图通过反射来破坏它),但javac仍然不会接受它,因为它不够强大。这里有一些很好的理由:

  1. 首先,它必须有一些限制。如果编译器能够完全确定两个条件是否满足,那么它将能够解决停机问题,这是不可能的。因此,选择了一些合理的子集。
  2. 想象一下它能够检测到这种特殊情况。这意味着您有一些私有方法必须从构造函数中调用,而不能从其他地方调用。在这种情况下,程序员需要一个描述性错误消息,说明为什么不能在此处调用这样的私有方法(乍一看像普通方法)。后来,有人会创建一些有条件地分配给最终字段的怪物方法。对于某些足够复杂的条件,javac将无法找出它是否将其分配给最终字段,因此某些人将面临一些神秘的错误消息。我认为在这种情况下很难做出好的错误消息。
  3. 调用私有实例方法已经很棘手了。由于该方法将在未完全初始化的对象上操作,因此它可以读取一些未初始化(甚至是final)属性。
  4. 我认为构造函数应该相当短小简单,通常只是将参数分配给字段,再加上一些验证。如果事情变得复杂,您可能需要创建一个工厂方法。对象的创建将变得明确分离于对象方法,而在需要调用私有方法时并非如此。

语言设计者们决定支持一些已经被广泛理解的情况,而不是进行类似于猫捉老鼠的游戏。在其他情况下,代码可能需要重构。因此,语言创作者可以将注意力集中在更重要的方面。


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