Java 中初始化器与构造器的使用
-
03-07-2019 - |
题
因此,我最近一直在温习我的 Java 技能,并发现了一些我以前不知道的功能。静态初始化器和实例初始化器就是两种这样的技术。
我的问题是何时使用初始化程序而不是在构造函数中包含代码?我想到了几种明显的可能性:
静态/实例初始值设定项可用于设置“最终”静态/实例变量的值,而构造函数不能
静态初始化器可用于设置类中任何静态变量的值,这应该比在每个构造函数开头使用“if (someStaticVar == null) // do stuff”代码块更有效
这两种情况都假设设置这些变量所需的代码比简单的“var = value”更复杂,否则似乎没有任何理由使用初始值设定项而不是在声明变量时简单地设置值。
然而,虽然这些并不是微不足道的收益(特别是设置最终变量的能力),但似乎应该使用初始化程序的情况相当有限。
人们当然可以使用初始化程序来完成构造函数中所做的许多事情,但我真的不明白这样做的原因。即使一个类的所有构造函数共享大量代码,使用私有的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"));
}
}
}
}
不要忘记,您现在必须在类级别而不是实例级别进行同步。这会为构造的每个实例产生成本,而不是加载类时的一次性成本。另外,它很丑;-)
我读了整篇文章,寻找初始化器的初始化顺序与初始化器的初始化顺序的答案。他们的构造者。我没有找到,所以我写了一些代码来检查我的理解。我想我应该添加这个小演示作为评论。为了测试您的理解程度,请在阅读底部之前看看您是否可以预测答案。
/**
* 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()
[IE。没有参数]隐式或者你必须做出一个特定的super(...)
手动调用。)
这意味着隐式或显式的super(...)
调用可能不会按照父类的预期初始化子类。
考虑这个初始化块的示例:
class ParentWithInitializer {
protected final 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 final 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 中加载驱动程序时,会发生类加载,并且会触发 Driver 类的静态初始化程序,并且其中的代码将 Driver 注册到 Driver Manager。这是静态代码块的重要用途之一。
正如您所提到的,它在很多情况下没有用,并且与任何较少使用的语法一样,您可能希望避免它只是为了阻止下一个人查看您的代码花费 30 秒将其从库中取出。
另一方面,这是做一些事情的唯一方法(我认为你几乎涵盖了这些)。
无论如何,静态变量本身应该在某种程度上避免——并不总是如此,但如果你使用了很多静态变量,或者你在一个类中使用了很多静态变量,你可能会找到不同的方法,你未来的自己会感谢你的。