这两种方法有什么优势吗?
示例 1:
class A {
B b = new B();
}
示例 2:
class A {
B b;
A() {
b = new B();
}
}
这两种方法有什么优势吗?
示例 1:
class A {
B b = new B();
}
示例 2:
class A {
B b;
A() {
b = new B();
}
}
另外还有初始化块,它也由编译器放入构造函数中:
{
a = new A();
}
从本教程:
但是,字段声明不是任何方法的一部分,因此它们不能像语句一样执行。相反,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;
}
}
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% 确定在哪里查找变量声明(在块的开头),以及它们的分配(只要在声明之后有意义)。这最终也可能更有效,因为您从不使用未使用的值初始化变量(例如声明和初始化变量,然后在一半需要具有值的变量之前抛出异常)。你也不会做无意义的初始化(比如 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,那么只有在使用最后一个构造函数时您才会得到一个空 b 。
请参阅http://java.sun.com/docs/books/tutorial/java/javaOO/initial.html了解有关 Java 初始化的更多详细信息(以及有关初始化程序块和其他不为人知的初始化功能的说明)。
使用依赖注入或延迟初始化总是更可取的,正如其他答案中已经详细解释的那样。
当您不想或不能使用这些模式时,对于原始数据类型,我可以想到三个令人信服的原因,为什么最好在构造函数之外初始化类属性:
我在回复中没有看到以下内容:
在声明时进行初始化的一个可能优势可能是现在的 IDE,您可以在其中非常轻松地
Ctrl-<hover_over_the_variable>-<left_mouse_click>
从代码中的任何位置跳转到变量的声明(主要是 )。然后您会立即看到该变量的值。否则,您必须“搜索”完成初始化的位置(主要是:构造函数)。
这个优势当然次于所有其他逻辑推理,但对于某些人来说,“特征”可能更重要。
这两种方法都可以接受。请注意,在后一种情况下,b=new B()
如果存在另一个构造函数,则可能不会被初始化。将构造函数之外的初始化代码视为通用构造函数,然后执行代码。
我认为示例 2 更可取。我认为最好的做法是在构造函数之外声明并在构造函数中初始化。
第二个是延迟初始化的例子。第一个是更简单的初始化,它们本质上是相同的。
在构造函数之外进行初始化还有一个更微妙的原因,之前没有人提到过(我必须说非常具体)。如果您使用 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),构造函数的初始化将替换在类级别完成的初始化。
所以在类级别声明变量,在构造函数中初始化它们。
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";
}
...