我应该实例化实例变量的声明或构造函数?

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

示例1:

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

示例2:

class A {
B b;


A() {
b = new B();
}
}
105252 次浏览

这两种方法都可以接受。注意,在后一种情况下,如果存在另一个构造函数,b=new B()可能无法初始化。将构造函数外部的初始化器代码看作一个公共构造函数,代码将被执行。

我认为例2更可取。我认为最佳实践是在构造函数外部声明,并在构造函数中初始化。

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

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

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

有关Java中初始化的更多细节,请参阅http://java.sun.com/docs/books/tutorial/java/javaOO/initial.html(以及关于初始化器块和其他不为人所知的初始化特性的解释)。

  • 没有区别——实例变量初始化实际上是由编译器放在构造函数中。
  • 第一个变体可读性更强。
  • 你不能对第一个变体进行异常处理。
  • 另外还有初始化块,它也由编译器放在构造函数中:

    {
    a = new A();
    }
    

Check Sun's explanation and advice

From this tutorial:

Field declarations, however, are not part of any method, so they cannot be executed as statements are. Instead, the Java compiler generates instance-field initialization code automatically and puts it in the constructor or constructors for the class. The initialization code is inserted into a constructor in the order it appears in the source code, which means that a field initializer can use the initial values of fields declared before it.

Additionally, you might want to lazily initialize your field. In cases when initializing a field is an expensive operation, you may initialize it as soon as it is needed:

ExpensiveObject o;


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

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

另一个选项是使用依赖注入

class A{
B b;


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

这免去了从A的构造函数中创建B对象的责任。从长远来看,这将使您的代码更易于测试和维护。其思想是减少两个类AB之间的耦合。这样做的一个好处是,你现在可以将任何扩展B(如果是接口,则实现B)的对象传递给A的构造函数,并且它将工作。一个缺点是你放弃了对B对象的封装,因此它暴露给了A构造函数的调用者。你必须考虑这些好处是否值得这样的代价,但在很多情况下是值得的。

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

  • 在开头声明所有变量 一块李< / >
  • 使所有变量为final,除非它们 李不能< / >
  • 每行声明一个变量
  • 从不初始化变量 李宣布< / >
  • 只初始化a中的内容 构造函数时使用 的构造函数 初始化李< / >

我的代码是这样的:

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%确定在哪里寻找变量声明(在块的开始),以及它们的赋值(在声明之后尽快有意义)。这可能会更有效,因为你从来没有用没有用过的值初始化变量(例如,声明和init vars,然后在一半的vars需要有值之前抛出异常)。你也不会做无意义的初始化(比如int i = 0;然后,在使用i之前,i = 5;。

我非常重视一致性,所以遵循这个“规则”是我一直在做的事情,它使处理代码变得更容易,因为你不需要到处寻找东西。

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

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


// ...


}

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

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

  1. 避免重复 =如果你有多个构造函数,或者当你需要添加更多构造函数时,你将不必在所有构造函数体中反复重复初始化;
  2. 改善可读性 =你一眼就能看出哪些变量必须从类外初始化;
  3. 减少代码行数 =在声明时进行的每一次初始化都会在构造函数中减少一行。

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

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初始化的之后

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

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

class A {
int b;


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


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

所以第二种选择更加灵活。

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

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

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

其实很不一样:

声明发生在构建之前。因此,如果在两个位置都初始化了变量(本例中为b),构造函数的初始化将取代在类级别所做的初始化。

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

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