Java中初始化程序和构造函数的使用

113

最近我一直在复习Java技能,发现了一些之前不知道的功能,其中包括静态初始化器和实例初始化器。

我的问题是,在什么情况下应该使用初始化器而不是将代码包含在构造函数中?我想到了几个明显的可能性:

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

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

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

然而,虽然这些收益并不微不足道(特别是设置 final 变量的能力),但似乎只有在非常有限的情况下才应该使用初始化器。

当然可以在构造函数中使用初始化器完成大部分工作,但我真的看不出使用初始化器的理由。即使一个类的所有构造函数共享大量代码,使用私有的 initialize() 函数似乎对我来说比使用初始化器更有意义,因为这不会使你在编写新的构造函数时锁定该代码运行。

我错过了什么吗?还有其他许多情况需要使用初始化器吗?还是它只是一个非常有限的工具,只能在非常特定的情况下使用?


2
由于实例初始化程序是一个鲜为人知的功能,因此这里提供一个示例来帮助读者理解:private final int somevar; {somevar = 2;}(请注意,没有构造函数)。更有趣的是,可以搜索“双括号初始化”(语法技巧)。 - Luke Usherwood
10个回答

65

匿名内部类无法拥有构造函数(因为它们是匿名的),所以它们非常适合用于实例初始化。


63
静态初始化器就像cletus提到的一样很有用,我也以同样的方式使用它们。如果您有一个静态变量需要在类加载时初始化,则静态初始化器是最好的选择,特别是因为它允许您进行复杂的初始化并仍然使静态变量成为“final”。这是一个巨大的优势。
我发现“if (someStaticVar == null) // do stuff”会很混乱,也容易出错。如果它被静态地初始化并声明为“final”,那么您就可以避免它可能为“null”的可能性。
但是,当你说:“静态/实例初始化器可以用于设置“final”静态/实例变量的值,而构造函数不能”时,我感到困惑。
我假设你是在说:
- 静态初始化器可以用于设置“final”静态变量的值,而构造函数不能 - 实例初始化器可以用于设置“final”实例变量的值,而构造函数不能
在第一个观点上,您是正确的,但在第二个上则是错误的。例如,您可以这样做:
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;
    }
}

4
是的,这正是我想表达的内容。我一直认为决赛必须在宣布时就确定,而不是只能在之后确定。我现在想想觉得这个想法有点傻,但它确实一直在我脑子里。谢谢你澄清了这一点。 - Inertiatic
我忘记了添加有关链接构造函数的部分,所以我刚刚添加了它。 - Eddie
1
在我看来,实例初始化程序只是被“复制”到构造函数中,因此它们可以执行与构造函数代码相同的操作,它们就是构造函数代码,尽管它们在视觉上是分开的。 - Rostislav Matl

27

我经常使用静态初始化块来设置final static数据,特别是集合。例如:

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")
  );

但是静态版本可能会更整洁,特别是当项目的初始化不太简单时。

一个朴素的实现也可能没有创建一个不可修改的列表,这是一个潜在的错误。上述代码创建了一个不可变数据结构,你可以放心地从公共方法中返回它。


4
我不太喜欢你的具体示例,因为它更适合作为“枚举”实现。 - JAB
23
把“套装”改成“前女友”或其他什么都可以。但你说得对,因为经典卡牌组被改变的可能性几乎为零,所以使用枚举更加合适。 - mike
在这里要注意“不可变”,实际上它是只读的。使用Collections.unmodifiableList(...)包装的原始列表是可以修改的,那么您获得的“视图”也会随之更改。 - Leo Tapia

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"));
                }
            }
        }
    }

不要忘记,现在你需要在类级别上同步,而不是实例级别上。这将导致每个实例构造时都会产生成本,而不是在加载类时只有一次成本。另外,这很丑陋 ;-)


如果在类第一次使用后创建user.prop,它将永远不会被考虑,或者是在编译之后?(对于静态初始化) - Ced

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

正是我正在寻找的例子!!!嗨,丹尼尔,谢谢你的示例。只有一个问题:为什么"A ctor"先运行?我预测应该是"B initX, A ctor, B ctor"。另外,你似乎对这门语言很熟悉,你同意吗? - Cody
构造函数首先运行,因为要创建B,必须先有A。我不确定自己对语言掌握得有多好,因为我的大脑拒绝学习C++和Java,除了已经知道的内容之外,这些语言设计得不太好,所以我不知道的东西常常尝起来像杯底残渣一样苦涩。 - Daniel
5
如果你利用初始化块和静态初始化块扩展这个例子,它会变得更加有用。 - Thomas Weller

8

静态初始化程序相当于静态上下文中的构造函数。您肯定会经常看到它,而不是实例初始化程序。有时候需要运行代码来设置静态环境。

通常,实例化程序最适合匿名内部类。查看JMock's cookbook,以了解一种创新的使用方式,使代码更易读。

有时候,如果您有一些逻辑很难在构造函数中链接(比如您正在子类化,并且不能调用this(),因为您需要调用super()),则可以通过在实例化程序中执行公共任务来避免重复。但是,实例化程序非常罕见,对许多人来说它们是令人惊讶的语法,因此我避免使用它们,而是更喜欢使我的类具体化,如果需要构造函数行为,则不使用匿名类。

JMock是一个例外,因为这就是该框架的预期使用方式。


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,即使这可能不是您想要的结果。


你说的初始化器可以被遮蔽是什么意思? - Aleksandr Dubinsky
子类也可以通过静态块或其他方式更改或初始化父类字段“aFieldToInitialize”。父类静态块仍将被执行,但子类可能会再次覆盖数据。 - Vankog
aFieldToInitialize 是 final 的。它不能被覆盖。 - Aleksandr Dubinsky
虽然我为了简单起见删除了最后一个,但重点仍然相同。;-) Touché. - Vankog
2
你说的仍然没有意义。一个基类应该总是设计它的构造函数来正确地初始化自己,然后你试图发明的问题就不存在了。初始化程序没有任何优势比构造函数。 - Aleksandr Dubinsky
你应该如何使用或不使用它并不是这个问题的一部分。 - Vankog

4

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


3
正如你所提到的,它在很多情况下都没有用处,就像任何不常用的语法一样,你可能想避免使用它,以防止下一个查看你代码的人花费30秒钟将其从存档中取出。
另一方面,它是完成某些事情的唯一方法(我认为你已经涵盖了这些内容)。
静态变量本身应该尽量避免使用 - 不总是这样,但如果你使用了很多静态变量,或者在一个类中使用了很多静态变量,你可能会发现其他方法更好,你未来的自己会感谢你。

0
请注意,具有执行一些副作用的静态初始化程序的一个大问题是,它们无法在单元测试中进行模拟。
我见过一些库这样做,而且这是非常痛苦的。
因此最好仅使这些静态初始化程序保持纯净。

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