[英]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.