Java 中初始化器与构造函数的使用

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

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

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

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

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

然而,尽管这些收益不是微不足道的(特别是设置最终变量的能力) ,但是似乎应该使用初始化器的情况有限。

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

我错过了什么吗?在其他情况下是否应该使用初始化程序?或者它真的只是一个非常有限的工具,用于非常具体的情况?

73243 次浏览

As you mentioned, it's not useful in a lot of cases and as with any less-used syntax, you probably want to avoid it just to stop the next person looking at your code from spending the 30 seconds to pull it out of the vaults.

On the other hand, it is the only way to do a few things (I think you pretty much covered those).

Static variables themselves should be somewhat avoided anyway--not always, but if you use a lot of them, or you use a lot in one class, you might find different approaches, your future self will thank you.

I most often use static initializer blocks for setting up final static data, especially collections. For example:

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


...
}

Now this example can be done with a single line of code:

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

but the static version can be far neater, particularly when the items are non-trivial to initialize.

A naive implementation may also not create an unmodifiable list, which is a potential mistake. The above creates an immutable data structure that you can happily return from public methods and so on.

Anonymous inner classes can't have a constructor (as they're anonymous), so they're a pretty natural fit for instance initializers.

Static initializers are useful as cletus mentioned and I use them in the same manner. If you have a static variable that is to be initialized when the class is loaded, then a static initializer is the way to go, especially as it allows you to do a complex initialization and still have the static variable be final. This is a big win.

I find "if (someStaticVar == null) // do stuff" to be messy and error prone. If it is initialized statically and declared final, then you avoid the possibility of it being null.

However, I'm confused when you say:

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

I assume you are saying both:

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

and you are correct on the first point, wrong on the second. You can, for example, do this:

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

Also, when a lot of code is shared between constructors, one of the best ways to handle this is to chain constructors, providing the default values. This makes is pretty clear what is being done:

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

A static initializer is the equivalent of a constructor in the static context. You will certainly see that more often than an instance initializer. Sometimes you need to run code to set up the static environment.

In general, an instance initalizer is best for anonymous inner classes. Take a look at JMock's cookbook to see an innovative way to use it to make code more readable.

Sometimes, if you have some logic which is complicated to chain across constructors (say you are subclassing and you can't call this() because you need to call super()), you could avoid duplication by doing the common stuff in the instance initalizer. Instance initalizers are so rare, though, that they are a surprising syntax to many, so I avoid them and would rather make my class concrete and not anonymous if I need the constructor behavior.

JMock is an exception, because that is how the framework is intended to be used.

Just to add to some already excellent points here. The static initializer is thread safe. It is executed when the class is loaded, and thus makes for simpler static data initialization than using a constructor, in which you would need a synchronized block to check if the static data is initialized and then actually initialize it.

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

versus

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

Don't forget, you now have to synchronize at the class, not instance level. This incurs a cost for every instance constructed instead of a one time cost when the class is loaded. Plus, it's ugly ;-)

I would also like to add one point along with all the above fabulous answers . When we load a driver in JDBC using Class.forName("") the the Class loading happens and the static initializer of the Driver class gets fired and the code inside it registers Driver to Driver Manager. This is one of the significant use of static code block.

I read a whole article looking for an answer to the init order of initializers vs. their constructors. I didn't find it, so I wrote some code to check my understanding. I thought I would add this little demonstration as a comment. To test your understanding, see if you can predict the answer before reading it at the bottom.

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


}

Output:

java CtorOrder
A ctor
B initX
B ctor

There is one important aspect that you have to consider in your choice:

Initializer blocks are members of the class/object, while constructors are not. This is important when considering extension/subclassing:

  1. Initializers are inherited by subclasses. (Though, can be shadowed)
    This means it is basically guaranteed that subclasses are initialized as intended by the parent class.
  2. Constructors are not inherited, though. (They only call super() [i.e. no parameters] implicitly or you have to make a specific super(...) call manually.)
    This means it is possible that a implicit or exclicit super(...) call might not initialize the subclass as intended by the parent class.

Consider this example of an initializer block:

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

output:

initializing in initializer block of: ChildOfParentWithInitializer
init

-> No matter what constructors the subclass implements, the field will be initialized.

Now consider this example with constructors:

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

output:

Constructor of ChildOfParentWithConstructor inits to null
null

-> This will initialize the field to null by default, even though it might not be the result you wanted.

Note that one big issue with static initializers that perform some side effects, is that they cannot be mocked in unit tests.

I've seen libraries do that, and it's a big pain.

So it's best to keep those static initializers pure only.