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

时间:2010-01-03 06:53:58

标签: java constructor instance-variables

这两种方法都有优势吗?

示例1:

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

示例2:

class A {
    B b;

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

15 个答案:

答案 0 :(得分:255)

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

    {
        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 - 即让其他人(另一个类/框架)实例化并在您的类中注入依赖项。

答案 1 :(得分:35)

另一种选择是使用Dependency Injection

class A{
   B b;

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

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

答案 2 :(得分:17)

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

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初始化。

答案 3 :(得分:15)

我个人的“规则”(几乎没有破坏)是:

  • 在开头声明所有变量 一个块
  • 将所有变量设为final,除非它们 不能
  • 每行声明一个变量
  • 从不初始化变量where 声明
  • 只能初始化某个内容 构造函数何时需要来自的数据 做的构造函数 初始化

所以我的代码如下:

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变量,然后在需要具有值的那些变量的一半之前抛出异常),因此最终可能会更高效。你也不会做无意义的初始化(比如int i = 0;然后在之后,在使用“i”之前,做i = 5;。

我非常重视一致性,所以遵循这个“规则”是我一直在做的事情,这使得使用代码变得更加容易,因为你不必去找东西。

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

答案 4 :(得分:7)

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

如果实例化不仅需要简单的new,请使用初始化块。无论使用的构造函数是什么,都将运行。 E.g。

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

    // ...

}

答案 5 :(得分:4)

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

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

有关Java初始化的更多详细信息,请参阅http://java.sun.com/docs/books/tutorial/java/javaOO/initial.html(有关initalizer块和其他未知的初始化功能的说明)。

答案 6 :(得分:4)

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

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

  1. 避免重复 =如果您有多个构造函数,或者当您需要添加更多构造函数时,您将不必在所有构造函数体中反复重复初始化;
  2. 提高了可读性 =您可以轻松地了解哪些变量必须从课外进行初始化;
  3. 减少代码行 =对于在声明处完成的每次初始化,构造函数中都会有一行更少。

答案 7 :(得分:1)

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

答案 8 :(得分:1)

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

答案 9 :(得分:1)

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

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

这个优势当然是所有其他逻辑推理之后的,但对于某些人来说,“功能”可能更重要。

答案 10 :(得分:0)

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

答案 11 :(得分:0)

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

答案 12 :(得分:0)

第二个选项是优选的,因为允许在ctors中使用不同的逻辑进行类实例化并使用ctors链接。 E.g。

class A {
    int b;

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

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

所以第二种选择更灵活。

答案 13 :(得分:0)

实际上是完全不同的:

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

因此在类级别声明变量,然后在构造函数中对其进行初始化。

答案 14 :(得分:0)

import numpy as np

a = np.random.rand(256, 256) # something with shape (256, 256)

c = a[ :, :, None] # start out by copying a into c but add in an extra dimension using None

for i in np.arange(10):
    b = np.random.rand(256, 256) # b is also something with shape (256, 256)
    c = np.concatenate((c, b[ :, :, None]), axis=2) # concatenate it to c, again using None to add in the extra dimension to b, and joining along the new axis.

c.shape # will be (256,256,11) for each of the ten iterations plus the initial copying of a into c.

关于上述内容,

    class MyClass extends FooClass {
    String a = null;

    public MyClass() {
        super();     // Superclass calls init();
    }

    @Override
    protected void init() {
        super.init();
        if (something)
            a = getStringYadaYada();
    }
}

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

String a = null;