106

所以我最近一直在复习我的 Java 技能,并发现了一些我以前不知道的功能。静态和实例初始化器就是两种这样的技术。

我的问题是什么时候会使用初始化程序而不是在构造函数中包含代码?我想到了几个明显的可能性:

  • 静态/实例初始化器可用于设置“最终”静态/实例变量的值,而构造函数不能

  • 静态初始化器可用于设置类中任何静态变量的值,这应该比在每个构造函数的开头使用“if (someStaticVar == null) // do stuff”代码块更有效

这两种情况都假设设置这些变量所需的代码比简单的“var = value”更复杂,否则似乎没有任何理由使用初始化程序而不是在声明变量时简单地设置值。

然而,虽然这些并不是微不足道的收获(尤其是设置最终变量的能力),但似乎确实有相当有限的情况应该使用初始化程序。

一个人当然可以使用初始化器来完成构造函数中所做的很多事情,但我真的不明白这样做的原因。即使一个类的所有构造函数共享大量代码,对我来说,使用私有 initialize() 函数似乎比使用初始化器更有意义,因为它不会锁定你在编写新代码时运行该代码构造函数。

我错过了什么吗?是否还有许多其他情况需要使用初始化程序?或者它真的只是在非常特定的情况下使用的一个相当有限的工具?

4

10 回答 10

62

静态初始化器就像提到的 cletus 一样有用,我以相同的方式使用它们。如果您有一个要在加载类时初始化的静态变量,那么静态初始化程序就是要走的路,特别是因为它允许您进行复杂的初始化并且仍然拥有静态变量 be final。这是一个很大的胜利。

我发现“if (someStaticVar == null) // do stuff”很混乱且容易出错。如果它是静态初始化并声明final的,那么您就避免了它的可能性null

但是,当您说:

静态/实例初始化器可用于设置“最终”静态/实例变量的值,而构造函数不能

我假设你说的是:

  • 静态初始值设定项可用于设置“最终”静态变量的值,而构造函数不能
  • 实例初始值设定项可用于设置“最终”实例变量的值,而构造函数不能

而且您在第一点上是正确的,在第二点上是错误的。例如,您可以这样做:

class MyClass {
    private final int counter;
    public MyClass(final int counter) {
        this.counter = counter;
    }
}

此外,当构造函数之间共享大量代码时,处理此问题的最佳方法之一是链接构造函数,提供默认值。这很清楚正在做什么:

class MyClass {
    private final int counter;
    public MyClass() {
        this(0);
    }
    public MyClass(final int counter) {
        this.counter = counter;
    }
}
于 2009-04-29T22:56:54.673 回答
59

匿名内部类不能有构造函数(因为它们是匿名的),所以它们非常适合实例初始化器。

于 2009-04-29T22:55:48.450 回答
27

我最常使用静态初始化程序块来设置最终的静态数据,尤其是集合。例如:

public class Deck {
  private final static List<String> SUITS;

  static {
    List<String> list = new ArrayList<String>();
    list.add("Clubs");
    list.add("Spades");
    list.add("Hearts");
    list.add("Diamonds");
    SUITS = Collections.unmodifiableList(list);
  }

  ...
}

现在这个例子可以用一行代码完成:

private final static List<String> SUITS =
  Collections.unmodifiableList(
    Arrays.asList("Clubs", "Spades", "Hearts", "Diamonds")
  );

但是静态版本可以更整洁,尤其是当项目初始化时。

幼稚的实现也可能不会创建不可修改的列表,这是一个潜在的错误。上面创建了一个不可变的数据结构,您可以愉快地从公共方法等返回。

于 2009-04-29T22:44:47.277 回答
16

只是在这里添加一些已经很出色的观点。静态初始化器是线程安全的。它在类加载时执行,因此与使用构造函数相比,静态数据初始化更简单,在构造函数中,您需要一个同步块来检查静态数据是否已初始化,然后实际初始化它。

public class MyClass {

    static private Properties propTable;

    static
    {
        try 
        {
            propTable.load(new FileInputStream("/data/user.prop"));
        } 
        catch (Exception e) 
        {
            propTable.put("user", System.getProperty("user"));
            propTable.put("password", System.getProperty("password"));
        }
    }

相对

public class MyClass 
{
    public MyClass()
    {
        synchronized (MyClass.class) 
        {
            if (propTable == null)
            {
                try 
                {
                    propTable.load(new FileInputStream("/data/user.prop"));
                } 
                catch (Exception e) 
                {
                    propTable.put("user", System.getProperty("user"));
                    propTable.put("password", System.getProperty("password"));
                }
            }
        }
    }

不要忘记,您现在必须在类而不是实例级别进行同步。这会为每个构造的实例带来成本,而不是加载类时的一次性成本。另外,它很丑;-)

于 2009-04-30T13:29:34.683 回答
13

我阅读了整篇文章来寻找初始化程序与其构造函数的初始化顺序的答案。我没有找到它,所以我写了一些代码来检查我的理解。我想我会添加这个小演示作为评论。为了测试您的理解,请在阅读底部之前查看您是否可以预测答案。

/**
 * Demonstrate order of initialization in Java.
 * @author Daniel S. Wilkerson
 */
public class CtorOrder {
  public static void main(String[] args) {
    B a = new B();
  }
}

class A {
  A() {
    System.out.println("A ctor");
  }
}

class B extends A {

  int x = initX();

  int initX() {
    System.out.println("B initX");
    return 1;
  }

  B() {
    super();
    System.out.println("B ctor");
  }

}

输出:

java CtorOrder
A ctor
B initX
B ctor
于 2012-01-30T01:35:59.173 回答
7

静态初始化器相当于静态上下文中的构造器。你肯定会比实例初始化器更频繁地看到它。有时您需要运行代码来设置静态环境。

一般来说,实例初始化器最适合匿名内部类。查看JMock 的说明书,了解使用它使代码更具可读性的创新方法。

有时,如果你有一些在构造函数之间链接起来很复杂的逻辑(比如你是子类化并且你不能调用 this(),因为你需要调用 super()),你可以通过在实例中做常见的事情来避免重复初始化器。然而,实例初始化器是如此罕见,以至于它们对许多人来说是一种令人惊讶的语法,所以我避免使用它们,如果我需要构造函数行为,我宁愿让我的类具体而不是匿名。

JMock 是一个例外,因为这就是框架的用途。

于 2009-04-29T23:31:01.203 回答
6

您在选择时必须考虑一个重要方面:

初始化块是类/对象的成员,而构造函数不是。这在考虑扩展/子类化时很重要:

  1. 初始化器由子类继承。(虽然,可以被遮蔽)
    这意味着基本上可以保证子类按照父类的意图进行初始化。
  2. 但是,构造函数不是继承的。(它们仅隐式调用[即无参数],或者您必须手动super()进行特定调用。) 这意味着隐式或显式调用可能不会按照父类的预期初始化子类。super(...)
    super(...)

考虑这个初始化块的例子:

    class ParentWithInitializer {
        protected String aFieldToInitialize;

        {
            aFieldToInitialize = "init";
            System.out.println("initializing in initializer block of: " 
                + this.getClass().getSimpleName());
        }
    }

    class ChildOfParentWithInitializer extends ParentWithInitializer{
        public static void main(String... args){
            System.out.println(new ChildOfParentWithInitializer().aFieldToInitialize);
        }
    }

输出:

initializing in initializer block of: ChildOfParentWithInitializer
init

-> 无论子类实现什么构造函数,该字段都会被初始化。

现在考虑这个带有构造函数的例子:

    class ParentWithConstructor {
        protected String aFieldToInitialize;

        // different constructors initialize the value differently:
        ParentWithConstructor(){
            //init a null object
            aFieldToInitialize = null;
            System.out.println("Constructor of " 
                + this.getClass().getSimpleName() + " inits to null");
        }

        ParentWithConstructor(String... params) {
            //init all fields to intended values
            aFieldToInitialize = "intended init Value";
            System.out.println("initializing in parameterized constructor of:" 
                + this.getClass().getSimpleName());
        }
    }

    class ChildOfParentWithConstructor extends ParentWithConstructor{
        public static void main (String... args){
            System.out.println(new ChildOfParentWithConstructor().aFieldToInitialize);
        }
    }

输出:

Constructor of ChildOfParentWithConstructor inits to null
null

-> 这将null默认初始化字段,即使它可能不是您想要的结果。

于 2018-05-30T17:27:22.667 回答
4

除了以上所有精彩的答案,我还想补充一点。当我们使用 Class.forName("") 在 JDBC 中加载驱动程序时,会发生类加载,驱动程序类的静态初始化程序被触发,其中的代码将驱动程序注册到驱动程序管理器。这是静态代码块的重要用途之一。

于 2011-06-18T14:29:46.403 回答
3

正如您所提到的,它在很多情况下都没有用,并且与任何较少使用的语法一样,您可能希望避免它只是为了阻止下一个人查看您的代码花费 30 秒将其从保管库中拉出。

另一方面,这是做一些事情的唯一方法(我认为你几乎涵盖了这些)。

无论如何都应该避免静态变量本身——并非总是如此,但如果你使用了很多,或者你在一个类中使用了很多,你可能会找到不同的方法,你未来的自己会感谢你的。

于 2009-04-29T22:44:22.487 回答
0

请注意,执行一些副作用的静态初始化程序的一个大问题是它们不能在单元测试中被模拟。

我见过图书馆这样做,这是一个很大的痛苦。

所以最好只保留那些静态初始化器。

于 2021-07-24T13:41:23.723 回答