[英]Passing "this" to private final members in Java
基於什么是“不完全構造的對象”? 以及JVM 的隱式 memory 障礙在鏈接構造函數時如何表現? 在構造函數中使用this
被認為是不好的做法,應該避免。 初始化private final
成員也是如此嗎?
例如下面的代碼,將this
傳遞給Chapter
是否被認為是不好的做法? 我知道如果Book
也以與章節相同的方式初始化,那么這個問題將不會出現,問題是book.title
是Chapter
構造函數中的null
。
public class Book {
protected final String title; // = "Title"
private class Chapter {
private final Book book;
Chapter(Book b) {
book = b; // Here book.title is null
}
}
private final Chapter chapter1 = new Chapter(this); // Bad Practice or valid in some cases?
private final Chapter chapter2 = new Chapter(this);
Book() {
title = "Book: 2 Chapters";
}
}
是的,不好的做法。 從某種意義上說,它與在構造函數中傳遞this
的情況實際上是相同的。
During the initialization of an object, there's a rather complex rigamarole that the runtime has to go through, considering that [A] the class itself must be available first (and this static
initialization which is also quite complicated), [B] the constructor必須運行,但為了做到這一點,必須首先運行父級的調用構造函數,並且 [C] 構造函數不是全部,還有實例初始化程序塊!
每當您有一個分配了值的(非靜態)字段時,只有兩個選項:
A.那個值是一個編譯時間常數,比如private int x = 5;
. 常量很少——基本上是原語和字符串。
B. 其他任何事情。
按照“A”的常數直接存儲在 class 文件中。 但是其他任何東西都不能“僅僅”直接存儲在 class 文件中(列出字段的塊對於 state 有空間,它的值必須從常量池初始化為特定常量,但是System.currentTimeMillis()
或new Chapter(this)
顯然不是你可以堅持在常量池中的東西)。 在 class 文件級別,這基本上只是為您移動到構造函數中。 一個實際的方法是用一個時髦的名字( <init>
,它不是一個有效的標識符,所以它不可能與一個名為<init>
的實際方法沖突,因為那不是有效的java)並被調用。 您可以自己編寫初始化程序,使用 java 語法的奇異位:只需在 class 定義中直接扔大括號:
class Test {
public Test() {
hello(30);
}
int foo = 5; // constant
int bar = hello(10);
{ System.out.println("Instance initializer"); }
int baz = hello(20);
static int hello(int in) {
System.out.println("Hello: " + in);
return in;
}
public static void main(String[] args) { new Test(); }
}
你可以編譯並運行上面的,你會得到:
Hello: 10
Instance initializer
Hello: 20
Hello: 30
它向您展示了它是如何工作的:所有“實例初始化”都以“詞法順序”運行(按照它們在源文件中出現的順序),並且private int bar = hello(10);
之間沒有區別和{hello(20);}
- 它們都是實例初始化器。
大喊“不好的做法!!!!”的問題是編程對於那種死記硬背的規則應用來說太難了。 如果就這么簡單,我們就讓計算機自己編程。
不,嚴格遵守一系列不良做法只是意味着你最終會得到更糟糕的代碼。 你不能用“經驗法則”來清理代碼('clean' 定義為:易於閱讀(這反過來意味着:你很快就會對代碼的作用有了初步印象,並且這個初始印象不會誤導),易於測試,靈活(定義為:當預期的更改請求出現時,處理這些請求並不特別困難,並且不需要對與此代碼交互的代碼進行很多更改,即很少或沒有 API 更改例子))。
因此,您必須首先了解為什么在構造函數中傳遞this
是“不好的做法”。 不理解為什么意味着該規則對您毫無用處。
原因是 - 您已完成初始化,變量可能還沒有值。 讓我們看看它的實際效果!
class Test {
final int bar = hello(this, 10);
final int baz = hello(this, 20);
final int foo = hello(this, 30);
public static int hello(Test t, int v) {
System.out.println("BAZ: " + t.baz);
return v;
}
public static void main(String[] args) {
new Test();
}
}
運行此打印:
BAZ: 0
BAZ: 0
BAZ: 20
這完全是,離牆,堅果。 final
變量是如何變化的?!?!!!
這就是為什么這是一個非常糟糕的主意的最大原因。 還有很多,但這個很容易理解,足以說明為什么你永遠不應該這樣做。 該行為通常屬於“令人難以置信的令人困惑的黑客”類別。 這意味着如果你必須訴諸這樣的事情,你必須添加大量關於為什么、將會發生什么以及你期望的前置和后置條件的評論。 通常,無論您花多少時間編寫和維護這些評論(請記住,維護評論真的很困難,因為您無法對它們進行單元測試)。 當然,無論您認為這樣做是為了實現什么捷徑,這都不值得,但是,嘿,如果真的值得,那么一定要這樣做。 破解。 不要忘記那些評論。
EXERCISE FOR THE READER: If you want to learn more, I suggest you write a more extensive example which also involves static initializers (non-constant expressions that initialize static fields, as well as static { codeGoesHere(); }
, just like that in一個class
定義,例如class Foo { static { System.out.println("HELLO;"); }}
),以及類型的層次結構。 對於加分,讓父類型的構造函數調用一個不是最終的方法,然后在您的子類中覆蓋該方法並讓該方法與字段交互。 例子:
class Parent {
Parent() {
System.out.println(thisIsWeird());
}
String thisIsWeird() {
return "Parent";
}
}
class Child extends Parent {
private final String field = "Simple".toLowerCase();
@Override String thisIsWeird() {
return field;
}
}
然后運行它,開始得出結論(然后准備忘記所有這些細節,只記住結果,即:不要做這種瘋狂的事情,這不值得)。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.