在Java中使用Initializers与Constructors

Use of Initializers vs Constructors in Java

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

我的问题是,什么时候使用初始值设定项而不是将代码包含在构造函数中?我想到了几个明显的可能性:

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

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

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

然而,虽然这些并不是微不足道的收获(尤其是设置最终变量的能力),但似乎在一定数量的情况下应该使用初始值设定项。

对于在构造函数中所做的许多事情,当然可以使用初始值设定项,但我真的看不到这样做的原因。即使一个类的所有构造函数都共享大量代码,对我来说,使用私有的initialize()函数似乎比使用初始值设定项更有意义,因为它不会使您在编写新的构造函数时锁定代码的运行。

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


静态初始值设定项和cletus提到的一样有用,我以同样的方式使用它们。如果您有一个静态变量要在加载类时初始化,那么静态初始值设定项就是方法,特别是它允许您进行复杂的初始化,并且静态变量仍然是final。这是一个巨大的胜利。

我发现"if(somestaticvar==null)//do-things"杂乱无章,容易出错。如果它是静态初始化并声明为final,那么就避免了它是null的可能性。

但是,当你说:

static/instance initializers can be used to set the value of"final"
static/instance variables whereas a constructor cannot

我想你说的都是:

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

你第一点是对的,第二点是错的。例如,您可以执行以下操作:

1
2
3
4
5
6
class MyClass {
    private final int counter;
    public MyClass(final int counter) {
        this.counter = counter;
    }
}

此外,当许多代码在构造函数之间共享时,处理这一问题的最佳方法之一是链接构造函数,提供默认值。这就清楚地说明了正在做的工作:

1
2
3
4
5
6
7
8
9
class MyClass {
    private final int counter;
    public MyClass() {
        this(0);
    }
    public MyClass(final int counter) {
        this.counter = counter;
    }
}


匿名内部类不能有构造函数(因为它们是匿名的),因此对于实例初始值设定项来说,它们是非常自然的。


我经常使用静态初始值设定项块来设置最终静态数据,尤其是集合。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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);
  }

  ...
}

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

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

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

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


只是为了增加一些已经很好的观点。静态初始值设定项是线程安全的。它是在加载类时执行的,因此与使用构造函数相比,静态数据初始化更简单,在该构造函数中,您需要一个同步块来检查静态数据是否已初始化,然后实际对其进行初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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"));
        }
    }

对战

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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"));
                }
            }
        }
    }

别忘了,现在必须在类而不是实例级别进行同步。当类被加载时,这会为每个构建的实例带来成本,而不是一次性成本。另外,它很难看;—)


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
 * 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");
  }

}

输出:

1
2
3
4
java CtorOrder
A ctor
B initX
B ctor


静态初始值设定项是静态上下文中构造函数的等效项。您肯定会比实例初始值设定项更经常看到这一点。有时需要运行代码来设置静态环境。

通常,实例初始化器最适合匿名内部类。请看一下JMock的食谱,以了解一种创新的方法来使用它来提高代码的可读性。

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

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


在您的选择中,有一个重要方面需要考虑:

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

  • 初始值设定项由子类继承。(不过,可以隐藏)这意味着基本上可以保证子类按照父类的预期进行初始化。
  • 不过,构造函数不是继承的。(它们只隐式地调用super()【即没有参数】,或者您必须手动进行特定的super(...)调用。)这意味着隐式或互斥的super(...)调用可能无法按照父类的预期初始化子类。
  • 考虑初始化器块的这个例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class ParentWithInitializer {
        protected final 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);
        }
    }

    输出:埃多克斯1〔3〕->无论子类实现什么构造函数,都将初始化字段。

    现在用构造函数来考虑这个例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    class ParentWithConstructor {
        protected final 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);
        }
    }

    输出:江户十一〔四〕号->默认情况下,这会将字段初始化为null,即使它可能不是您想要的结果。


    我还想加上一点,连同上面所有精彩的答案。当我们使用class.forname(")在JDBC中加载驱动程序时,类将被加载,驱动程序类的静态初始值设定项将被激发,其中的代码将驱动程序注册到驱动程序管理器。这是静态代码块的重要用途之一。


    正如您所提到的,它在很多情况下都不有用,而且对于任何不太常用的语法,您可能希望避免使用它,只是为了阻止下一个查看您的代码的人花费30秒的时间将其从保险库中拉出。

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

    不管怎样,静态变量本身应该有所避免——不总是如此,但是如果你使用了很多静态变量,或者你在一个类中使用了很多静态变量,你可能会发现不同的方法,你未来的自我会感谢你。