静态初始化块

308
据我理解,“静态初始化块”用于为静态字段设置值,如果无法在一行中完成。但是我不明白为什么需要一个特殊的块来完成这个任务。例如,我们声明一个字段为静态(没有赋值),然后编写几行代码生成并分配一个值给上述声明的静态字段。那么我们为什么需要将这些行放在像static {...}这样的特殊块内呢?

6
小小的反馈,但请您能否清楚地陈述您的假设,并因此澄清哪个答案是正确的。当我第一次阅读您的问题时,我误解了并认为您知道{...}static {...}之间的区别(在这种情况下,Jon Skeet 的回答肯定更好)。 - David T.
1
这个问题非常不清楚,你让回答者们猜测很多并且做出了很多冗长的推测。为什么不明确地写出你所想要的静态初始化块的示例和你的替代方案,这样人们就有明确的东西可以回答了呢? - Don Hatch
14个回答

508

非静态代码块:

{
    // Do Something...
}

每当类的实例被创建时,它都会被调用。静态块只会在类本身初始化时被调用一次,不管你创建多少个该类型的对象。

例如:

public class Test {

    static{
        System.out.println("Static");
    }

    {
        System.out.println("Non-static block");
    }

    public static void main(String[] args) {
        Test t = new Test();
        Test t2 = new Test();
    }
}

这将打印:

Static
Non-static block
Non-static block

48
它回答了这个问题:“每次构造类时都会调用它。静态块只会被调用一次,无论你创建多少该类型的对象。” - Adam Arold
95
对于好奇的读者,非静态代码块实际上是由Java编译器复制到类的每个构造函数中的(来源)。因此,初始化字段仍然是构造函数的工作。 - Martin Andersson
19
为什么这个答案突然被踩了?你可能不同意它被接受为答案,但它肯定不是错误或误导的。它只是试图用一个简单的例子来帮助理解这些语言结构。 - Frederik Wordenskjold
1
也许这不是真正回答问题的答案,但它回答了我读真正答案时提出的问题。 :-) 这是因为它获得了赞。而真正的答案也会得到赞,因为这是公正的。 - peterh
1
JVM如何知道一个类是被构造了一次还是多次 - Avinash Kumar
显示剩余2条评论

145

如果它们不在静态初始化块中,它们会在哪里?如何声明一个仅用于初始化的局部变量,并将其与字段区分开?例如,你希望如何编写:

public class Foo {
    private static final int widgets;

    static {
        int first = Widgets.getFirstCount();
        int second = Widgets.getSecondCount();
        // Imagine more complex logic here which really used first/second
        widgets = first + second;
    }
}
如果firstsecond不在一个块内,它们看起来像字段。如果它们在没有static前缀的块中,那将被视为实例初始化块而不是静态初始化块,因此它将在每个构造实例时执行一次,而不是总共执行一次。
现在在这种特殊情况下,您可以使用静态方法代替:
public class Foo {
    private static final int widgets = getWidgets();

    static int getWidgets() {
        int first = Widgets.getFirstCount();
        int second = Widgets.getSecondCount();
        // Imagine more complex logic here which really used first/second
        return first + second;
    }
}

但是当你希望在同一块中分配多个变量或者不分配任何变量(例如,如果你只想记录信息--或者可能初始化一个本地库),这种方法就行不通了。


1
静态块是在静态变量被分配之前还是之后发生的?private static int widgets = 0; static{widgets = 2;} - Weishi Z
1
我很好奇静态块是在静态变量分配之前还是之后发生的。例如:private static int widgets = 0; static{widgets = 2;} 我发现'='赋值按顺序进行,这意味着先放置的'='将首先被分配。上面的例子将为'widgets'赋值为2。(附注:不知道评论只能在5分钟内编辑...) - Weishi Z
@WeishiZeng:是的,这正如http://docs.oracle.com/javase/specs/jls/se8/html/jls-12.html#jls-12.4.2 - 第9点所述。 - Jon Skeet
但是你也可以使用一个私有静态方法,该方法具有与静态初始化块完全相同的代码,并将小部件分配给私有静态方法,对吧? - Zachary Kraus
1
@Zachary:你是指返回值并将方法调用的结果赋值吗?如果是这样,当你作为块的结果只分配给一个变量时,是的。我会在大约7个小时后编辑我的答案并提供详细信息... - Jon Skeet
@JonSkeet 好的,谢谢你回答了我一个重要的问题,关于何时必须使用静态初始化块和私有静态方法来初始化静态字段。 - Zachary Kraus

108

以下是一个示例:

  private static final HashMap<String, String> MAP = new HashMap<String, String>();
  static {
    MAP.put("banana", "honey");
    MAP.put("peanut butter", "jelly");
    MAP.put("rice", "beans");
  }

在“static”部分的代码将在类加载时执行,在构造任何实例(以及从其他地方调用任何静态方法之前)。这样一来,您可以确保类资源已准备好供使用。

还可以有非静态初始化块。它们就像是为类定义的构造方法集的扩展。它们看起来就像静态初始化块,只是没有使用关键字“static”。


4
有时候,在那个特定的例子中,双括号模式被“滥用” :) - BalusC
它可能会被滥用,但另一方面它确实可以清理一些混乱,并使某些类型的代码更加“稳定”。我喜欢用Erlang编程,你会迷上不需要本地变量的感觉 :-) - Pointy
1
“static”部分的代码将在类加载时执行,在构造类的任何实例之前(以及从其他地方调用任何静态方法之前)。这样,您可以确保类资源都已准备好使用。这是非常重要的一点,需要注意静态块的执行。 (以上答案中提到的“Pointy”) - learner
我们能否在 afterPropertiesSet 方法中使用 InitializingBean 来实现这个功能? - egemen

51

当你实际上不想将值分配给任何东西时,例如在运行时仅加载某个类时,它也非常有用。

例如:

static {
    try {
        Class.forName("com.example.jdbc.Driver");
    } catch (ClassNotFoundException e) {
        throw new ExceptionInInitializerError("Cannot load JDBC driver.", e);
    }
}

嘿,还有另一个好处,你可以用它来处理异常。想象一下,这里的getStuff()抛出了一个Exception,它真的属于一个catch块:

private static Object stuff = getStuff(); // Won't compile: unhandled exception.

如果需要在赋值期间处理异常,可以使用static初始化器。在那里处理异常会很有用。

另一个例子是在赋值期间无法完成的后续操作:

private static Properties config = new Properties();

static {
    try { 
        config.load(Thread.currentThread().getClassLoader().getResourceAsStream("config.properties");
    } catch (IOException e) {
        throw new ExceptionInInitializerError("Cannot load properties file.", e);
    }
}

回到JDBC驱动程序的示例,任何良好的JDBC驱动程序本身也利用static初始化器在DriverManager中注册自己。另请参见答案。


2
这里存在危险的巫术... 静态初始化器在合成的clinit()方法中运行,该方法是隐式同步的。这意味着JVM将获取有关类文件的锁定。如果两个类尝试相互加载,并且每个类都在不同的线程中开始加载,则这可能导致多线程环境中的死锁。请参见http://www-01.ibm.com/support/docview.wss?uid=swg1IV48872 - Ajax
@Ajax:我认为这要么是JDBC驱动程序的bug,要么是负责加载它的应用程序代码的问题。通常情况下,如果使用良好的JDBC驱动程序,在应用程序启动期间全局仅加载一次,就不会有任何问题。 - BalusC
这肯定是一个 bug,但并不完全是 JDBC 驱动程序的错。也许驱动程序天真地有自己的静态初始化程序,也许你无辜地初始化了这个类和应用程序中的其他一些类,然后,意外的是,一些意想不到的类彼此循环加载,现在你的应用程序死锁了。我发现这是由于 java.awt.AWTEvent 和 sun.util.logging.PlatformLogger 之间的死锁引起的。 我只是触及 AWTEvent 告诉它运行 headless,并且某些其他的库最终加载了 PlatformLogger... 而 AWTEvent 也加载了它。 - Ajax
1
两个类在不同的线程上都被同步了,我的构建大约有1/150次死锁。因此,我现在对静态块中的类加载更加小心。在上述情况下,使用延迟提供程序模式,我可以立即创建一个临时提供程序类(没有死锁的机会),初始化字段,然后当它实际被访问时(在非同步字段访问中),然后我实际上加载可能导致死锁的类。 - Ajax

12

我认为静态块只是语法糖。你不能使用静态块做任何其他事情。

以下是一些已发布的示例,可以重复使用此代码。

可以重写此代码段,而不使用静态初始化程序。

方法#1:使用静态

private static final HashMap<String, String> MAP;
static {
    MAP.put("banana", "honey");
    MAP.put("peanut butter", "jelly");
    MAP.put("rice", "beans");
  }

方法2:不使用 static
private static final HashMap<String, String> MAP = getMap();
private static HashMap<String, String> getMap()
{
    HashMap<String, String> ret = new HashMap<>();
    ret.put("banana", "honey");
    ret.put("peanut butter", "jelly");
    ret.put("rice", "beans");
    return ret;
}

11

它的存在有几个实际原因:

  1. 初始化可能会引发异常的static final成员
  2. 使用计算值初始化static final成员

人们倾向于使用static {}块作为初始化类运行时所依赖的东西的方便方式,例如确保特定类已加载(例如,JDBC驱动程序)。虽然可以用其他方式完成这些操作,但我提到的这两件事只能用像static {}块这样的结构来完成。


9

在对象构造之前,您可以在静态块中为类执行代码片段。

例如:

class A {
  static int var1 = 6;
  static int var2 = 9;
  static int var3;
  static long var4;

  static Date date1;
  static Date date2;

  static {
    date1 = new Date();

    for(int cnt = 0; cnt < var2; cnt++){
      var3 += var1;
    }

    System.out.println("End first static init: " + new Date());
  }
}

7

普遍误解认为静态块只能访问静态字段。为此,我想展示下面这段代码,它是我在实际项目中经常使用的(部分引用自另一个答案,但上下文略有不同):

public enum Language { 
  ENGLISH("eng", "en", "en_GB", "en_US"),   
  GERMAN("de", "ge"),   
  CROATIAN("hr", "cro"),   
  RUSSIAN("ru"),
  BELGIAN("be",";-)");

  static final private Map<String,Language> ALIAS_MAP = new HashMap<String,Language>(); 
  static { 
    for (Language l:Language.values()) { 
      // ignoring the case by normalizing to uppercase
      ALIAS_MAP.put(l.name().toUpperCase(),l); 
      for (String alias:l.aliases) ALIAS_MAP.put(alias.toUpperCase(),l); 
    } 
  } 

  static public boolean has(String value) { 
    // ignoring the case by normalizing to uppercase
    return ALIAS_MAP.containsKey(value.toUpper()); 
  } 

  static public Language fromString(String value) { 
    if (value == null) throw new NullPointerException("alias null"); 
    Language l = ALIAS_MAP.get(value); 
    if (l == null) throw new IllegalArgumentException("Not an alias: "+value); 
    return l; 
  } 

  private List<String> aliases; 
  private Language(String... aliases) { 
    this.aliases = Arrays.asList(aliases); 
  } 
} 

这里的初始化器用于维护一个索引(ALIAS_MAP),将一组别名映射回原始枚举类型。它旨在扩展Enum自身提供的内置valueOf方法。
正如您所看到的,静态初始化器甚至访问了private字段aliases。重要的是要理解,在枚举类型中初始化和执行顺序的情况下static块已经可以访问Enum值实例(例如ENGLISH)。这是因为需要Enum构造函数和实例块以及实例初始化先发生。
  1. 隐式静态字段的Enum常量。这也需要Enum构造函数和实例块,以及实例初始化首先发生。
  2. static块按出现顺序进行静态字段初始化。
这种乱序初始化(构造函数在static块之前)很重要。当我们像单例模式一样使用实例来初始化静态字段时,也会发生这种情况(做了简化):
public class Foo {
  static { System.out.println("Static Block 1"); }
  public static final Foo FOO = new Foo();
  static { System.out.println("Static Block 2"); }
  public Foo() { System.out.println("Constructor"); }
  static public void main(String p[]) {
    System.out.println("In Main");
    new Foo();
  }
}

我们看到的是以下输出:
Static Block 1
Constructor
Static Block 2
In Main
Constructor

清楚的是,静态初始化实际上可以在构造函数之前甚至之后发生:
仅仅访问主方法中的Foo,就会导致类被加载并开始静态初始化。但作为静态初始化的一部分,我们再次调用静态字段的构造函数,之后它恢复静态初始化,并完成从主方法中调用的构造函数。这是一个相当复杂的情况,希望在正常编码中我们不必处理。
有关更多信息,请参阅书籍“Effective Java”。

1
拥有对aliases的访问权限并不意味着静态块可以访问非静态成员。aliases是通过values()方法返回的Language值来访问的。 正如您所提到的,枚举变量在此时已经可用是不寻常的 - 普通类的非静态成员在这种情况下是不可访问的。 - Ignazio
静态块仍然只访问静态字段(在您的枚举ENGLISH,GERMAN等情况下...),这些字段在此情况下是对象。由于静态字段本身就是对象,因此可以访问静态对象的实例字段。 - Swami PR
1
类 Foo {static final Foo Inst1; static final Foo Inst2; static{ Inst1 = new Foo("Inst1"); Inst2 = new Foo("Inst2"); } static { System.out.println("Inst1: " + Inst1.member); System.out.println("Inst2: " + Inst2.member); } private final String member; private Foo(String member){ this.member = member; }}上述代码与枚举示例没有区别,仍允许在静态块中访问实例变量。 - Swami PR
@SwamiPR确实,令我惊讶的是它编译通过了,而且我不得不承认代码原理上并没有什么不同。我需要重新阅读Java规范,感觉有些地方我漏掉了。回复得很好,谢谢。 - YoYo
@SwamiPR 问题实际上是我们应该使用“枚举”。这是确保我们指向单个实例的最佳方法-请参见此处。至于您提到的几点,我已经进行了几次更新。 - YoYo

4

如果你有一个静态字段(也称为“类变量”,因为它属于类而不是类的实例;换句话说,它与类相关联而不是与任何对象相关联),并且你想要初始化它。所以如果你不想创建这个类的实例并且想操作这个静态字段,有三种方法:

1- 当你声明变量时,只需将其初始化:

static int x = 3;

2- 添加静态初始化块:

static int x;

static {
 x=3;
}

3- 需要有一个类方法(静态方法),用于访问类变量并进行初始化: 这是替代上述静态块的方法;你可以编写一个私有的静态方法:

public static int x=initializeX();

private static int initializeX(){
 return 3;
}

为什么要使用静态初始化块而不是静态方法?

这取决于你程序中的需求。但你必须知道,静态初始化块只会被调用一次,而类方法的唯一优点是如果你需要重新初始化类变量,它们可以被重复使用。

比如说,在你的程序中有一个复杂的数组。你使用 for 循环来初始化它,然后在程序运行过程中这个数组的值会发生变化,但是在某些时候你想重新初始化它(回到初始值)。在这种情况下,你可以调用私有的静态方法。如果你的程序不需要重新初始化值,那么你可以使用静态块,而不需要静态方法,因为你在程序中不会再次使用它。

注意:静态块按照它们在代码中出现的顺序被调用。

示例1:

class A{
 public static int a =f();

// this is a static method
 private static int f(){
  return 3;
 }

// this is a static block
 static {
  a=5;
 }

 public static void main(String args[]) {
// As I mentioned, you do not need to create an instance of the class to use the class variable
  System.out.print(A.a); // this will print 5
 }

}

示例2:

class A{
 static {
  a=5;
 }
 public static int a =f();

 private static int f(){
  return 3;
 }

 public static void main(String args[]) {
  System.out.print(A.a); // this will print 3
 }

}

3
如果您的静态变量需要在运行时设置,那么static {...}块非常有用。例如,如果您需要将静态成员设置为存储在配置文件或数据库中的值。当您想要向静态Map成员添加值时也很有用,因为您不能在初始成员声明中添加这些值。

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