为什么在使用类名和静态变量时不显示非法向前引用错误?

6
在下面的代码中,使用类名访问静态变量时不会引发前向引用错误,但在不使用类名访问时会引发。为什么使用类名访问时不会发生这种情况呢?
class Test{
    static {
        System.out.println(a); // shows error
        a = 99; // and this line too doesn't give error  
        System.out.println(Test.a); // this line doesn't
    }
    static int a = 10;  
    static{
        System.out.println(a);
    }
}

1
请问您能否提供您的精确错误信息? - David says Reinstate Monica
你在尝试做什么?这段代码没有任何意义。我根据你的提供的信息回答了,但这是非常不合理的代码。 - JNYRanger
在Test中出现了非法的向前引用,针对第一个静态块中的System.out.println(a);这一行。 - Prasanna
4个回答

6
前向引用的规则在JLS §8.3.3中定义:

有时候,即使这些类变量在作用域内(§6.3),其声明在使用后面的类变量的使用也受到限制。如果以下所有条件均为真,则会出现编译时错误:

  • 类或接口C中类变量的声明在使用类变量之后以文本方式出现;

  • 使用是C类变量初始化程序或C的静态初始化程序中的简单名称;

  • 使用不在赋值的左侧;

  • C是包含使用的最内部类或接口。

所以,基本上你的第一个Sysout()满足以上4个条件,因此它是编译时错误。

在第二个 Sysout() 中,你使用了变量 a 的全限定名而不是简单名称,根据上述规则这是允许的。
现在,这么做的原因是,当你访问 Test.a 时,编译器确信 Test 类已被加载并且所有静态字段已被初始化,所以它可以访问字段 a。但是,在使用简单名称访问 a 时,编译器无法确定是否已经运行了 a 的初始化程序,因为它可能仍在加载类的过程中。
考虑以下类加载的过程:
  • 当一个类被加载时,为其中声明的所有static变量分配内存。此时,变量a已经分配了内存(声明完成)。
  • 然后按照出现顺序运行所有static初始化器。
    • 第一条语句是Sysout(a);a尚未初始化,因此无法访问它。(错误)
    • 第二个语句是a = 99。在这里,您实际上正在初始化变量a。非常好。
    • 第三个语句是Sysout(Test.a)——其推理已经在上面发布过了。编译器知道Test已经加载。
    • 然后执行static int a = 10。它将a重新初始化为10。请记住,在第一步中已经处理了声明部分。

@Prasanna 因为这实际上是在初始化变量。请阅读完整的答案,我已经在那里写了原因。 - Rohit Jain

2
首先,让我们看看JLS对于非法的前向引用有什么说法。
类变量的使用,即使这些类变量在作用域内(§6.3),但其声明出现在使用之后时,有时会受到限制。具体来说,如果以下所有条件都成立,则会出现编译时错误:
- 在类或接口C中,一个类变量的声明在使用该类变量之后; - 使用是C的类变量初始化器或静态初始化器中的简单名称; - 使用不在赋值的左侧; - C是最内层包含使用的类或接口。
实例变量的使用,即使这些实例变量在作用域内,其声明出现在使用之后时,有时也会受到限制。具体来说,如果以下所有条件都成立,则会出现编译时错误:
- 在类或接口C中,一个实例变量的声明在使用该实例变量之后; - 使用是C的实例变量初始化器或实例初始化器中的简单名称; - 使用不在赋值的左侧; - C是最内层包含使用的类或接口。
它为每个静态和实例变量定义了什么是非法的前向引用。然而,对于它们两者来说,定义似乎是相同的。由于您的问题涉及静态变量,我们只会深入探讨这个问题。
1)看到了吗,对于一个静态变量,有“声明”和“初始化”两个步骤。
 static int a; //only declaration
 static int b = 10; //both declaration and initialization

使用在使用之前文本上出现的类变量有时会受到限制,即使这些类变量在作用域内。这段话告诉我们什么?它说有时候即使我们在后面声明了静态变量,也可以使用它们。但在某些情况下,这是不允许的。那么,什么情况下是不允许的呢?
如果同时满足以下4个条件,就不允许使用,否则即使在声明之后也可以自由使用。
a) The declaration of a class variable in a class or interface C appears textually after a use of the class variable;

b) The use is a simple name in either a class variable initializer of C or a static initializer of C;

c) The use is not on the left hand side of an assignment;

d) C is the innermost class or interface enclosing the use.

好的,a)点很简单。它说当你在声明静态变量之前使用它时,你必须只看这些规则,否则为什么要深入研究JLS呢? b)点是,如果你使用简单名称,比如boy(而不是类名附加到它上面,比如MyClass.boy),那么你可能会遇到非法向前引用的问题,否则你是安全的,我的朋友。但是,仅仅满足这个条件并不符合非法向前引用的条件,否则你代码中的a=99会立即给我们报错。还有两个条件必须满足才能生成此错误。如果以下2个条件不符合,您可以像这样使用它。 c)点非常直观。你没有在赋值的左边使用它吗?如果我们看看a=99,没有!!System.out.println(a) - 这甚至不是一个赋值语句。因此,没有左手赋值是正确的。 d)点也很简单。它只告诉你在定义中他指的是哪个类/接口。你的C = Test。
现在让我们重新审视您的代码。对于每行代码,我会像这样评论`True+False+True+false`,意思是对于每行代码,a)、b)、c)、d)分别会给出什么评价。好吗?
class Test {

    static {
        System.out.println(a); // True + True + True +True ; Illegal forward reference
        a = 99; // True +  True + False +True ; No illegal forward reference
        System.out.println(Test.a); // True + False + True + True No illegal forward reference
    }

    static int a = 10;

    static {
        System.out.println(a);
    }
}

下一个问题可能是它会打印什么?如果我们在声明之前使用它,它会取什么值?
现在我们来讲解静态初始化的规则。在这里,我们将按照您的代码进行说明。
当类被加载且没有非法的向前引用时,所有静态变量都已被初始化为默认值并存在内存中。
现在,静态初始化完成后,程序继续执行。
class Test {

    static {
        System.out.println(Test.a);  // prints 0
        a = 99; // a get 99
        System.out.println(Test.a); // prints 99
    }

    static int a = 10;

    static {
        System.out.println(a); // prints 10
    }

    public static void main(String[] args) {

    }
}

0

类初始化器在类加载时运行,您无法确定确切的时间。在类初始化器中运行的代码应该只用于需要为其他静态方法(或类本身)正确工作而“初始化”的事物。

此外,您正在引用尚未声明的变量。在您的类初始化器(第一个静态块)中,您分配了a = 99,但a尚未声明。如果有任何东西,您需要先声明变量,然后在静态块中初始化它。

对于您发布的代码类型,没有理由使用类初始化器。如果有什么,这应该是一个静态方法。

以下是一个示例:

class Test{
    static int a = 10;

    static void doSomething(){
        System.out.println(a);
        a = 99;
        System.out.println(a);
    }
}

然后在主函数中你可以调用Test.doSomething();

请发帖询问静态块和静态变量的相关问题,谢谢。 - Prasanna
你想知道什么? - JNYRanger
当我给静态变量'a'赋值时,它不会显示前向引用错误,但如果在声明之前打印静态变量,则会显示错误。为什么?但是,当我们使用类名(Test.a)引用时,它不会显示错误。 - Prasanna
基本上是因为变量在调用之后声明,即使它仍然在作用域内。由于它已经在前一行抛出了错误,所以没有必要继续抛出相同的编译时错误。此外,通过以非设计方式使用构造函数(例如将类初始化器作为自动调用的void方法),您会使自己的生活更加复杂。不要误解我的意思,但我认为您需要更多地了解面向对象编程构造,例如类和对象,以及静态关键字如何修改它们,然后再使用像这样的静态块。 - JNYRanger

0

好的,静态块

static {
    System.out.println(a);
    a = 99;
    System.out.println(Test.a);
}

在声明变量 a 之前将被执行。

带有 Test.a 的行不会触发任何错误,因为编译器检查并找到了在类 Test 中声明的静态变量 a


因为此时变量a已经初始化为其默认值,然后执行a=99;语句将99赋值给它。如果您注释掉System.out.println(a);这行代码,程序的输出结果将是99和10。因此,首先它会将99赋值给变量a并打印出来,然后重新初始化为10并将其打印出来。 - Alexander
由于它是一个类级别变量,所以值从99变为10,分配的值将在类级别范围内而不是块级别范围内。 - Prasanna

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