249

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

示例 1:

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

示例 2:

class A {
    B b;

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

15 回答 15

282
  • 没有区别 - 实例变量初始化实际上是由编译器放入构造函数中的。
  • 第一个变体更具可读性。
  • 您不能使用第一个变体进行异常处理。
  • 另外还有初始化块,它也由编译器放入构造函数中:

    {
        a = new A();
    }
    

查看Sun 的解释和建议

本教程

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

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

ExpensiveObject o;

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

最终(正如 Bill 所指出的),为了依赖管理,最好避免在类中的任何地方使用new操作符。相反,使用依赖注入更可取——即让其他人(另一个类/框架)实例化并在你的类中注入依赖。

于 2010-01-03T07:27:10.300 回答
39

另一种选择是使用依赖注入

class A{
   B b;

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

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

于 2010-01-03T07:31:43.567 回答
24

我今天以一种有趣的方式被烧毁:

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(),初始化之后初始化。aa = null

于 2015-05-20T00:08:57.287 回答
15

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

  • 在块的开头声明所有变量
  • 使所有变量最终,除非它们不能
  • 每行声明一个变量
  • 永远不要在声明的地方初始化变量
  • 仅当需要来自构造函数的数据进行初始化时才在构造函数中初始化某些内容

所以我会有如下代码:

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”之前,做 i = 5;。

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

你的旅费可能会改变。

于 2010-01-03T07:50:55.417 回答
7

示例 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.
        }
    }

    // ...

}
于 2010-02-18T15:03:26.873 回答
4

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

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

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

于 2010-01-03T07:02:04.970 回答
4

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

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

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

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

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

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

于 2018-07-30T09:13:27.680 回答
1

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

于 2010-01-03T07:00:38.060 回答
1

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

于 2010-01-03T07:01:26.827 回答
0

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

于 2010-01-03T07:01:51.130 回答
0

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

于 2015-06-09T14:15:52.560 回答
0

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

class A {
    int b;

    // secondary ctor
    A(String b) {
         this(Integer.valueOf(b));
    }

    // primary ctor
    A(int b) {
         this.b = b;
    }
}

所以第二种选择更灵活。

于 2017-06-14T14:10:28.160 回答
0

实际上完全不同:

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

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

于 2019-02-04T01:26:27.933 回答
0
    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";
    }
    ...
于 2019-10-03T12:42:49.377 回答