6

我可以从 Java 中的静态初始化程序调用静态方法吗?以下内容是否有效并保证按照 Java 规范工作?

public class Foo {

  private final static int bar;

  private static int generateValue() {
    return 123;
  }

  static {
    bar = generateValue();
  }

}

让我想知道的是,我可能期望bar在里面有空generateValue()。我知道静态初始化块的顺序很重要,但我没有听说过静态方法声明的顺序很重要。但是在执行静态初始化块之前,静态方法是否可用?

4

3 回答 3

5

正如@Mureinik 所说,“总之——是的。这个代码是完全合法的。” 我想提供一个更彻底的答案,因为在将静态初始化程序与类方法结合使用时,您很容易出现问题——出现的顺序会影响类状态,而且这是一个令人讨厌的错误

类中声明的静态初始化器在类初始化时执行。与类变量的任何字段初始化器一起......静态初始化器可用于初始化类的类变量——Java语言规范(JLS)§8.7

初始化通常按照出现的顺序进行(称为文本排序)。例如,考虑以下代码:

class Bar {
    static int i = 1;
    static {i += 1;}
    static int j = i;
}

class Foo {
    static int i = 1;
    static int j = i;
    static {i += 1;}

    public static void main(String[] args) {
        System.out.println("Foo.j = " + Foo.j);
        System.out.println("Bar.j = " + Bar.j);
    }
}

Foo.j和的值Bar.j不同,因为代码的文本顺序不同:

Foo.j = 1
Bar.j = 2

OP 的示例按文本顺序执行。但是如果代码被重新排列,比如说,以相反的顺序:

class Foo {
    static { bar = generateValue(); }                  //originally 3rd
    private static int generateValue() { return 123; } //originally 2nd
    private final static int bar;                      //originally 1st

    public static void main(String[] args) {
        System.out.println("Foo.bar = " + Foo.bar);
    }
}

事实证明,这编译没有错误。此外,输出为:Foo.bar = 123。所以,bar实际上确实包含123在运行时。但是,以下代码(来自JLS §8.3.1.1j )会产生编译时错误,因为它在声明之前尝试访问j

//Don't do this!
class Z { 
    static { i = j + 2; } //Produces a compilation error
    static int i, j;
    static { j = 4; }
}

有趣的是,方法的访问不会以这种方式检查,因此:

class Foo {
    static int peek() { return j; }
    static int i = peek();
    static int j = 1;

    public static void main(String[] args) {
        System.out.println("Foo.i = " + Foo.i);
    }
}

产生以下输出:

Foo.i = 0

发生这种情况是因为

变量初始化器i使用类方法peek访问变量的值j之前j已被其变量初始化器初始化,此时它仍然具有其默认值——JLS §8.3.2.3

如果相反,在之后i初始化,则输出为 j

Foo.i = 1

当使用对象而不是原始类型时,情况会变得更糟,例如:

class Foo { //Don't do this
    static int peek() { return j.hashCode(); } // NullPointerException here
    static int i = peek();
    static Object j = new Object();

    public static void main(String[] args) {
        System.out.println("Foo.i = " + Foo.i);
    }
}

peek会引发一段NullPointerException时间的初始化i

Exception in thread "main" java.lang.ExceptionInInitializerError
Caused by: java.lang.NullPointerException
    at TestGame.Foo.peek(Foo.java:4)
    at TestGame.Foo.<clinit>(Foo.java:5)

运行上述代码时,Eclipse 会弹出此窗口:

发生 Java 异常

将其与 OP 联系起来,如果123generateValue()方法不是返回,而是返回某个其他静态字段(或方法)的值,那么 的值bar取决于代码的文本顺序。

那么,什么时候文本排序很重要?

并不总是使用文本排序。有时 JVM 会提前执行初始化。重要的是要知道何时可以进行前瞻,以及何时以文本顺序进行初始化。JLS在 §8.3.2.3 中描述了初始化期间使用字段的限制(强调我自己的):

仅当成员是 [a] ...类或接口 C 的静态字段并且满足以下所有条件时,成员的声明才需要在使用之前以文本形式出现

  • 用法出现在 [a]... C 的静态变量初始化器或 [a] ...C 的静态初始化器中。
  • 用法不在作业的左侧。
  • 用法是通过一个简单的名称。
  • C 是包含用法的最里面的类或接口。

最后一点:首先初始化常量(根据 JLS §8.3.2.1):

在运行时,最终的静态字段和使用常量表达式(第 15.28 节)初始化的静态字段首先被初始化(第 12.4.2 节)。

于 2016-03-14T04:41:17.767 回答
3

一句话—​​—是的。这是完全合法的代码,应该 [静态地] 初始化bar123.

于 2016-03-13T23:23:11.133 回答
1

静态初始化器在这里真的是不必要的,我不会使用它。你可以做

private final static int bar = generateValue();

即使 generateValue() 方法是在静态成员之后定义的(我只是尝试确定)。

在我的书中,静态初始化器仅在复杂初始化或初始化器可以抛出异常时才需要。例如,这是行不通的

private final InetAddress inet = InetAddress.getByName ("some bad host name");

因为可以抛出异常。如果需要处理 if-then-else 逻辑,可能需要一个临时变量,或者其他任何不是直接赋值的东西,您还需要使用静态初始化程序。

但是对于您在这里所拥有的,静态初始化程序块是完全无关的,在我看来,这不是模拟的最佳实践。

于 2016-03-14T00:43:35.157 回答