【问题标题】:Are there circumstances under which Java does not initialize static fields immediately?是否存在 Java 不立即初始化静态字段的情况?
【发布时间】:2015-02-24 20:27:37
【问题描述】:

在一个更大的项目中,我遇到了静态字段初始化的奇怪行为(至少就我的理解而言)。据我了解,所有静态字段都应该在程序启动时初始化,这意味着当开始使用非静态字段时,不应该有任何未初始化的静态字段(更准确地说,所有静态赋值“field = .. ." 应该已经执行)。

以下代码不是 MWE,因为它符合我的预期,但它本质上正是我在更大的上下文中所做的。我无法创建一个导致相同问题的较小示例。

运行此代码时:

import java.util.HashSet;

public class FB {
    private static final HashSet<String> collection = new HashSet<>();
    public static final String foo = bar("item");

    public static String bar(String newItem) {
        collection.add(newItem);
        System.out.println("Yes, I've been invoked, and I currently store this: " + collection);
        return newItem;
    }

    public static void main(String[] args) {
    }
}

输出是(因为 Java 首先通过调用 bar(.) 初始化静态字段 'collection' 然后 foo):

Yes, I've been invoked, and I currently store this: [item]

到目前为止,一切都很好。在实际项目中,我正是这样做的(尽管 foo 和 bar(.) 在不同的类中),但是在我实际使用 foo 的值之前不会调用 bar(.)。 (至少这种情况发生在五分之一的情况下——它们都以与上面所示相同的方式创建。其他四个工作正常。)是否有任何情况会导致 Java 出现这样的行为?

我已经看过这些讨论,但它们似乎没有抓住我的问题:

When are static variables are initialized?
Why static fields are not initialized in time?
Java Static Field Initialization

我意识到,当交换 foo 和 collection 的位置时,方法调用无法工作,因为当 foo 被初始化时,collection 不会被初始化(或者更确切地说,初始化为 null?)。 (老实说,我不确定静态字段在不同类中的初始化顺序,所以这可能是问题的根源。)但这会导致

Exception in thread "main" java.lang.ExceptionInInitializerError

而不仅仅是不调用 bar(.)。

如果需要,我可以提供有关实际项目的更多详细信息,但到目前为止我不知道还有什么感兴趣的。 (抱歉,描述的太模糊了,但到目前为止我只有这些。)

【问题讨论】:

  • main 的第一行被执行时,你对“程序启动”的确切定义是什么? (当然,在那之前可能有代码在运行,包括在所有静态初始化程序运行之前。)
  • 如果你不能提供最小的重现示例,我建议你使用调试器来查看初始化是如何发生的。
  • 我想,我的误解毕竟可能很简单。 Eddie B 写道,如果一个类没有被加载,相应的静态字段也不会被初始化。如果这是真的,它将解释我观察到的行为。我会调查的!
  • 嗯,是的,如果 JVM 不加载类,它甚至不知道类中有哪些静态变量。所以任何与静态变量相关的事情都只能在类加载之后发生。
  • 是的,这就是问题所在。谢谢大家的支持——我学到了很多! (虽然最后答案很明显。)

标签: java static-initialization


【解决方案1】:

静态变量由 JVM 类加载器实例化,并由类的每个实例共享。

public class StaticVars {

    static int i = 3;

    public static void main( String[] args ) {
        System.out.println( "Called at Runtime: " + getStaticVar() );
        System.out.println( "Called via it's static member: " + i );
    }


    static int getStaticVar() {
        return i;
    }

    static {
        int i = 1;
        System.out.println( "JVM ClassLoaded: " + i );
    }

    static {
        int i = 2;
        System.out.println( "Second JVM ClassLoaded: " + i);
    }

}

此外,正如dognoseNPE 所引用的,如果您在初始化之前尝试引用静态变量,您将得到Illegal Forward Reference Error,因为静态字段是按顺序初始化的。以下限制适用于静态fields 静态方法的检查方式不同。

8.3.2.3. Restrictions on the use of Fields during Initialization

  • 仅当成员是类或接口 C 的实例(分别为静态)字段并且满足以下所有条件时,成员的声明才需要在使用前以文本形式出现:

  • 在 C 的实例(分别为静态)变量初始化器或 C 的实例(分别为静态)初始化器中使用。

  • 用法不在作业的左侧。

  • 用法是通过一个简单的名称。

  • C 是包含用法的最内层类或接口。

更多信息 :: Illegal forward reference error for static final fields

【讨论】:

  • 啊!所以除非加载了周围的类,否则不会初始化静态字段? (当然,这是有道理的......但我没有明白!)
  • @lukas.coenig 雅。静态变量属于Class
【解决方案2】:

如果你编译上面的类,你可以看到在静态初始化块中调用了bar。

static <clinit>()V
  L0
    LINENUMBER 4 L0
    NEW java/util/HashSet
    DUP
    INVOKESPECIAL java/util/HashSet.<init> ()V
    PUTSTATIC FB.collection : Ljava/util/HashSet;
  L1
    LINENUMBER 5 L1
    LDC "item"
    INVOKESTATIC FB.bar (Ljava/lang/String;)Ljava/lang/String;
    PUTSTATIC FB.foo : Ljava/lang/String;
    RETURN
    MAXSTACK = 2
    MAXLOCALS = 0

FB 中看到不完整的初始化类的唯一方法是已经在静态块中。

例如说FB.&lt;clinit&gt;() 调用另一个类,该类在初始化之前又使用FB.foo,它将看到null 而不是调用bar()

【讨论】:

  • 我不确定如何阅读代码...这是原始字节码吗? (不过,我相信,如果 null 初始化是问题所在,我会收到异常而不是所描述的行为。)
  • @lukas.coenig 这是来自javap 的字节码转储。如果此类由于先前的错误而无法初始化,您将收到 ExceptionInInitializerError。您需要回顾您的错误/异常以找到真正的原因。
【解决方案3】:

当开始使用非static 字段时,不应有任何未初始化的static 字段(更准确地说,应该已执行所有静态分配field = ...)。

一般情况下是这样的。但是,可以构建一个不存在的示例。 The following code 打印出来

static_obj=null
instance_obj=4

意思是static_objinstance_obj已经被初始化的时候还没有被初始化:

class Ideone
{
    static {
        new Ideone();
    }   

    public static final Object static_obj = new Integer(42);
    public final Object instance_obj = new Integer(4);

    public Ideone() {
        System.out.printf("static_obj=%s\n", static_obj);
        System.out.printf("instance_obj=%s\n", instance_obj);
    }

    public static void main(String[] args) {
    }
}

【讨论】:

    【解决方案4】:

    静态字段按照它们声明的顺序进行初始化。因此,如果您将字段的顺序更改为:

    import java.util.HashSet;
    
        public class FB {
            public static final String foo = bar("item");
            private static final HashSet<String> collection = new HashSet<>();
    
            public static String bar(String newItem) {
                collection.add(newItem);
                System.out.println("Yes, I've been invoked, and I currently store this: " + collection);
                return newItem;
            }
    
            public static void main(String[] args) {
            }
        }
    

    当您尝试访问尚未初始化的集合时,您将在bar-方法中收到NullpointerException。 (因为您对bar() 的调用发生在初始化第一个变量的过程中)

    Exception in thread "main" java.lang.ExceptionInInitializerError                                
    
    Caused by: java.lang.NullPointerException                                                       
    
            at HelloWorld.bar(HelloWorld.java:14)                                                   
    
            at HelloWorld.<clinit>(HelloWorld.java:10)    
    

    【讨论】:

    • 是的,但这正是我没有得到的——也是我感到困惑的原因......但我现在解决了,请参阅上面的 cmets。
    【解决方案5】:

    类的静态字段和静态初始化块并不总是被初始化/执行。如果您不使用类或不加载它,则不会发生。例如 - 假设您有:

    class Test
    {
        static
        {
            System.out.println("Test");
        }
    
        public static int a = 4; 
    }
    

    如果不使用Test类静态块将不会被执行。因此,您必须例如实例化一个类来初始化静态字段并调用静态初始化块:

    Test t = new Test();
    

    或使用任何静态的东西,例如:

    System.out.println(Test.a); // this will trigger 'initialisation'
    

    毕竟 - 如果您不在代码中使用静态数据 - 为什么 JVM 会费心用它做任何事情 - 认为这是合理的优化 ;-)。

    另一个使用 JDBC 的例子——你必须调用一次才能让驱动程序自己“初始化”——即。执行所有静态的和以后需要的东西:

    Class.forName( "com.mysql.jdbc.Driver" ).newInstance();
    

    顺便说一句:您可以这样初始化您的收藏:

    private static final HashSet<String> collection = new HashSet<String>() {{add("item");}};
    

    【讨论】:

    • 是的,这正是问题所在......谢谢!
    • 但是 - 如果 ACCESS 导致初始化 - 访问未初始化的值怎么会发生? :)
    • @dognose:如果您不初始化 obj 字段的类,它将被初始化为其默认值,在这种特殊情况下(int)将为 0。如果您不尝试“打印”'a ' 初始化不会发生。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-10-06
    相关资源
    最近更新 更多