BlackFlame33

BlackFlame33

若无力驾驭,自由便是负担。个人博客 https://blackflame33.cn/

《On Java》閱讀筆記-02

第 6 章 初始化和清理#

‘不安全’的編程是導致編程成本高昂的罪魁禍首之一。

而 == 初始化 == 和 == 清理 == 正是導致‘不安全’編程的兩個因數。

本章著重說明了 Java 使用構造器初始化對象以及垃圾收集器的一些說明。圖靈導讀指南說明本章有重點和難點,因此儘量詳細闡述自己的心得。

第六章

用構造器保證初始化#

構造器(構造函數),是 Java 保證對象能夠正確初始化的重要手段。構造器是與類名完全相同沒有返回類型的方法。類名完全相同意味著構造器並不適用於方法名採用 lowerCamelCase 的編程規範;沒有返回類型並非返回類型為void,而是沒有返回類型。與 C++ 對比,Java 的構造器與初始化是一體的。

這裡會有一個經典的陷阱:我們在編寫代碼時,總是使用new關鍵字,調用構造器創建對象,所以很容易認為構造器的作用是創建對象,然而事實卻並非如此。對象的創建是由JVM負責的,JVM負責調用構造器,而構造器的作用僅為為對象的初始化。

方法重載#

方法重載的規則:每個重載方法必須有獨一無二的參數類型列表,即:

  1. 形參數量不同。
  2. 形參數量相同,類型不同。
  3. 形參數量相同,類型相同,順序不同。
  • 返回值類型不同,不構成方法重載。
  • 形參標識符不同,不構成方法重載。
  • 修飾符不同,不構成方法重載。

當然,當傳入參數的類型不在重載方法的參數類型列表中時,較小的類型會被提升;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實際上採用了這種方案。
而垃圾收集器的回收算法,大體上可以分為以下幾種:

  1. 停止 - 複製
  2. 標記 - 清除
  3. 分代
  4. 自適應

所有的方案都遵循一個思路:所有堆上存活的對象,最後都能追溯到棧上或靜態存儲中的引用。

停止 - 複製方案#

這種方案的處理簡單粗暴:直接將程序停止,然後將所有存活的對象(也就是可以通過引用訪問的對象)從一個堆複製到另外一個堆。沒有被複製的對象將視為垃圾被回收掉。
該方案會有兩個明顯的缺點:

  1. 需要兩倍的內存來實現複製。當然這個問題也有解決方案:有些JVM實現可以再將堆分成塊,複製發生在塊之間。
  2. 若程序運行穩定,垃圾產生很少。但仍舊會觸發 “停止 - 複製”,這是一種資源浪費。

標記 - 清除方案#

為了防止 “停止 - 複製方案” 造成的資源浪費,當檢測沒有垃圾產生時,JVM會切換到 “標記 - 清除” 方案:從棧和靜態存儲開始,遍歷查找所有引用指向的對象,對查找到的對象設置標誌,此時並未發生回收動作。當遍歷完成後,再執行回收。
該方案在通常情況下效率比 “停止 - 複製” 方案低,但當程序穩定,垃圾沒有那麼多時,該方案的速度就很快了。

自適應#

JVM會根據情況,自動切換適合的方案執行。

唉,確實是繞,可能我的 Java 境界還沒到能研究 GC 的地步。這裡做個 TO-DO ,後期再來補更詳細的細節。

對象創建的過程#

  1. 當第一次創建對象,或第一次訪問類的靜態成員或者方法時,Java 解釋器會根據路徑搜索對應的.class文件。
  2. .class文件被加載後,按照文件定義順序開始其所有的靜態初始化。靜態初始化僅會在首次加載時發生一次。
  3. 在堆上創建對應的對象,分配足夠的存儲空間。
  4. 將其對象的所有基本類型設為默認值,引用設置為null
  5. 按照文件定義順序初始化成員變量。
  6. 執行構造器。

可變參數列表#

function(Object... args)使用省略號,編譯器會自動為你填充,args 是一個長度可變的數組。可變參數列表必須放在參數列表的末尾。

JDK 11局部變量類型推斷#

JDK 11中,可以通過關鍵字var聲明變量,編譯器會自動進行類型推斷。

第 7 章 實現隱藏#

本章主要介紹的核心思想為:將變化的事物與保持不變的事物分離。通過訪問權限修飾符允許庫開發者說明哪些是對客戶程序員可用的,哪些是不可用的。個人覺得重點主要是在搞懂訪問權限修飾符上。

第 7 章

  1. public:任意類均能訪問,實際就是沒有限制訪問權限。
  2. protected:同包中的其他類都可以訪問,不同包下必須是子類才能夠訪問。
  3. (缺省的)什麼關鍵字都不寫,表示同包中的其他類都可以訪問。
  4. private:僅對自身類中的其他成員可見。

第 8 章 復用#

本章主要介紹了Java語言的兩種代碼復用的方式:組合繼承組合是指在新類中創建現有類的對象,我們復用的是代碼的功能,而非形式。繼承則是直接複製了現有類的形式,並添加了新代碼,使其成為不污染現有代碼的新類。圖靈導讀中還詳細解讀了類加載的時機與順序,與繼承體系中,子類與基類初始化的正確順序。

第 8 章

重載與重寫#

  • 方法重載:在同一個類中,或者基類與子類中,多個方法的方法名可以相同,這被叫做方法重載。方法重載的條件為:

    1. 形參數量不同时。

    2. 形參數量相同,數據類型不同时。

    3. 形參數量相同,數據類型相同,順序不同时。

      不過第 3 種最好別實際運用。

  • 方法重寫:重寫是子類對父類的允許訪問的方法的實現過程進行重新編寫,返回值和形參都不能改變。即外殼不變,核心重寫!重點是方法簽名是一樣的,不能更改!

慎重考慮繼承#

儘管繼承思想在各類書籍和教材中都受到重視,但這並不意味著我們要儘可能的使用繼承。在有相關需求時,最好是優先使用組合,再謹慎考慮使用繼承。書中提供了一種清晰分辨是選擇組合還是考慮繼承的問題:“我需要使用向上轉型嗎?”。

final#

final數據#

對於基本數據類型,final關鍵字使其值恆定不變;對於引用數據類型,final使其引用恆定不變。也就是說,一旦一個被final修飾的引用被指向(初始化)了一個對象,那麼這個引用將無法指向另外一個對象。

空白final#

原話摘抄:對final執行賦值的操作只能發生在兩個地方:

  1. 欄位定義處使用表達式賦值。
  2. 每個構造器中。

理由:這是為了保證final欄位在使用前總是被初始化。

private方法就是隱式的final方法#

方法中兩個修飾符添加在一起沒有額外的意義。private方法無法訪問,也無法重寫。雖然測試下來好像可以重寫,但其實兩者沒有任何關係。private方法已經不是類的接口了,它只是類中隱含的代碼。所以兩個類中的相同方法名只是重名了,當你用註解@Override時就能檢查出這個錯誤。

初始化及類的加載#

類的初始化時機一般為:

8.9 初始化及類的加載 - 8.9 初始化及類的加載 @02-21.07 1672980574647

而類的初始化順序為:

8.9 初始化及類的加載 - 8.9 初始化及類的加載 @02-21.07 1672980574648

第 9 章 多態#

本章詳細介紹了多態 —— 面向對象思想的又一大基本特徵,它是程序員 “將變化的事物與不變的事物分離” 的一項重要技術。回憶大學的 Java學習中,多態常常與封裝,繼承並稱面向對象思想的三大特徵。可是就目前的眾多反饋,包括《On Java》書中也提到,若非必要,優先考慮組合,謹慎考慮繼承。讀過本章後,我的總結是:繼承的優點就在可以運用多態上。多態是這麼的美好,以至於應該儘可能運用多態;但繼承體系導致了後續若有稍微不適用於繼承體系的新類出現後,後續的重構修改會很頭疼。而接口可以有效解決這個頭疼的問題。

第 9 章

綁定#

使用多態時,常常會有這樣的問題:明明我傳入的是基類的引用,調用的是基類的方法,為什麼把子類引用傳入,也能調用子類的方法呢?換句話說,編譯器是怎麼知道我這個引用的確切類型的呢?實際上編譯器是不知道的,但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.nameStudent.name被分配了不同的存儲空間。要想訪問父類的同名字段,必須顯示使用super.name。所以為了避免這種令人困惑的情況,一般情況下都是將成員變量設為private

構造器內部的多態方法行為#

準則:用盡可能少的操作使對象進入正常狀態,若非必要,儘量不要再構造器中調用任何其他方法。

使用多態的例子,假如父類的構造器使用了一個方法,這個方法同時還被子類重寫,這種情況下就會很危險:== 調用了此時還沒有初始化的子類對象的方法!==

協變返回類型#

子類重寫方法的返回值可以是基類方法返回值的子類型

使用繼承的準則#

使用繼承表達行為上的差異,使用字段表達狀態上的變化。

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。