默认构造函数与内联字段初始化

时间:2011-02-06 22:45:45

标签: java oop constructor

默认构造函数与直接初始化对象的字段之间有什么区别?

有哪些理由更喜欢以下示例中的一个而不是另一个?

示例1

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

5 个答案:

答案 0 :(得分:64)

初始化器在构造函数体之前执行。 (如果您同时拥有初始化器和构造函数,构造函数代码执行第二个并覆盖初始值,则会产生影响)

当您始终需要相同的初始值时(例如,在您的示例中,给定大小的数组或特定值的整数),初始化器很好,但它可以对您有利或对您不利:

如果你有许多构造函数以不同的方式初始化变量(即具有不同的值),那么初始化程序是无用的,因为更改将被覆盖并且浪费。

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

像迈克尔说的那样,也有一些品味问题 - 您可能希望将代码保存在一个地方。虽然如果你有很多构造函数,你的代码在任何情况下都不在一个地方,所以我倾向于初始化者。

答案 1 :(得分:20)

更喜欢第一个例子的原因是它对于更少的代码(总是好的)具有相同的功能。

除此之外,没有区别。

但是,如果你有明确的构造函数,我宁愿将所有初始化代码放入那些(并链接它们),而不是在构造函数和字段初始化器之间拆分。

答案 2 :(得分:6)

我们是否应该使用字段初始值设定项或构造函数为字段提供默认值?

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

<强>

<强> 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];
    }
}

在具有如此真实特征的真实班级中,事情是不同的。
事实上,根据遇到的具体情况,一种方式,其中一个或任何一个应该受到青睐。

更详细的例子来说明

研究案例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字段才有真正的默认值。

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构造函数中的实例化逻辑,这与前面的构造函数链接解决方案相反。

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

所以最后在fields声明中的赋值并不总是使构造函数不能委托给另一个构造函数。

这是一个改进版本。

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() body,但是调用this()而不使用其他构造函数的arg并不是必然的,可能很容易被遗忘:

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()方法或无需arg构造函数来调用。 现场初始化器是完美的。

<强>结论

在任何情况下)不应对所有字段执行字段初始值设定项中的字段,而只应对那些无法被构造函数覆盖的字段执行。

用例1)如果多个构造函数之间有共同处理,则主要基于意见。
解决方案2(使用构造函数来评估所有字段并依赖于构造函数链接)和解决方案4(在字段初始化器中为构造函数不为其分配新字段的字段提供默认值价值和依赖于构造函数链接)作为最具可读性,可维护性和可靠性的解决方案。

用例2)如果多个构造函数之间没有共同的处理/关系,就像在单个构造函数的情况下一样,解决方案2(在字段的inureizers中给出一个默认值施工人员不会给他们分配新值看起来更好。

答案 3 :(得分:3)

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

@Michael B说:

  

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

MichaelB(我向71 + K代表低头)很有道理,但我的倾向是将简单的初始化保留在内联的最终初始化器中,并在构造函数中执行初始化的复杂部分。

答案 4 :(得分:2)

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

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

然后在第一个例子中,你将不再有一个默认的构造函数,而在第二个例子中,你仍然会有默认的构造函数(如果我们想要,甚至可以从我们的新构造函数中调用它)< / p>