我应该在声明时还是在构造函数中实例化实例变量?

254

有没有任何一种方法更有优势?

示例1:

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

例子2:

class A {
    B b;

    A() {
         b = new B();
    }
}
15个回答

289
  • 没有区别 - 实例变量初始化实际上是由编译器放在构造函数中的。
  • 第一种变体更易读。
  • 第一种变体无法进行异常处理。
  • 此外还有初始化块,也是由编译器放在构造函数中的:

    {
        a = new A();
    }
    

请查看Sun的解释和建议

根据这个教程

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

此外,您可能希望对字段进行惰性初始化。在初始化字段是一项昂贵操作的情况下,您可以在需要时立即进行初始化:

ExpensiveObject o;

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

正如 Bill 指出的那样,为了依赖管理的考虑,在你的类中最好避免使用 new 运算符。而是使用依赖注入 - 即让其他类/框架来实例化和注入你类中的依赖。


1
@Bozho 对象初始化是在构造函数之前还是之后进行初始化块? - Cruncher
之前,我认为。但不确定 :) - Bozho
13
第一个变量更易读,这是有争议的:如果你在构造函数中初始化了所有字段,就可以确切地知道,当你阅读代码时,只需在一个地方搜索即可... - nbro
@Bozho - 您能否解释一下为什么第一种变体不能进行异常处理? - Mario Galea
@Bozho 我认为在第一种情况下,调用new时的异常无法在类本身内处理,而是会被抛出并让用户处理(或不处理)。而在第二种情况下,可以在new周围放置try {},并在类本身内处理异常。 - Gordon Liang
3
最终(正如Bill所指出的),为了依赖管理起见,在类中最好避免使用new运算符。相反,使用依赖注入更为可取。至少你说得很好。如果过于热衷于遵循这种“Bob主义”,可能会导致诸多问题(如工厂膨胀)。使用new运算符并非有错,也并非所有依赖关系都需要注入,特别是如果您对社交化测试感到满意的话。 - brumScouse

39

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

class A{
   B b;

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

这样做可以将创建 B 对象的责任从 A 的构造函数中移除。这会使您的代码在长期维护和测试时更易于管理。其思想是降低类 AB 之间的耦合度。其中一个好处是,现在您可以将任何扩展 B 的对象(或者是实现 B 接口的对象)传递给 A 的构造函数,它都可以正常工作。缺点是放弃了对 B 对象的封装性,因此它暴露给了调用 A 构造函数的用户。您必须考虑这种权衡是否值得,但在许多情况下确实是有益的。


12
另一方面,这会增加耦合,因为现在您已经使得AB之间的联系更加明显。以前,使用BA内部问题,如果事实证明不使用B更好,则更难更改您的建议。 - JaakkoK
17
耦合性是存在的 - A 需要 B。但是在类内实例化意味着 "A 需要 确切的这个B ",而依赖注入则允许使用多种不同的 B。 - Bozho
4
在这个设计中,A 现在需要 B,我的观点是关于如果这种情况发生变化的。 - JaakkoK
3
如果你在创建对象时将其与业务逻辑分离,特别是在创建A的地方使用DI和工厂类,那么更改就不难了。它只需要在一个地方进行更改,即创建A对象的工厂中。如果你始终保持一致,这并不难理解。我认为好处大于成本。耦合减少了,整体设计更易于测试和维护。 - Bill the Lizard
5
@BilltheLizard,你会使用这个成语来描述像 List<Integer> intList = new ArrayList<>(); 这样简单的东西吗?这完全可以是一个内部实现细节。将ArrayList传递到构造函数中似乎与良好封装的理念背道而驰。 - sprinter
显示剩余6条评论

25

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

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初始化程序之后发生。


14
这里的教训是永远不要在构造函数中调用可重写的函数! :) 《Effective Java》第17条对此有很好的讨论。 - Mohit Chugh
1
非常好的观点。通过在声明时初始化,您失去了对变量何时初始化的控制。这可能会让你陷入困境(是的,编译器也会改变它们的实现!)。 - SMBiggs
1
@MohitChugh:确实,坚如磐石。事实上,现代的Java IDE(比如NetBeans,当然还有其他的)如果你从构造函数中调用可重写方法,它们会向你发出警告。这是因为Edward Falk遇到的原因。 - GeertVc

15

我的个人“规则”(几乎从不违反)是:

  • 在块的开头声明所有变量
  • 除非无法将其设置为final,否则使所有变量final
  • 每行声明一个变量
  • 不要在声明变量时初始化变量
  • 只有在需要构造函数中的数据来进行初始化时才在构造函数中初始化变量

因此,我的代码看起来像:

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

通过这种方式,我始终可以百分之百确定在哪里查找变量声明(在块的开头),以及它们的赋值(在声明后尽快进行,只要有意义)。这样做可能更有效率,因为你不会使用不需要的值来初始化变量(例如声明和初始化变量,然后在需要一半变量具有值之前抛出异常)。同时也不会进行无意义的初始化(比如 int i = 0; 然后稍后,在使用 "i" 之前,将其设置为 i = 5;。

我非常重视一致性,因此我始终遵循这个“规则”,这使与代码的工作变得更加容易,因为你不必四处搜索。

你的情况可能有所不同。


1
我宁愿人们因为技术原因而投反对票,而不是因为审美原因(例如{ }的放置或非必需使用)。如果人们投反对票,他们至少应该说出他们认为答案有什么问题...从技术上讲没有任何问题,这是我在过去20年中在C/C++/Java中编码的方式(好吧,Java 16),所以我百分之百确定它可以工作 :-) (感谢反对票 :-)) - TofuBeer
25
这东西丑得要死,这就是它的问题所在。你不愿使用三元运算符,却喜欢多个静态初始化块而非适当的面向对象构造函数,真是有趣。你这种方法完全破坏了依赖注入(表面上,编译器会帮你把所有东西都移到构造函数中,但实际上你在教人们依赖编译器的魔法,而不是正确的方式),难以维护,让我们回到了可怕的C++时代。新手读者,请不要这样做。 - Visionary Software Solutions
@VisionarySoftwareSolutions 我看到一个静态初始化块。对于三元运算符,当你将来有多个语句时,它会变得非常丑陋。我不使用 DI,我更愿意依赖编译器的魔法(它并不是魔法,它在语言规范中被明确定义),这是可调试的,而不是运行时的魔法,这是无法知晓的。 - TofuBeer
2
如果你要包括规则关于使能够final的变量都是final的话,那么你真的应该包括将所有可以是private的变量都设为私有的部分。 - Cruncher
1
@TofuBeer:别担心。大多数 Java 开发人员倾向于过度追求细节和挑剔。我相信,即使 Joshua Bloch 写的代码(假设他们不知道是他写的)也会遭到他们的挑剔。个人口味因人而异;最终,CPU 和 JRE 都不在乎语法风格。 - Aquarelle
显示剩余4条评论

7
例子2不够灵活。如果添加另一个构造函数,你需要记得在该构造函数中也要实例化字段。直接实例化字段,或在getter中引入延迟加载。
如果实例化需要更多的操作,使用初始化块。这将在任何使用的构造函数中运行。例如:
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.
        }
    }

    // ...

}

4

在其他答案中已经详细解释了,通常优先使用依赖注入延迟初始化

当您不想或不能使用这些模式,并且对于原始数据类型,我可以想到三个强有力的理由,说明在构造函数外部初始化类属性的优先级更高:

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

3

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

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

有关Java中初始化的更多详细信息(以及有关初始化器块和其他不太知名的初始化功能的解释),请参见http://java.sun.com/docs/books/tutorial/java/javaOO/initial.html


这就是为什么你需要 DI 和 @Required :) - Sujoy
是的,我只是在描述 OP 的两个示例之间的区别。 - Vinko Vrsalovic
很有可能,大量的构造函数意味着您正在违反单一职责原则,并且在设计方面存在更大的问题。 - Visionary Software Solutions

2
我在回复中没有看到以下内容:
在声明时初始化的一个可能的优点是现今的IDE可以很容易地跳转到变量的声明处(通常为Ctrl-<hover_over_the_variable>-<left_mouse_click>),无论你在代码的哪个位置。这样,您就可以立即看到该变量的值。否则,您需要“搜索”初始化的位置(通常为构造函数)。
当然,这个优点次于所有其他逻辑上的原因,但对于一些人来说,“这个特性”可能更重要。

1

这两种方法都是可行的。请注意,在后一种情况下,如果存在另一个构造函数,则b=new B()可能不会被初始化。将构造函数外的初始化器代码视为公共构造函数,并执行该代码。


1

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接