[英]Use of Initializers vs Constructors in Java
所以最近我一直在复习我的 Java 技能,并发现了一些我以前不知道的功能。 静态和实例初始化器就是两种这样的技术。
我的问题是什么时候会使用初始化程序而不是在构造函数中包含代码? 我已经想到了几个明显的可能性:
静态/实例初始值设定项可用于设置“最终”静态/实例变量的值,而构造函数不能
静态初始化器可用于设置类中任何静态变量的值,这应该比在每个构造函数的开头使用“if (someStaticVar == null) // do stuff” 代码块更有效
这两种情况都假设设置这些变量所需的代码比简单的“var = value”更复杂,否则似乎没有任何理由使用初始化程序而不是在声明变量时简单地设置值。
然而,虽然这些不是微不足道的收获(尤其是设置 final 变量的能力),但似乎应该使用初始化器的情况相当有限。
对于构造函数中所做的很多事情,当然可以使用初始化程序,但我真的看不出这样做的原因。 即使一个类的所有构造函数都共享大量代码,对我来说,使用私有的 initialize() 函数似乎比使用初始化器更有意义,因为它不会让你在编写新的代码时运行该代码构造函数。
我错过了什么吗? 是否有许多其他情况应该使用初始化程序? 或者它真的只是一个在非常特定情况下使用的相当有限的工具?
正如 cletus 提到的,静态初始化器很有用,我以相同的方式使用它们。 如果您有一个在类加载时要初始化的静态变量,那么静态初始化程序是可行的方法,特别是因为它允许您进行复杂的初始化并且仍然使静态变量为final
。 这是一个很大的胜利。
我发现“if (someStaticVar == null) // do stuff”很混乱并且容易出错。 如果它被静态初始化并声明为final
,那么你就避免了它为null
的可能性。
但是,当您说:
静态/实例初始值设定项可用于设置“最终”静态/实例变量的值,而构造函数不能
我假设你说的是:
你在第一点上是正确的,在第二点上是错误的。 例如,您可以这样做:
class MyClass {
private final int counter;
public MyClass(final int counter) {
this.counter = counter;
}
}
此外,当构造函数之间共享大量代码时,处理此问题的最佳方法之一是链接构造函数,提供默认值。 这很清楚正在做什么:
class MyClass {
private final int counter;
public MyClass() {
this(0);
}
public MyClass(final int counter) {
this.counter = counter;
}
}
匿名内部类不能有构造函数(因为它们是匿名的),因此它们非常适合实例初始化器。
我最常使用静态初始化块来设置最终的静态数据,尤其是集合。 例如:
public class Deck {
private final static List<String> SUITS;
static {
List<String> list = new ArrayList<String>();
list.add("Clubs");
list.add("Spades");
list.add("Hearts");
list.add("Diamonds");
SUITS = Collections.unmodifiableList(list);
}
...
}
现在这个例子可以用一行代码完成:
private final static List<String> SUITS =
Collections.unmodifiableList(
Arrays.asList("Clubs", "Spades", "Hearts", "Diamonds")
);
但是静态版本可以更简洁,特别是当项目不是很容易初始化时。
一个简单的实现也可能不会创建一个不可修改的列表,这是一个潜在的错误。 以上创建了一个不可变的数据结构,您可以愉快地从公共方法等返回。
只是在这里添加一些已经很好的点。 静态初始化程序是线程安全的。 它在类加载时执行,因此比使用构造函数更简单的静态数据初始化,在构造函数中,您需要一个同步块来检查静态数据是否已初始化,然后实际初始化它。
public class MyClass {
static private Properties propTable;
static
{
try
{
propTable.load(new FileInputStream("/data/user.prop"));
}
catch (Exception e)
{
propTable.put("user", System.getProperty("user"));
propTable.put("password", System.getProperty("password"));
}
}
相对
public class MyClass
{
public MyClass()
{
synchronized (MyClass.class)
{
if (propTable == null)
{
try
{
propTable.load(new FileInputStream("/data/user.prop"));
}
catch (Exception e)
{
propTable.put("user", System.getProperty("user"));
propTable.put("password", System.getProperty("password"));
}
}
}
}
不要忘记,您现在必须在类而不是实例级别进行同步。 这会为每个构造的实例带来成本,而不是加载类时的一次性成本。 另外,它很丑;-)
我阅读了整篇文章,寻找初始化器与构造器的 init 顺序的答案。 我没有找到,所以我写了一些代码来检查我的理解。 我想我会添加这个小演示作为评论。 为了测试你的理解,看看你是否可以在阅读底部之前预测答案。
/**
* Demonstrate order of initialization in Java.
* @author Daniel S. Wilkerson
*/
public class CtorOrder {
public static void main(String[] args) {
B a = new B();
}
}
class A {
A() {
System.out.println("A ctor");
}
}
class B extends A {
int x = initX();
int initX() {
System.out.println("B initX");
return 1;
}
B() {
super();
System.out.println("B ctor");
}
}
输出:
java CtorOrder
A ctor
B initX
B ctor
静态初始化器相当于静态上下文中的构造函数。 您肯定会比实例初始化程序更频繁地看到它。 有时您需要运行代码来设置静态环境。
一般来说,实例初始化器最适合匿名内部类。 看看JMock 的说明书,看看有没有一种创新的方式来使用它来提高代码的可读性。
有时,如果你有一些复杂的逻辑在构造函数之间链接起来很复杂(假设你是子类化并且你不能调用 this() 因为你需要调用 super()),你可以通过在实例中做常见的事情来避免重复初始化器。 然而,实例初始化器是如此罕见,以至于它们对许多人来说是一种令人惊讶的语法,所以我避免使用它们,如果我需要构造函数行为,我宁愿使我的类具体而不是匿名。
JMock 是一个例外,因为这就是框架的用途。
您在选择时必须考虑一个重要方面:
初始化块是类/对象的成员,而构造函数不是. 这在考虑扩展/子类化时很重要:
super()
[即没有参数],或者您必须手动进行特定的super(...)
调用。)super(...)
调用可能无法按照父类的预期初始化子类。考虑这个初始化块的例子:
class ParentWithInitializer {
protected String aFieldToInitialize;
{
aFieldToInitialize = "init";
System.out.println("initializing in initializer block of: "
+ this.getClass().getSimpleName());
}
}
class ChildOfParentWithInitializer extends ParentWithInitializer{
public static void main(String... args){
System.out.println(new ChildOfParentWithInitializer().aFieldToInitialize);
}
}
输出:
initializing in initializer block of: ChildOfParentWithInitializer
init
-> 不管子类实现了什么构造函数,字段都会被初始化。
现在考虑这个带有构造函数的例子:
class ParentWithConstructor {
protected String aFieldToInitialize;
// different constructors initialize the value differently:
ParentWithConstructor(){
//init a null object
aFieldToInitialize = null;
System.out.println("Constructor of "
+ this.getClass().getSimpleName() + " inits to null");
}
ParentWithConstructor(String... params) {
//init all fields to intended values
aFieldToInitialize = "intended init Value";
System.out.println("initializing in parameterized constructor of:"
+ this.getClass().getSimpleName());
}
}
class ChildOfParentWithConstructor extends ParentWithConstructor{
public static void main (String... args){
System.out.println(new ChildOfParentWithConstructor().aFieldToInitialize);
}
}
输出:
Constructor of ChildOfParentWithConstructor inits to null
null
-> 默认情况下,这会将字段初始化为null
,即使它可能不是您想要的结果。
除了以上所有精彩的答案,我还想补充一点。 当我们使用 Class.forName("") 在 JDBC 中加载驱动程序时,类加载发生,驱动程序类的静态初始化程序被触发,其中的代码将驱动程序注册到驱动程序管理器。 这是静态代码块的重要用途之一。
正如您所提到的,它在很多情况下都没有用,并且与任何较少使用的语法一样,您可能希望避免它只是为了阻止下一个人查看您的代码花费 30 秒将其从保险库中取出。
另一方面,这是做一些事情的唯一方法(我认为你几乎涵盖了这些)。
无论如何都应该避免静态变量本身——并非总是如此,但如果你使用了很多静态变量,或者你在一个类中使用了很多,你可能会发现不同的方法,你未来的自己会感谢你。
请注意,执行一些副作用的静态初始值设定项的一个大问题是它们不能在单元测试中被模拟。
我见过图书馆这样做,这是一个很大的痛苦。
所以最好只保留那些静态初始化器。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.