我应该在声明或构造函数中实例化实例变量吗?
这两种方法有没有优势?
例1:
class A { B b = new B(); }
例2:
class A { B b; A() { b = new B(); } }
- 没有区别 – 实例变量初始化实际上是由编译器放在构造函数中的。
- 第一个变体更具可读性。
- 第一个变体不能进行异常处理。
-
还有一个初始化块,它也被编译器放在构造函数中:
{ a = new A(); }
查看Sun的解释和建议
从这个教程 :
然而,字段声明不是任何方法的一部分,所以它们不能像声明那样执行。 相反,Java编译器会自动生成实例字段初始化代码,并将其放入该类的构造函数或构造函数中。 初始化代码按照它在源代码中出现的顺序插入到构造函数中,这意味着字段初始值设定项可以使用它之前声明的字段的初始值。
另外,你可能想懒惰地初始化你的领域。 在初始化一个字段是一个昂贵的操作的情况下,你可以在需要的时候立即初始化它:
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
对象的责任。 这将使您的代码更加可测试,并且从长远来看更容易维护。 这个想法是为了减少两个类A
和B
之间的耦合。 这给你带来的好处是,你现在可以传递任何扩展B
对象(或者实现B
如果它是一个接口的话)给A
的构造函数,它就可以工作。 一个缺点是你放弃了B
对象的封装,所以它暴露给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%确定在哪里寻找变量声明(在块的开始处)以及它们的赋值(只要在声明后有意义)。 这可能会更有效,因为你永远不会用一个没有使用的值初始化一个变量(例如declare和init vars,然后在这些变量的一半需要有一个值之前抛出一个异常)。 你也不会做无意义的初始化(像int i = 0;然后在“i”被使用之前,做i = 5;)。
我非常重视一致性,所以遵循这个“规则”是我一直在做的事情,它使得使用代码更容易,因为你不必四处寻找东西。
你的旅费可能会改变。
今天我被烧得很有意思:
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
初始化。
示例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. } } // ... }
我认为它几乎只是一个味道的问题,只要初始化简单,不需要任何逻辑。
如果你不使用初始化块,构造函数的方法会更脆弱一点,因为如果你稍后添加第二个构造函数并忘记初始化b,那么只有在使用最后一个构造函数时才会得到一个空b。
使用依赖注入或惰性初始化总是可取的,正如在其他答案中已经详细解释的那样。
当你不想或不能使用这些模式,以及对于原始数据类型时,有三个令人信服的理由,我可以想到为什么最好在构造函数之外初始化类属性:
- 避免重复 =如果你有多个构造函数,或者当你需要添加更多的时候,你将不必在所有的构造函数体内重复初始化;
- 改进的可读性 =你可以很容易地看出哪些变量将不得不从课外进行初始化;
- 减少的代码行数 =在声明中完成的每个初始化操作都会在构造函数中减少一行。
我认为例2是优选的。 我认为最好的做法是在构造函数之外声明并在构造函数中进行初始化。
这两种方法都可以接受。 请注意,在后一种情况下,如果存在另一个构造函数,则b=new B()
可能不会被初始化。 将构造函数之外的初始化程序代码视为一个通用构造函数,并执行代码。
第二个是延迟初始化的例子。 首先是更简单的初始化,它们本质上是相同的。
还有一个更微妙的原因是在构造函数之外初始化,之前没有人提到(非常具体,我必须说)。 如果您使用UML工具从代码生成类图(逆向工程),我相信大部分工具都会注意到示例1的初始化,并将其转换为图(如果您希望显示初始值,例如我做)。 他们不会从例子2中得到这些初始值。再一次,这是一个非常具体的原因 – 如果你正在使用UML工具,但是一旦我了解到了这一点,我试图把所有我的默认值都带到构造函数之外,除非前面提到,有一个可能的异常抛出或复杂的逻辑问题。
第二个选项是可取的,因为允许在类实例化中使用不同的逻辑,并使用ctors链接。 例如
class A { int b; // secondary ctor A(String b) { this(Integer.valueOf(b)); } // primary ctor A(int b) { this.b = b; } }
所以第二个选项更灵活。