【问题标题】:Use of Initializers vs Constructors in JavaJava 中初始化器与构造器的使用
【发布时间】:2010-10-22 17:16:52
【问题描述】:

所以我最近一直在复习我的 Java 技能,并且发现了一些我以前不知道的功能。静态和实例初始化器就是这样的两种技术。

我的问题是什么时候会使用初始化程序而不是在构造函数中包含代码?我想到了几个明显的可能性:

  • 静态/实例初始化器可用于设置“最终”静态/实例变量的值,而构造函数则不能

  • 静态初始化器可用于设置类中任何静态变量的值,这应该比在开头使用“if (someStaticVar == null) // do stuff”代码块更有效每个构造函数的

这两种情况都假定设置这些变量所需的代码比简单的“var = value”更复杂,否则似乎没有任何理由使用初始化程序而不是在声明时简单地设置值变量。

然而,虽然这些并不是微不足道的收获(尤其是设置最终变量的能力),但似乎确实有相当数量的情况需要使用初始化程序。

在构造函数中完成的很多事情当然可以使用初始化程序,但我真的不明白这样做的原因。即使一个类的所有构造函数共享大量代码,对我来说,使用私有 initialize() 函数似乎比使用初始化器更有意义,因为它不会锁定你在编写新代码时运行该代码构造函数。

我错过了什么吗?是否还有许多其他情况需要使用初始化程序?或者它真的只是一个在非常特定的情况下使用的相当有限的工具?

【问题讨论】:

  • 由于实例初始化器是一个鲜为人知的特性,这里有一个例子来帮助读者:private final int somevar; {somevar = 2;}(注意,没有构造器。)为了更有趣,搜索“双括号初始化”(语法黑客)。

标签: java constructor initializer static-initializer initialization-block


【解决方案1】:

正如您所提到的,它在很多情况下都没有用,并且与任何较少使用的语法一样,您可能希望避免它只是为了阻止下一个查看您的代码的人花费 30 秒将其拉出保险库。

另一方面,它是做一些事情的唯一方法(我认为你几乎涵盖了这些)。

无论如何都应该避免静态变量本身——并非总是如此,但如果你使用了很多,或者你在一个课程中使用了很多,你可能会找到不同的方法,你未来的自己会感谢你的。

【讨论】:

    【解决方案2】:

    我最常使用静态初始化程序块来设置最终的静态数据,尤其是集合。例如:

    public class Deck {
      private final static List<String> SUITS;
    
      static {
        List<String> list = new ArrayList<String>();
        list.add("Clubs");
        list.add("Spades");
        list.add("Hearts");
        list.add("Diamonds");
        SUITS = Collections.unmodifiableList(list);
      }
    
      ...
    }
    

    现在这个例子可以用一行代码完成:

    private final static List<String> SUITS =
      Collections.unmodifiableList(
        Arrays.asList("Clubs", "Spades", "Hearts", "Diamonds")
      );
    

    但静态版本可以更整洁,特别是当项目初始化时。

    幼稚的实现也可能不会创建不可修改的列表,这是一个潜在的错误。以上创建了一个不可变的数据结构,您可以愉快地从公共方法等返回。

    【讨论】:

    • 我不太喜欢您的具体示例,因为它更适合作为enum 实现。
    • 然后将 SUITS 更改为 PAST_GIRLFRIENDS 或其他。但你是对的,因为经典卡组被改变的可能性大约为零,枚举会更...适合。
    【解决方案3】:

    匿名内部类不能有构造函数(因为它们是匿名的),所以它们非常适合实例初始化器。

    【讨论】:

      【解决方案4】:

      静态初始化器就像提到的 cletus 一样有用,我以同样的方式使用它们。如果您有一个要在加载类时初始化的静态变量,那么静态初始化程序是可行的方法,特别是因为它允许您进行复杂的初始化并且仍然让静态变量为final。这是一个巨大的胜利。

      我发现“if (someStaticVar == null) // do stuff”很混乱而且容易出错。如果它是静态初始化并声明为final,那么你就避免了它是null的可能性。

      但是,当你说:

      静态/实例初始化器可用于设置“final”的值 静态/实例变量,而构造函数不能

      我假设你说的是两个:

      • 静态初始值设定项可用于设置“最终”静态变量的值,而构造函数则不能
      • 实例初始值设定项可用于设置“最终”实例变量的值,而构造函数则不能

      你在第一点上是正确的,在第二点上是错误的。例如,您可以这样做:

      class MyClass {
          private final int counter;
          public MyClass(final int counter) {
              this.counter = counter;
          }
      }
      

      此外,当构造函数之间共享大量代码时,处理此问题的最佳方法之一是链接构造函数,提供默认值。这很清楚正在做什么:

      class MyClass {
          private final int counter;
          public MyClass() {
              this(0);
          }
          public MyClass(final int counter) {
              this.counter = counter;
          }
      }
      

      【讨论】:

      • 这就是我所说的。我的想法是,决赛必须在宣布时设置,而不是只能设置一次。当我想到它时,这有点愚蠢,但它仍然在我的脑海中。感谢您澄清这一点。
      • 我忘了添加关于链接构造函数的部分,所以我只是添加了它。
      • 恕我直言,实例初始化程序只是“复制”到构造函数中,因此它们可以与构造函数代码执行相同的操作,它们是构造函数代码,尽管它们在视觉上是分开的。
      【解决方案5】:

      静态初始化器相当于静态上下文中的构造器。你肯定会比实例初始化器更频繁地看到它。有时您需要运行代码来设置静态环境。

      一般来说,实例初始化器最适合匿名内部类。查看JMock's cookbook,了解使用它使代码更具可读性的创新方法。

      有时,如果你有一些复杂的逻辑在构造函数之间进行链接(比如你是子类化并且你不能调用 this(),因为你需要调用 super()),你可以通过做常见的事情来避免重复在实例初始化器中。然而,实例初始化器是如此罕见,以至于它们对许多人来说是一种令人惊讶的语法,所以我避免使用它们,如果我需要构造函数行为,我宁愿让我的类具体而不是匿名。

      JMock 是一个例外,因为这就是框架的用途。

      【讨论】:

        【解决方案6】:

        只是在这里添加一些已经很出色的观点。静态初始化器是线程安全的。它在类加载时执行,因此与使用构造函数相比,静态数据初始化更简单,在构造函数中,您需要一个同步块来检查静态数据是否已初始化,然后实际初始化它。

        public class MyClass {
        
            static private Properties propTable;
        
            static
            {
                try 
                {
                    propTable.load(new FileInputStream("/data/user.prop"));
                } 
                catch (Exception e) 
                {
                    propTable.put("user", System.getProperty("user"));
                    propTable.put("password", System.getProperty("password"));
                }
            }
        

        public class MyClass 
        {
            public MyClass()
            {
                synchronized (MyClass.class) 
                {
                    if (propTable == null)
                    {
                        try 
                        {
                            propTable.load(new FileInputStream("/data/user.prop"));
                        } 
                        catch (Exception e) 
                        {
                            propTable.put("user", System.getProperty("user"));
                            propTable.put("password", System.getProperty("password"));
                        }
                    }
                }
            }
        

        不要忘记,您现在必须在类而不是实例级别进行同步。这会为每个构造的实例带来成本,而不是加载类时的一次性成本。另外,它很丑;-)

        【讨论】:

        • 所以如果 user.prop 是在第一次使用该类之后创建的,它将永远不会被考虑,还是在编译之后? (用于静态初始化)
        【解决方案7】:

        除了以上所有精彩的答案之外,我还想补充一点。当我们使用 Class.forName("") 在 JDBC 中加载驱动程序时,会发生类加载,驱动程序类的静态初始化程序被触发,其中的代码将驱动程序注册到驱动程序管理器。这是静态代码块的重要用途之一。

        【讨论】:

          【解决方案8】:

          我阅读了整篇文章来寻找初始化程序与构造函数的初始化顺序的答案。我没有找到它,所以我写了一些代码来检查我的理解。我想我会添加这个小演示作为评论。为了测试你的理解,看看你是否能在阅读底部之前预测答案。

          /**
           * Demonstrate order of initialization in Java.
           * @author Daniel S. Wilkerson
           */
          public class CtorOrder {
            public static void main(String[] args) {
              B a = new B();
            }
          }
          
          class A {
            A() {
              System.out.println("A ctor");
            }
          }
          
          class B extends A {
          
            int x = initX();
          
            int initX() {
              System.out.println("B initX");
              return 1;
            }
          
            B() {
              super();
              System.out.println("B ctor");
            }
          
          }
          

          输出:

          java CtorOrder
          A ctor
          B initX
          B ctor
          

          【讨论】:

          • 正是我要找的例子!!!嗨丹尼尔,谢谢前任。只是有一个问题:为什么“演员”首先运行?我预测“B initX,A ctor,B ctor”。另外,你似乎很懂这门语言,你同意吗?
          • A ctor 首先运行,因为要制作 B,您首先必须有 A。不确定我对这门语言的了解程度,因为我的大脑拒绝学习 C++ 和 Java 比它已经知道的更好因为这些语言设计得不是很好,所以我不知道的东西通常尝起来就像玻璃杯底部的残渣:苦涩。
          • 如果把这个例子扩展为初始化块和静态初始化块,会更有用。
          【解决方案9】:

          您在选择时必须考虑一个重要方面:

          初始化块是类/对象的成员,而构造函数不是。 这在考虑扩展/子类化时很重要:

          1. 初始化器由子类继承。 (虽然,可以阴影)
            这意味着基本上可以保证子类按照父类的意图进行初始化。
          2. 构造函数是继承的。 (他们只隐式调用super() [即无参数],或者您必须手动进行特定的super(...) 调用。)
            这意味着隐式或显式 super(...) 调用可能不会按照父类的预期初始化子类。

          考虑这个初始化块的例子:

              class ParentWithInitializer {
                  protected String aFieldToInitialize;
          
                  {
                      aFieldToInitialize = "init";
                      System.out.println("initializing in initializer block of: " 
                          + this.getClass().getSimpleName());
                  }
              }
          
              class ChildOfParentWithInitializer extends ParentWithInitializer{
                  public static void main(String... args){
                      System.out.println(new ChildOfParentWithInitializer().aFieldToInitialize);
                  }
              }
          

          输出:

          initializing in initializer block of: ChildOfParentWithInitializer
          init
          

          ->无论子类实现什么构造函数,该字段都会被初始化。

          现在考虑这个带有构造函数的例子:

              class ParentWithConstructor {
                  protected String aFieldToInitialize;
          
                  // different constructors initialize the value differently:
                  ParentWithConstructor(){
                      //init a null object
                      aFieldToInitialize = null;
                      System.out.println("Constructor of " 
                          + this.getClass().getSimpleName() + " inits to null");
                  }
          
                  ParentWithConstructor(String... params) {
                      //init all fields to intended values
                      aFieldToInitialize = "intended init Value";
                      System.out.println("initializing in parameterized constructor of:" 
                          + this.getClass().getSimpleName());
                  }
              }
          
              class ChildOfParentWithConstructor extends ParentWithConstructor{
                  public static void main (String... args){
                      System.out.println(new ChildOfParentWithConstructor().aFieldToInitialize);
                  }
              }
          

          输出:

          Constructor of ChildOfParentWithConstructor inits to null
          null
          

          -> 默认情况下,这会将字段初始化为null,即使它可能不是您想要的结果。

          【讨论】:

          • 初始化器可以被隐藏是什么意思?
          • 子类也可能以静态块或其他方式更改或初始化父字段“aFieldToInitialize”。父静态块仍会执行,但子静态块可能会再次覆盖数据。
          • aFieldToInitialize 是最终的。它不能被覆盖。
          • 触摸。不过,要点保持不变;-) 为简单起见,我删除了决赛。
          • 你说的还是没有道理。基类应始终设计其构造函数以正确初始化自身,然后您尝试发明的问题就不存在了。初始化器与构造器相比没有任何优势。
          【解决方案10】:

          请注意,执行一些副作用的静态初始化程序的一个大问题是它们不能在单元测试中被模拟。

          我见过图书馆这样做,这是一个很大的痛苦。

          所以最好只保留那些静态初始化器。

          【讨论】:

            猜你喜欢
            • 2015-05-01
            • 2014-03-22
            • 1970-01-01
            • 1970-01-01
            • 2013-11-17
            • 2010-11-24
            • 2011-03-10
            • 1970-01-01
            • 2018-08-02
            相关资源
            最近更新 更多