我们应该倾向于使用字段初始化器还是构造器来为字段赋予默认值?
我不会考虑在字段实例化和字段惰性/急切实例化期间可能出现的异常,这些异常会涉及除可读性和可维护性之外的其他问题。
对于执行相同逻辑并产生相同结果的两个代码,应优先选择可读性和可维护性最好的方式。
TL;DR
选择第一个或第二个选项首先是代码组织、可读性和可维护性的问题。
在选择的方式上保持一致性(它使整个应用程序代码更清晰)
不要犹豫,使用字段初始化器来实例化Collection 字段以防止NullPointerException
不要对可能被构造函数覆盖的字段使用字段初始值设定项
在具有单个构造函数的类中,字段初始值设定项的方式通常更具可读性和简洁性
在具有多个构造函数的类中构造函数之间没有耦合或耦合很少,字段初始化方法通常更具可读性和更少冗长
在具有多个构造函数的类中构造函数之间存在耦合,这两种方式都不是更好,但无论选择哪种方式,将其与链式构造函数结合起来就是一种方式(参见用例 1)。
OP 问题
使用非常简单的代码,字段声明期间的赋值似乎更好,确实如此。
这不那么冗长,更直接:
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];
}
}
在具有如此真实特性的真实课程中,情况有所不同。
事实上,根据遇到的具体情况,一种方式,另一种或任何一种方式都应该受到青睐。
更详细的例子来说明
案例一
我将从一个简单的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;
}
}
字段声明中指定的默认值并非全部可靠。
只有 name 和 origin 字段具有真正的默认值。
nbSeat 和 color 字段首先在它们的声明中赋值,然后可以在构造函数中用参数覆盖这些字段。
它很容易出错,并且除了使用这种评估字段的方式外,该类还降低了其可靠性级别。怎么可能
依赖在字段声明期间分配的任何默认值,而事实证明它对于两个字段不可靠?
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.在字段初始化器中为构造函数未分配新值的字段提供默认值更好,但仍然存在重复问题
通过在声明中不重视 nbSeat 和 color 字段,我们可以清楚地区分具有默认值的字段和没有默认值的字段。
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";
}
}
由于我们在声明中不重视name 和origin 字段,并且我们没有一个由其他构造函数自然调用的公共构造函数,因此我们不得不引入initDefaultValues() 方法并在每个构造函数中调用它。
所以我们不要忘记调用这个方法。
请注意,我们可以在无 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() 方法或无参数构造函数来调用。
字段初始化器是完美的。
结论
在任何情况下)不应为所有字段执行字段初始化器中的字段值,而应仅对那些不能被构造函数覆盖的字段执行。
用例 1) 在多个构造函数之间有共同处理的情况下,它主要是基于意见的。
解决方案 2(使用构造函数对所有字段赋值并依赖构造函数链接)和解决方案 4(在字段初始化器中为构造函数未分配新值的字段提供默认值,并且依赖构造函数链接)似乎是最具可读性、可维护性和健壮性的解决方案。
用例 2) 如果多个构造函数之间没有共同的处理/关系,就像在单个构造函数的情况下,解决方案 2(在字段初始化器中为以下字段提供默认值构造函数不会为它们分配新值)看起来更好。