第 6 章 初始化和清理#
‘不安全’的編程是導致編程成本高昂的罪魁禍首之一。
而 == 初始化 == 和 == 清理 == 正是導致‘不安全’編程的兩個因數。
本章著重說明了 Java 使用構造器初始化對象以及垃圾收集器的一些說明。圖靈導讀指南說明本章有重點和難點,因此儘量詳細闡述自己的心得。
用構造器保證初始化#
構造器(構造函數),是 Java 保證對象能夠正確初始化的重要手段。構造器是與類名完全相同的沒有返回類型的方法。類名完全相同意味著構造器並不適用於方法名採用 lowerCamelCase 的編程規範;沒有返回類型並非返回類型為void
,而是沒有返回類型。與 C++ 對比,Java 的構造器與初始化是一體的。
這裡會有一個經典的陷阱:我們在編寫代碼時,總是使用new
關鍵字,調用構造器創建對象,所以很容易認為構造器的作用是創建對象,然而事實卻並非如此。對象的創建是由JVM
負責的,JVM
負責調用構造器,而構造器的作用僅為為對象的初始化。
方法重載#
方法重載的規則:每個重載方法必須有獨一無二的參數類型列表,即:
- 形參數量不同。
- 形參數量相同,類型不同。
- 形參數量相同,類型相同,順序不同。
- 返回值類型不同,不構成方法重載。
- 形參標識符不同,不構成方法重載。
- 修飾符不同,不構成方法重載。
當然,當傳入參數的類型不在重載方法的參數類型列表中時,較小的類型會被提升;char
類型會被提升為int
類型;類型過大則會報錯。當方法調用可以匹配多個方法時,一般選擇數據類型 “近” 的,自動類型轉換 “次數最少” 的。
無參構造器#
無參構造器的常規部分還蠻好理解的:沒有參數的構造器。
不過需要注意的是:只要你已經定義了一個構造器,無論其是否帶參數,編譯器都 == 不會 == 幫你自動創建一個無參構造器。
形象比喻:當你沒有定義構造器時,編譯器會認為:“哦~你一定需要一個構造器,讓我來幫你添加一個無參構造器吧”;但當你已經提供有構造器,編譯器認為:“你已經有構造器了,你知道自己在做什麼;如果你自己沒有提供無參構造器的話,你一定有自己的理由,或許是你不想要它,那我也不會幫你添加。”
它真的,我哭死
this
關鍵字#
this
參數是每個成員方法的隱式傳參,它是當前對象的引用,這可以通過向終端輸出該引用確認。不要濫用this
,這會影響代碼的可讀性,它應該只出現在必要的地方。
在構造器中,this
如果後面帶參數列表,表示調用匹配參數列表的構造器。注意,不能同時調用兩個,並且構造器調用必須出現在方法的最開始部分,也就是方法體的第一行。
static
靜態方法沒有this
,這是其靜態的本質:靜態方法沒法隱式傳參this
,因為靜態方法與對象無關。
清理與垃圾收集#
資源清理#
我們常常使用new
關鍵字創建對象,JVM
也會通過垃圾收集器,也就是 GC,來進行對象的生命週期管理。Java
程序員 “可以” 一股腦的創建對象,而不必關心該對象何時需要銷毀。
使用 finalize () 方法管理內存空間釋放,這個方法的調用時機是由 GC 控制的。可 GC 何時啟動,這是不確定的。因此不建議使用 finalize () 方法。實際上,自從JDK 9
開始,finalize () 方法已被註解為@Deprecated
,這個方法已經被棄用了R.I.P, finalize()
目前推薦的方式是顯式定義 “善後” 方法,例如Java
中各種流的close()
方法。
垃圾收集#
垃圾收集器GC
,是在空閒時期不定時回收沒有引用的對象的內存空間的一種機制。它讓我們不必像C++
那樣不使用對象後必須顯式執行析構函數。這減輕了程序員的工作,緩解了內存泄漏的問題。
為何存儲的釋放會影響存儲的分配#
C
等一眾語言在堆上分配空間的前例,可能會讓你覺得Java
在堆上分配所有內容(基本類型除外)的代價很高昂。但實際上你也可以將Java
的分配方式想象成傳送帶:分配空間後,“堆指針” 向前移動到尚未分配的區域,這就和棧分配的方式很類似了,儘管也會有一些額外開銷。但這樣不是沒有節制的,長期以往,這樣會導致操作系統開始執行分頁調度—— 通過移入移出磁碟,可以執行比物理內存大得多的程序,高效利用物理內存空間,但也會顯著影響性能。而這就到GC
出場的時刻了,垃圾收集器會清理所有無用數據,壓縮對象,回收空間,將 “堆指針” 重新移到靠近起始位置的地方,從而儘量避免產生缺頁錯誤—— 當需要的頁不在內存中時,操作系統會產生中斷,從磁碟中調入需要的頁再重新執行中斷產生處的代碼。
引用計數 —— 實際沒有採用的方案#
我們可能或多或少聽過有關垃圾收集器實現原理的介紹:每個對象都持有一個引用計數器,每當對象被引用時,計數器都會增加;而每當引用離開作用域或者被設為 null 時,計數器都會減少。垃圾收集器會檢查這些計數器是否為 0,為 0 表示沒有引用指向它,開始回收它的空間。引用計數通常用於解釋垃圾收集器的工作方式,但並沒有哪款JVM
實際上採用了這種方案。
而垃圾收集器的回收算法,大體上可以分為以下幾種:
- 停止 - 複製:
- 標記 - 清除:
- 分代:
- 自適應:
所有的方案都遵循一個思路:所有堆上存活的對象,最後都能追溯到棧上或靜態存儲中的引用。
停止 - 複製方案#
這種方案的處理簡單粗暴:直接將程序停止,然後將所有存活的對象(也就是可以通過引用訪問的對象)從一個堆複製到另外一個堆。沒有被複製的對象將視為垃圾被回收掉。
該方案會有兩個明顯的缺點:
- 需要兩倍的內存來實現複製。當然這個問題也有解決方案:有些
JVM
實現可以再將堆分成塊,複製發生在塊之間。 - 若程序運行穩定,垃圾產生很少。但仍舊會觸發 “停止 - 複製”,這是一種資源浪費。
標記 - 清除方案#
為了防止 “停止 - 複製方案” 造成的資源浪費,當檢測沒有垃圾產生時,JVM
會切換到 “標記 - 清除” 方案:從棧和靜態存儲開始,遍歷查找所有引用指向的對象,對查找到的對象設置標誌,此時並未發生回收動作。當遍歷完成後,再執行回收。
該方案在通常情況下效率比 “停止 - 複製” 方案低,但當程序穩定,垃圾沒有那麼多時,該方案的速度就很快了。
自適應#
JVM
會根據情況,自動切換適合的方案執行。
唉,確實是繞,可能我的 Java 境界還沒到能研究 GC 的地步。這裡做個 TO-DO ,後期再來補更詳細的細節。
對象創建的過程#
- 當第一次創建對象,或第一次訪問類的靜態成員或者方法時,Java 解釋器會根據路徑搜索對應的
.class
文件。 - 當
.class
文件被加載後,按照文件定義順序開始其所有的靜態初始化。靜態初始化僅會在首次加載時發生一次。 - 在堆上創建對應的對象,分配足夠的存儲空間。
- 將其對象的所有基本類型設為默認值,引用設置為
null
。 - 按照文件定義順序初始化成員變量。
- 執行構造器。
可變參數列表#
function(Object... args)
使用省略號,編譯器會自動為你填充,args
是一個長度可變的數組。可變參數列表必須放在參數列表的末尾。
JDK 11
局部變量類型推斷#
在JDK 11
中,可以通過關鍵字var
聲明變量,編譯器會自動進行類型推斷。
第 7 章 實現隱藏#
本章主要介紹的核心思想為:將變化的事物與保持不變的事物分離。通過訪問權限修飾符允許庫開發者說明哪些是對客戶程序員可用的,哪些是不可用的。個人覺得重點主要是在搞懂訪問權限修飾符上。
- public:任意類均能訪問,實際就是沒有限制訪問權限。
- protected:同包中的其他類都可以訪問,不同包下必須是子類才能夠訪問。
- (缺省的)什麼關鍵字都不寫,表示同包中的其他類都可以訪問。
- private:僅對自身類中的其他成員可見。
第 8 章 復用#
本章主要介紹了Java
語言的兩種代碼復用的方式:組合與繼承。組合是指在新類中創建現有類的對象,我們復用的是代碼的功能,而非形式。繼承則是直接複製了現有類的形式,並添加了新代碼,使其成為不污染現有代碼的新類。圖靈導讀中還詳細解讀了類加載的時機與順序,與繼承體系中,子類與基類初始化的正確順序。
重載與重寫#
-
方法重載:在同一個類中,或者基類與子類中,多個方法的方法名可以相同,這被叫做方法重載。方法重載的條件為:
-
形參數量不同时。
-
形參數量相同,數據類型不同时。
-
形參數量相同,數據類型相同,順序不同时。
不過第 3 種最好別實際運用。
-
-
方法重寫:重寫是子類對父類的允許訪問的方法的實現過程進行重新編寫,返回值和形參都不能改變。即外殼不變,核心重寫!重點是方法簽名是一樣的,不能更改!
慎重考慮繼承#
儘管繼承思想在各類書籍和教材中都受到重視,但這並不意味著我們要儘可能的使用繼承。在有相關需求時,最好是優先使用組合,再謹慎考慮使用繼承。書中提供了一種清晰分辨是選擇組合還是考慮繼承的問題:“我需要使用向上轉型嗎?”。
final
#
final
數據#
對於基本數據類型,final
關鍵字使其值恆定不變;對於引用數據類型,final
使其引用恆定不變。也就是說,一旦一個被final
修飾的引用被指向(初始化)了一個對象,那麼這個引用將無法指向另外一個對象。
空白final
#
原話摘抄:對final
執行賦值的操作只能發生在兩個地方:
- 欄位定義處使用表達式賦值。
- 每個構造器中。
理由:這是為了保證final
欄位在使用前總是被初始化。
private
方法就是隱式的final
方法#
方法中兩個修飾符添加在一起沒有額外的意義。private
方法無法訪問,也無法重寫。雖然測試下來好像可以重寫,但其實兩者沒有任何關係。private
方法已經不是類的接口了,它只是類中隱含的代碼。所以兩個類中的相同方法名只是重名了,當你用註解@Override
時就能檢查出這個錯誤。
初始化及類的加載#
類的初始化時機一般為:
而類的初始化順序為:
第 9 章 多態#
本章詳細介紹了多態 —— 面向對象思想的又一大基本特徵,它是程序員 “將變化的事物與不變的事物分離” 的一項重要技術。回憶大學的 Java
學習中,多態常常與封裝,繼承並稱面向對象思想的三大特徵。可是就目前的眾多反饋,包括《On Java》書中也提到,若非必要,優先考慮組合,謹慎考慮繼承。讀過本章後,我的總結是:繼承的優點就在可以運用多態上。多態是這麼的美好,以至於應該儘可能運用多態;但繼承體系導致了後續若有稍微不適用於繼承體系的新類出現後,後續的重構修改會很頭疼。而接口可以有效解決這個頭疼的問題。
綁定#
使用多態時,常常會有這樣的問題:明明我傳入的是基類的引用,調用的是基類的方法,為什麼把子類引用傳入,也能調用子類的方法呢?換句話說,編譯器是怎麼知道我這個引用的確切類型的呢?實際上編譯器是不知道的,但Java
是後期綁定的語言。後期綁定發生在運行時,並且確定對象的類型。正是這種方法調用機制的存在,儘管編譯器實際上並不知道確切的類型,依然可以正確調用方法。而在有了多態這個機制後,我們只需要編寫與基類溝通的方法,就可以與其子類進行溝通,不必再去判斷對象類型,沒有冗餘代碼,實現代碼復用。
繼承與成員變量#
繼承後如果子類定義了與父類同名的成員變量,則如何展現取決於引用。例如;
class Person {
String name = "Person";
public String getName() {
return name;
}
}
class Student extends Person {
String name = "Student";
@Override
public String getName() {
return name;
}
}
public class Demo {
public static void main(String[] args) {
Student student = new Student();
Person person = new Student();
System.out.println(student.name);
System.out.println(person.name);
System.out.println(student.getName());
System.out.println(person.getName());
}
}
輸出結果:
這說明成員變量沒有多態,成員方法才有多態可言。實際上子類此時會有兩個 name
成員變量:Person.name
和Student.name
被分配了不同的存儲空間。要想訪問父類的同名字段,必須顯示使用super.name
。所以為了避免這種令人困惑的情況,一般情況下都是將成員變量設為private
。
構造器內部的多態方法行為#
準則:用盡可能少的操作使對象進入正常狀態,若非必要,儘量不要再構造器中調用任何其他方法。
使用多態的例子,假如父類的構造器使用了一個方法,這個方法同時還被子類重寫,這種情況下就會很危險:== 調用了此時還沒有初始化的子類對象的方法!==
協變返回類型#
子類重寫方法的返回值可以是基類方法返回值的子類型。
使用繼承的準則#
使用繼承表達行為上的差異,使用字段表達狀態上的變化。