【问题标题】:Default constructor vs. inline field initialization默认构造函数与内联字段初始化
【发布时间】:2011-06-22 10:49:16
【问题描述】:

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

有什么理由比另一个更喜欢以下示例?

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

【问题讨论】:

  • 根据this answer,您的示例 1 实际上已转换为您的示例 2。

标签: java oop constructor


【解决方案1】:

更喜欢示例一的原因是它具有相同的功能,但代码更少(这总是好的)。

除此之外,没有区别。

但是,如果您确实有显式构造函数,我更愿意将所有初始化代码放入其中(并将它们链接起来),而不是在构造函数和字段初始值设定项之间拆分。

【讨论】:

    【解决方案2】:

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

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

    如果您有许多以不同方式初始化变量的构造函数(即使用不同的值),那么初始化器将毫无用处,因为这些更改将被覆盖且浪费。

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

    就像 Michael 所说的那样,这也涉及到品味问题 - 您可能希望将代码保存在一个地方。虽然如果你有很多构造函数,你的代码在任何情况下都不在一个地方,所以我更喜欢初始化器。

    【讨论】:

      【解决方案3】:

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

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

      那么在第一个示例中,您将不再拥有默认构造函数,而在第二个示例中,您仍然拥有默认构造函数(如果我们愿意,甚至可以从我们的新构造函数中调用它)

      【讨论】:

        【解决方案4】:

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

        @Michael B 说:

        ...我更愿意将所有初始化代码放入其中(并将它们链接起来),而不是在构造函数和字段初始值设定项之间拆分。

        MichaelB(我向 71+ K 代表鞠躬)非常有道理,但我倾向于将简单的初始化保留在内联最终初始化程序中,并在构造函数中完成初始化的复杂部分。

        【讨论】:

          【解决方案5】:

          我们应该倾向于使用字段初始化器还是构造器来为字段赋予默认值?

          我不会考虑在字段实例化和字段惰性/急切实例化期间可能出现的异常,这些异常会涉及除可读性和可维护性之外的其他问题。
          对于执行相同逻辑并产生相同结果的两个代码,应优先选择可读性和可维护性最好的方式。

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

          字段声明中指定的默认值并非全部可靠。 只有 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 构造函数中重复了实例化逻辑,这与之前使用构造函数链接的解决方案相反。

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

          因此,最终在字段声明中的赋值不会总是让构造函数委托给另一个构造函数。

          这是一个改进的版本。

          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() 方法并在每个构造函数中调用它。 所以我们不要忘记调用这个方法。
          请注意,我们可以在无 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(在字段初始化器中为以下字段提供默认值构造函数不会为它们分配新值)看起来更好。

          【讨论】:

            猜你喜欢
            • 2020-01-18
            • 1970-01-01
            • 1970-01-01
            • 2011-01-25
            • 2011-06-17
            • 2012-04-11
            • 2018-04-23
            相关资源
            最近更新 更多