【问题标题】:Should I instantiate instance variables on declaration or in the constructor?我应该在声明中还是在构造函数中实例化实例变量?
【发布时间】:2010-12-31 22:35:20
【问题描述】:

这两种方法有什么优势吗?

示例 1:

class A {
    B b = new B();
}

示例 2:

class A {
    B b;

    A() {
         b = new B();
    }
}

【问题讨论】:

    标签: java constructor instance-variables


    【解决方案1】:
        class MyClass extends FooClass {
        String a = null;
    
        public MyClass() {
            super();     // Superclass calls init();
        }
    
        @Override
        protected void init() {
            super.init();
            if (something)
                a = getStringYadaYada();
        }
    }
    

    关于上述,

    String a = null;
    

    null init 可以避免,因为无论如何它是默认值。 但是,如果您需要另一个默认值, 然后,由于不受控制的初始化顺序, 我会修复如下:

    class MyClass extends FooClass 
    {
        String a;
        {
            if( a==null ) a="my custom default value";
        }
        ...
    

    【讨论】:

      【解决方案2】:

      其实很不一样:

      声明发生在构造之前。所以说如果一个人在两个地方都初始化了变量(在这种情况下是 b),构造函数的初始化将替换在类级别完成的初始化。

      所以在类级别声明变量,在构造函数中初始化它们。

      【讨论】:

        【解决方案3】:

        我没有在回复中看到以下内容:

        在声明时进行初始化的一个可能优势可能是现在的 IDE,您可以在其中非常轻松地跳转到变量的声明(主要是 Ctrl-<hover_over_the_variable>-<left_mouse_click>) 来自代码中的任何位置。然后您会立即看到该变量的值。否则,您必须“搜索”完成初始化的位置(主要是:构造函数)。

        这个优势当然次于所有其他逻辑推理,但对于某些人来说,“特征”可能更重要。

        【讨论】:

          【解决方案4】:

          第二个选项更可取,因为它允许在 ctors 中使用不同的逻辑进行类实例化并使用 ctors 链接。例如

          class A {
              int b;
          
              // secondary ctor
              A(String b) {
                   this(Integer.valueOf(b));
              }
          
              // primary ctor
              A(int b) {
                   this.b = b;
              }
          }
          

          所以第二个选项更灵活。

          【讨论】:

            【解决方案5】:

            另一种选择是使用Dependency Injection

            class A{
               B b;
            
               A(B b) {
                  this.b = b;
               }
            }
            

            这消除了从A 的构造函数创建B 对象的责任。从长远来看,这将使您的代码更具可测试性并且更易于维护。这个想法是减少AB这两个类之间的耦合。这给您带来的一个好处是,您现在可以将扩展B(或实现B,如果它是一个接口)的任何对象传递给A 的构造函数,并且它将起作用。一个缺点是您放弃了B 对象的封装,因此它暴露给A 构造函数的调用者。您必须考虑这些好处是否值得这种权衡,但在许多情况下确实如此。

            【讨论】:

            • 另一方面,它增加了耦合,因为现在您已经使AB 之间的链接更加可见。之前B的使用是A内部的事情,如果发现更好的设计不是使用B,你的建议就更难改变了。
            • 耦合仍然存在 - A 需要 B。但是在类中实例化它意味着“A 需要 正是这个 B ”,而 DI 允许许多不同的 B使用。
            • Athis 设计中需要 B now,我的意思是如果这种情况发生变化。
            • @jk:如果您通过使用 DI 和 Factory 类将对象创建与任何地方的业务逻辑分开 - 特别是在创建 A 的地方 - 根本不难改变。它只需要在一个地方进行更改,即创建 A 对象的工厂。如果您对此保持一致,那根本不难掌握。我认为收益大于成本。耦合减少,整体设计更易于测试和维护。
            • @BilltheLizard 你会用这个成语甚至像List<Integer> intList = new ArrayList<>(); 这样简单的东西吗?这可能完全是一个内部实现细节。将 ArrayList 传递给构造函数似乎与良好的封装完全相反。
            【解决方案6】:
            • 没有区别 - 实例变量初始化实际上是由编译器放入构造函数中的。
            • 第一个变体更具可读性。
            • 您不能使用第一个变体进行异常处理。
            • 另外还有一个初始化块,它也是由编译器放入构造函数中的:

              {
                  a = new A();
              }
              

            查看Sun's explanation and advice

            来自this tutorial

            然而,字段声明不是任何方法的一部分,因此它们不能像语句一样执行。相反,Java 编译器会自动生成实例字段初始化代码并将其放入类的构造函数或构造函数中。初始化代码按照它在源代码中出现的顺序插入到构造函数中,这意味着字段初始化器可以使用在它之前声明的字段的初始值。

            此外,您可能希望延迟初始化您的字段。如果初始化字段是一项昂贵的操作,您可以在需要时立即对其进行初始化:

            ExpensiveObject o;
            
            public ExpensiveObject getExpensiveObject() {
                if (o == null) {
                    o = new ExpensiveObject();
                }
                return o;
            }
            

            最终(正如 Bill 所指出的),为了依赖管理,最好避免在类中的任何位置使用new 运算符。相反,使用Dependency Injection 更可取 - 即让其他人(另一个类/框架)实例化并在您的类中注入依赖项。

            【讨论】:

            • @Bozho 对象初始化是在初始化块之前还是之后进入构造函数?
            • 之前,我想。但不确定:)
            • the first variant is more "readable" 这是可以讨论的:如果您在构造函数中初始化所有字段,您就会清楚地知道,当您阅读代码时,您只有一个可以搜索的地方...
            • @Bozho - 你能解释一下为什么你不能对第一个变体进行异常处理吗?
            • “最终(正如 Bill 所指出的),为了依赖管理,最好避免在类中的任何地方使用 new 运算符。相反,使用依赖注入更可取”。至少你说更可取。如果热心地遵循这种鲍勃叔叔主义,可能会导致很多问题(如工厂爆炸)。 new 运算符没有任何问题,也不是所有依赖项都需要注入,特别是如果您对社交测试感到满意。
            【解决方案7】:

            我个人的“规则”(几乎从未被打破)是:

            • 在开头声明所有变量 一个块
            • 将所有变量设为最终变量,除非它们 不能
            • 每行声明一个变量
            • 从不初始化变量 声明
            • 只在一个 当它需要数据时的构造函数 构造函数做 初始化

            所以我会有这样的代码:

            public class X
            {
                public static final int USED_AS_A_CASE_LABEL = 1; // only exception - the compiler makes me
                private static final int A;
                private final int b;
                private int c;
            
                static 
                { 
                    A = 42; 
                }
            
                {
                    b = 7;
                }
            
                public X(final int val)
                {
                    c = val;
                }
            
                public void foo(final boolean f)
                {
                    final int d;
                    final int e;
            
                    d = 7;
            
                    // I will eat my own eyes before using ?: - personal taste.
                    if(f)
                    {
                        e = 1;
                    }
                    else
                    {
                        e = 2;
                    }
                }
            }
            

            这样我总是 100% 确定在哪里查找变量声明(在块的开头),以及它们的赋值(只要在声明之后有意义)。这最终可能会更有效,因为您从不使用未使用的值初始化变量(例如声明和初始化变量,然后在一半需要具有值的变量之前抛出异常)。你也不会做无意义的初始化(比如 int i = 0; 然后稍后,在使用“i”之前,do i = 5;.

            我非常看重一致性,所以我一直都遵循这个“规则”,而且它使代码的工作变得更加容易,因为您不必四处寻找东西。

            您的里程可能会有所不同。

            【讨论】:

            • 这可能是因为“从不初始化声明的变量”(尽管不是我)。或者花括号换行,这被认为是 C/C++ 习语。无论如何,我投了一张赞成票来补偿;)
            • 我宁愿人们出于技术原因投反对票,而不是出于审美原因({ } 展示位置,或它们的非必需使用)。如果人们投反对票,他们至少应该说出他们认为答案有什么问题......技术上没有任何问题,这是我过去 20 年用 C/C++/Java 编码的方式(Java 16)所以我 100% 确定它有效 :-) (感谢反对票 :-)
            • 这就像罪恶一样丑陋,这就是它的问题所在。在使用三元运算符之前你会吃自己的眼睛,但更喜欢多个静态初始化块而不是 OOP 适当的构造函数,这很有趣。你的方式完全打破了依赖注入(从表面上看,是的,编译器基本上通过将所有内容移至构造函数来为你修复它,但是你实际上是在教人们依赖编译器魔法而不是正确的东西),是不可维护的,并将我们带回到 C++ 的糟糕日子。新手读者,请不要这样做。
            • 如果你要包含关于使变量可以是最终的,最终的规则。那么你真的应该包含一些关于使所有可以私有的变量,私有的。
            • @TofuBeer:别担心。大多数 Java 开发人员往往过于迂腐和挑剔。我敢肯定,即使 Joshua Bloch 编写了代码,他们也会选择代码(假设他们不知道是他)。个人品味就是个人品味;最终,CPU 和 JRE 都不关心语法风格。
            【解决方案8】:

            在构造函数之外进行初始化还有一个更微妙的原因,之前没有人提到过(我必须说非常具体)。如果您使用 UML 工具从代码生成类图(逆向工程),我相信大多数工具都会记录示例 1 的初始化并将其转换为图表(如果您希望它显示初始值,例如我愿意)。他们不会从示例 2 中获取这些初始值。同样,这是一个非常具体的原因 - 如果您正在使用 UML 工具,但是一旦我了解到这一点,我就会尝试将我的所有默认值都放在构造函数之外,除非像以前那样前面提到过,存在可能抛出异常或逻辑复杂的问题。

            【讨论】:

              【解决方案9】:

              我今天被一种有趣的方式烫伤了:

              class MyClass extends FooClass {
                  String a = null;
              
                  public MyClass() {
                      super();     // Superclass calls init();
                  }
              
                  @Override
                  protected void init() {
                      super.init();
                      if (something)
                          a = getStringYadaYada();
                  }
              }
              

              看到错误了吗?事实证明,a = null 初始化程序在调用超类构造函数之后被调用。由于超类构造函数调用 init(),a 的初始化之后是a = null 的初始化。

              【讨论】:

              • 这里的教训是永远不要从构造函数中调用可覆盖的函数! :) Effective Java,item 17 有很好的讨论。
              • 好点。通过在声明时进行初始化,您将无法准确控制 何时 变量被初始化。这可以,但你在 a$$ 中(是的,编译器也改变了他们的实现!)。
              • @MohitChugh:确实,像石头一样真实。事实上,如今的现代 Java IDE 像 NetBeans(当然还有其他),如果您从构造函数调用可覆盖的方法,它们会向您发出警告。这就是 Edward Falk 遇到的原因。
              【解决方案10】:

              使用依赖注入延迟初始化总是更可取的,正如在其他答案中已经详细解释的那样。

              当您不想或不能使用这些模式时,对于原始数据类型,我可以想到三个令人信服的理由,为什么最好在构造函数之外初始化类属性:

              1. 避免重复 = 如果您有多个构造函数,或者当您需要添加更多构造函数时,您不必在所有构造函数主体中一遍又一遍地重复初始化;
              2. 提高了可读性 = 您可以一眼就看出哪些变量必须从类外部初始化;
              3. 减少代码行数 = 对于在声明中完成的每个初始化,构造函数中都会减少一行。

              【讨论】:

                【解决方案11】:

                示例 2 不太灵活。如果添加另一个构造函数,则需要记住在该构造函数中实例化字段。只需直接实例化该字段,或在 getter 的某处引入延迟加载。

                如果实例化需要的不仅仅是简单的new,请使用初始化程序块。这将运行无论使用的构造函数。例如

                public class A {
                    private Properties properties;
                
                    {
                        try {
                            properties = new Properties();
                            properties.load(Thread.currentThread().getContextClassLoader().getResourceAsStream("file.properties"));
                        } catch (IOException e) {
                            throw new ConfigurationException("Failed to load properties file.", e); // It's a subclass of RuntimeException.
                        }
                    }
                
                    // ...
                
                }
                

                【讨论】:

                  【解决方案12】:

                  我认为这几乎只是一个品味问题,只要初始化简单且不需要任何逻辑即可。

                  如果您不使用初始化块,构造函数方法会更加脆弱,因为如果您稍后添加第二个构造函数并忘记在那里初始化 b,那么只有在使用最后一个构造函数时您才会得到空 b .

                  请参阅http://java.sun.com/docs/books/tutorial/java/javaOO/initial.html,了解有关 Java 初始化的更多详细信息(以及有关初始化程序块和其他不为人知的初始化功能的说明)。

                  【讨论】:

                  • 这就是为什么你有 DI 和 @Required :)
                  • 是的。我只是在描述 OP 的两个示例之间的差异。
                  • 可能有大量构造函数意味着您违反了单一职责原则,并且您的设计存在更大的问题。
                  【解决方案13】:

                  第二个是惰性初始化的例子。第一个是更简单的初始化,它们本质上是相同的。

                  【讨论】:

                    【解决方案14】:

                    我认为示例 2 更可取。我认为最好的做法是在构造函数之外声明并在构造函数中初始化。

                    【讨论】:

                      【解决方案15】:

                      这两种方法都可以接受。请注意,在后一种情况下,如果存在另一个构造函数,b=new B() 可能不会被初始化。将构造函数之外的初始化代码视为通用构造函数,然后执行代码。

                      【讨论】:

                        猜你喜欢
                        • 1970-01-01
                        • 1970-01-01
                        • 1970-01-01
                        • 1970-01-01
                        • 1970-01-01
                        • 1970-01-01
                        • 2012-10-30
                        • 2017-06-02
                        • 1970-01-01
                        相关资源
                        最近更新 更多