两种方法都有任何优势吗?
例1:
class A {
B b = new B();
}
例2:
class A {
B b;
A() {
b = new B();
}
}
{
a = new A();
}
检查Sun's explanation and advice
但是,字段声明不是任何方法的一部分,因此它们不能像语句那样执行。相反,Java编译器会自动生成实例字段初始化代码,并将其放在类的构造函数或构造函数中。初始化代码按照它在源代码中出现的顺序插入到构造函数中,这意味着字段初始值设定项可以使用在它之前声明的字段的初始值。
此外,您可能希望懒洋洋地初始化您的字段。如果初始化字段是一项昂贵的操作,您可以在需要时立即对其进行初始化:
ExpensiveObject o;
public ExpensiveObject getExpensiveObject() {
if (o == null) {
o = new ExpensiveObject();
}
return o;
}
最终(正如Bill所指出的),为了依赖管理,最好避免在班级的任何地方使用new
运算符。相反,最好使用Dependency Injection - 即让其他人(另一个类/框架)实例化并在您的类中注入依赖项。
我在回复中没有看到以下内容:
在声明时进行初始化的一个可能的优点可能是现在的IDE,您可以从代码中的任何位置轻松跳转到变量(主要是Ctrl-<hover_over_the_variable>-<left_mouse_click>
)的声明。然后,您立即看到该变量的值。否则,您必须“搜索”初始化的位置(主要是:构造函数)。
这种优势当然是所有其他逻辑推理的次要因素,但对于某些人而言,“特征”可能更为重要。
第二个是延迟初始化的示例。第一个是更简单的初始化,它们基本相同。
在构造函数之外进行初始化还有一个微妙的原因是之前没有人提到过(非常具体我必须说)。如果您使用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;
}
}
所以第二种选择更灵活。
它实际上是完全不同的:
声明在施工前发生。因此,如果在两个位置初始化了变量(在本例中为b),构造函数的初始化将替换在类级别完成的变量。
因此,在类级别声明变量,在构造函数中初始化它们。
另一种选择是使用Dependency Injection。
class A{
B b;
A(B b) {
this.b = b;
}
}
这消除了从B
的构造函数创建A
对象的责任。从长远来看,这将使您的代码更易于测试并且更易于维护。这个想法是减少两个类A
和B
之间的耦合。这给你带来的好处是你现在可以传递任何扩展B
的对象(或者如果它是一个接口就实现B
)到A
的构造函数,它会起作用。一个缺点是你放弃了对B
对象的封装,所以它暴露给A
构造函数的调用者。你必须考虑这些利益是否值得这种权衡,但在很多情况下它们都是。
今天我以一种有趣的方式被烧了:
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
初始化。
我个人的“规则”(几乎没有破坏)是:
所以我会有如下代码:
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;。
我非常重视一致性,所以遵循这个“规则”是我一直在做的事情,这使得使用代码变得更加容易,因为你不必去找东西。
你的旅费可能会改变。
例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,那么只有在使用最后一个构造函数时才会得到null b。
有关Java中初始化的更多详细信息,请参阅http://java.sun.com/docs/books/tutorial/java/javaOO/initial.html(以及有关初始化程序块和其他未知的初始化功能的说明)。
使用依赖注入或延迟初始化总是优选的,正如在其他答案中已经详细解释的那样。
当您不希望或不能使用这些模式以及原始数据类型时,有三个令人信服的理由我可以想到为什么在构造函数之外初始化类属性更为可取:
这两种方法都是可以接受的。请注意,在后一种情况下,如果存在另一个构造函数,则b=new B()
可能无法初始化。将构造函数外部的初始化代码视为通用构造函数并执行代码。
我认为例2更可取。我认为最好的做法是在构造函数外部声明并在构造函数中初始化。