缺省构造函数与内联字段初始化

缺省构造函数和直接初始化对象的字段有什么区别?

有什么原因让我们更喜欢下面的例子呢?

例子一

public class Foo
{
private int x = 5;
private String[] y = new String[10];
}

例子2

public class Foo
{
private int x;
private String[] y;


public Foo()
{
x = 5;
y = new String[10];
}
}
58169 次浏览

选择示例一的原因是,它对于较少的代码具有相同的功能(这总是好的)。

除此之外,没有区别。

但是,如果您确实有显式的构造函数,我更愿意将所有初始化代码放到这些构造函数中(并将它们链接起来) ,而不是在构造函数和字段初始化器之间进行拆分。

初始化程序在构造函数体之前执行。(如果您同时拥有初始化器和构造函数,那么构造函数代码会执行第二个并覆盖一个初始化的值)

如果你总是需要相同的初始值(比如在你的例子中,给定大小的数组,或者特定值的整数) ,那么初始化器是很好的选择,但是它可能对你有利,也可能对你不利:

如果有许多构造函数以不同的方式初始化变量(例如,使用不同的值) ,那么初始化程序就没有用处,因为更改将被重写,并且是浪费的。

另一方面,如果有许多构造函数使用相同的值进行初始化,那么可以通过将初始化保持在一个位置来节省代码行(并使代码稍微更易于维护)。

正如 Michael 所说,这也涉及到品味问题——您可能希望将代码保存在一个地方。尽管如果你有很多构造函数,你的代码在任何情况下都不在一个地方,所以我更喜欢初始化程序。

我能想到的唯一区别是,如果要添加另一个构造函数

public Foo(int inX){
x = inX;
}

然后在第一个例子中,你将不再有一个缺省构造函数,而在第二个例子中,你仍然有缺省构造函数(甚至可以从我们的新构造函数中调用它,如果我们想的话)

当需要执行复杂的初始化逻辑时,我更喜欢使用字段初始化器和缺省构造函数(例如,填充一个 map,通过一系列启发式步骤执行一个 ivar 依赖于另一个 ivar,等等)。

@ Michael B 说:

... 我更愿意将所有的初始化代码放到这些代码中(并将它们链接起来) ,而不是在构造函数和字段初始化器之间进行拆分。

MichaelB (我向71 + K rep 致敬)非常有意义,但我倾向于在内联的最终初始化器中保留简单的初始化,并在构造函数中完成复杂的初始化部分。

我们应该支持字段初始值设置器还是构造函数为字段提供默认值?

我不会考虑在字段实例化和字段懒惰/渴望实例化期间可能出现的异常,这些异常涉及可读性和可维护性以外的其他问题。
对于执行相同逻辑并产生相同结果的两个代码,应该采用具有最佳可读性和可维护性的方法。

DR

  • 选择第一个或第二个选项首先是代码 组织可读性可维护性的问题。

  • 保持选择方式的一致性(这使得整个应用程序代码更加清晰)

  • 不要犹豫使用字段初始化器来实例化 Collection字段,以防止 NullPointerException

  • 不要对可能被构造函数覆盖的字段使用字段初始化器

  • 在具有单个构造函数的类中,字段初始值设定项方式通常更具可读性,而且不那么冗长

  • 在具有多个构造函数 构造函数之间没有耦合或耦合很少的类中,字段初始值设定项方法通常更具可读性,而且不那么冗长

  • 在具有多个构造函数 其中构造函数之间有耦合的类中,这两种方法都不是真正更好的方法,但无论选择哪种方法,将其与链接构造函数组合在一起都是一种方法(参见用例1)。


问题

对于一个非常简单的代码,字段声明期间的赋值看起来更好,事实也的确如此。

这句话没有那么冗长,而是更加直白:

public class Foo {
private int x = 5;
private String[] y = new String[10];
}

而不是构造函数的方式:

public class Foo{
private int x;
private String[] y;


public Foo(){
x = 5;
y = new String[10];
}
}

在具有如此真实特性的真实类中,情况是不同的。
事实上,根据所遇到的具体情况,一种方式,另一种或者任何一种都应该受到青睐。


更详细的例子来说明

研究个案1

我将从一个简单的 Car类开始,我将更新它来说明这些要点。
Car声明了4个字段和它们之间的一些构造函数。

1. 在所有字段的字段初始化器中给出一个默认值是不可取的

public class Car {


private String name = "Super car";
private String origin = "Mars";
private int nbSeat = 5;
private Color color = Color.black;
...


...
// Other fields


...


public Car() {
}


public Car(int nbSeat) {
this.nbSeat = nbSeat;
}


public Car(int nbSeat, Color color) {
this.nbSeat = nbSeat;
this.color = color;
}


}

字段声明中指定的默认值并不都是可靠的。 只有 nameorigin字段有真正的默认值。 < br >

nbSeatcolor字段首先在它们的声明中赋值,然后这些字段可能在带参数的构造函数中被覆盖。
它很容易出错,而且除了使用这种赋值字段的方法之外,该类还降低了它的可靠性级别 依赖于在字段声明期间分配的任何默认值,而事实证明它对两个字段不可靠?

2. 使用构造函数对所有字段进行估值,并依靠构造函数链接是可行的

public class Car {


private String name;
private String origin;
private int nbSeat;
private Color color;
...


...
// Other fields


...


public Car() {
this(5, Color.black);
}


public Car(int nbSeat) {
this(nbSeat, Color.black);
}


public Car(int nbSeat, Color color) {
this.name = "Super car";
this.origin = "Mars";
this.nbSeat = nbSeat;
this.color = color;
}


}

这个解决方案非常好,因为它不会创建复制,它将所有逻辑集中在一个地方: 具有最大参数数量的构造函数。
它有一个缺点: 需要将调用链接到另一个构造函数。
但这是一个缺点吗?

3.在字段初始化器中为构造函数没有赋值的字段提供一个默认值会更好,但是仍然存在重复问题

通过在声明中不赋值 nbSeatcolor字段,我们可以清楚地区分具有默认值的字段和不具有默认值的字段。

public class Car {


private String name = "Super car";
private String origin = "Mars";
private int nbSeat;
private Color color;
...


...
// Other fields


...


public Car() {
nbSeat = 5;
color = Color.black;
}


public Car(int nbSeat) {
this.nbSeat = nbSeat;
color = Color.black;
}


public Car(int nbSeat, Color color) {
this.nbSeat = nbSeat;
this.color = color;
}


}

这个解决方案相当不错,但是它在每个 Car构造函数中重复实例化逻辑,这与之前使用构造函数链接的解决方案相反。

在这个简单的示例中,我们可以开始理解复制问题,但它似乎只有一点点恼人。
在实际情况中,重复可能非常重要,因为构造函数可以执行计算和验证。
让一个构造函数执行实例化逻辑非常有帮助。

因此最后,字段声明中的赋值并不总是将构造函数委托给另一个构造函数。

这是一个改进版本。

4.在字段初始化器中为构造函数没有赋予它们新值的字段提供一个默认值,并依赖于构造函数链接是可行的

public class Car {


private String name = "Super car";
private String origin = "Mars";
private int nbSeat;
private Color color;
...


...
// Other fields


...


public Car() {
this(5, Color.black);
}


public Car(int nbSeat) {
this(nbSeat, Color.black);
}


public Car(int nbSeat, Color color) {
// assignment at a single place
this.nbSeat = nbSeat;
this.color = color;
// validation rules at a single place
...
}


}

研究个案2

我们将修改原来的 Car类。
现在,Car声明了5个字段和3个构造函数,它们之间没有关系

1. 使用构造函数为具有默认值的字段赋值是不可取的

public class Car {


private String name;
private String origin;
private int nbSeat;
private Color color;
private Car replacingCar;
...


...
// Other fields


...


public Car() {
initDefaultValues();
}


public Car(int nbSeat, Color color) {
initDefaultValues();
this.nbSeat = nbSeat;
this.color = color;
}


public Car(Car replacingCar) {
initDefaultValues();
this.replacingCar = replacingCar;
// specific validation rules
}


private void initDefaultValues() {
name = "Super car";
origin = "Mars";
}


}

因为我们没有在它们的声明中赋值 nameorigin字段,也没有其他构造函数自然调用的公共构造函数,所以我们不得不引入一个 initDefaultValues()方法并在每个构造函数中调用它。 所以我们不要忘记调用这个方法。
注意,我们可以在 no arg 构造函数中内联 initDefaultValues()主体,但是从其他构造函数中调用不带 arg 的 this()是不必要的,而且很容易忘记:

public class Car {


private String name;
private String origin;
private int nbSeat;
private Color color;
private Car replacingCar;
...


...
// Other fields


...


public Car() {
name = "Super car";
origin = "Mars";
}


public Car(int nbSeat, Color color) {
this();
this.nbSeat = nbSeat;
this.color = color;
}


public Car(Car replacingCar) {
this();
this.replacingCar = replacingCar;
// specific validation rules
}


}

2. 在字段初始值设置器中为构造函数没有赋值的字段设置一个默认值

public class Car {


private String name = "Super car";
private String origin = "Mars";
private int nbSeat;
private Color color;
private Car replacingCar;
...


...
// Other fields


...


public Car() {
}


public Car(int nbSeat, Color color) {
this.nbSeat = nbSeat;
this.color = color;
}


public Car(Car replacingCar) {
this.replacingCar = replacingCar;
// specific validation rules
}


}

这里我们不需要调用 initDefaultValues()方法或 no arg 构造函数。 字段初始化器很完美。


结论

在任何情况下) 不应该对所有字段执行字段初始化器中的字段赋值,而应该只对那些不能被构造函数覆盖的字段执行。

用例1) 对于具有共同处理的多个构造函数,它主要是基于意见的。
解决方案2(使用构造函数对所有字段进行值并依赖构造函数链接)和解决方案4(在字段初始化器中为构造函数不赋值的字段提供默认值,并依赖于构造函数链接)是可读性、可维护性和可靠性最强的解决方案。

用例2) 在多个构造函数之间没有公共处理/关系的情况下,比如在单个构造函数的情况下,解决方案2(在字段初始化器中为构造函数不赋值的字段提供默认值)看起来更好。