《Java程式設計思想》讀書筆記一

小懶程式設計日記發表於2022-01-26

很早之前就買了《Java程式設計思想》這本書,初學時看這本書看的雲裡霧裡的,實在費勁,就放在一邊墊桌底了。感覺這本書是適合C/C++程式設計師轉行到Java學習的一本書,並不適合零基礎的初學者去看這本書,畢竟當初花了一百多買了這本書,現在還是把它倒騰出來看一下吧,當作是鞏固Java基礎知識,本文會把自己感興趣的知識點記錄一下,相關例項程式碼:https://gitee.com/reminis_com/thinking-in-java

第一章:物件導論

  這一章主要是幫助我們瞭解物件導向程式設計的全貌,更多是介紹的背景性和補充性的材料。其實萌新應該跳過這一章,因為這章並不會去講語法相關的知識,當然可以在看完這本書後續章節後,再來回看這一章,這樣有助於我們瞭解到物件的重要性,以及怎樣使用物件進行程式設計。

​ Alan Kay曾經總結了第一個成功的面嚮物件語言、同時也是Java所基於的語言之一的Smalltalk的五個基本特性,這些特性表現了一種純粹的物件導向的程式設計方式:

  1. 萬物皆為物件。理論上講,你可以抽取待求解問題的任何概念化構件(狗、建築物、服務等),將其表示為程式中的物件。
  2. 程式是物件的集合,它們通過傳送訊息來告知彼此所要做的。要想請求一個物件,就必須對該物件傳送一條訊息。更具體的說,可以把訊息想象為對某個特定物件的方法的呼叫請求。
  3. 每個物件都有自己的由其它物件所構成的儲存。換句話說,可以通過建立包含現有物件的方式來建立新型別的物件。
  4. 每個物件都擁有其型別。按照通用的說法,“每個物件都是某個類(class)的一個例項(instance)”,每個類最重要的區別與其他類的特性就是“可以傳送什麼樣的訊息給它”。
  5. 某一特定型別的所有物件都可以接受同樣的訊息

第二章:一切都都是物件

用引用操縱物件

  每種程式語言都有自己操作記憶體中元素的方式。有時候,程式設計師必須注意將要處理的資料是什麼型別,你是直接操縱元素,還是用某種特殊語法的間接表示(例如C/C++裡得指標)來操作物件?

  所有這一切在Java裡都得到了簡化。一切都被視為物件,因此可採用單一固定的語法。儘管一切都看作物件,但操縱的識別符號實際上是物件的一個"引用"(reference)。可以將這情形想像成用遙控器(引用)來操縱電視機(物件)。只要握住這個遙控器,就能保持與電視機的連線。當有人想改變頻道或者減小音量時,實際操控的是遙控器(引用),再由遙控器來調控電視機(物件)。如果想在房間裡四處走走,同時仍能調控電視機,那麼只需攜帶遙控器(引用)而不是電視機(物件)。
此外,即使沒有電視機,遙控器亦可獨立存在。也就是說,你擁有一個引用,並不一定需要有一個物件與它關聯。

儲存到什麼地方

  程式執行時,物件是怎麼進行放置安排的呢?特別是記憶體是怎樣分配的呢?對這些方面的瞭解會對你有很大的幫助。有五個不同的地方可以儲存資料∶
1)暫存器。這是最快的儲存區,因為它位於不同於其他儲存區的地方——處理器內部。但是暫存器的數量極其有限,所以暫存器根據需求進行分配。你不能直接控制,也不能在程式中感覺到暫存器存在的任何跡象(另一方面,C和C++允許您向編譯器建議暫存器的分配方式)。
2)堆疊。位於通用RAM(隨機訪問儲存器)中,但通過堆疊指標可以從處理器那裡獲得直接支援。堆疊指標若向下移動,則分配新的記憶體;若向上移動、則釋放那些記憶體。這是一種快速有效的分配儲存方法,僅次於暫存器。建立程式時,Java系統必須知道儲存在堆疊內所有項的確切生命週期,以便上下移動堆疊指標。這一約束限制了程式的靈活性,所以雖然某些Java 資料儲存於堆疊中--特別是物件引用,但是Java物件並不儲存於其中。
3)。一種通用的記憶體池(也位於RAM區),用於存放所有的Java物件。堆不同於堆疊的好處是∶編譯器不需要知道儲存的資料在堆裡存活多長時間。因此,在堆裡分配儲存有很大的靈活性。當需要一個物件時,只需用new寫一行簡單的程式碼,當執行這行程式碼時、會自動在堆裡進行儲存分配。當然,為這種靈活性必須要付出相應的代價∶用堆進行儲存分配和清理可能比用堆疊進行儲存分配需要更多的時間(如果確實可以在Java中像在C++中一樣在棧中建立物件)。
4)常量儲存。常量值通常直接存放在程式程式碼內部,這樣做是安全的,因為它們永遠不會被改變。有時,在嵌入式系統中,常量本身會和其他部分隔離開,所以在這種情況下,可以選擇將其存放在ROM(只讀儲存器)中。
5)非RAM儲存。如果資料完全存活於程式之外,那麼它可以不受程式的任何控制,在程式沒有執行時也可以存在。其中兩個基本的例子是流物件和持久化物件。在流物件中,物件轉化成位元組流,通常被髮送給另一臺機器。在"持久化物件"中,物件被存放於磁碟上,因此,即使程式終止,它們仍可以保持自己的狀態。這種儲存方式的技巧在於∶把物件轉化成可以存放在其它媒介上的事物,在需要時,可恢復成常規的、基於RAM的物件。java提供了對輕量級持久化的支援,而諸如JDBC和Hibernate這樣的機制提供了更加複雜的對在資料庫中儲存和讀取物件資訊的支援。

第三章:操作符

本章的內容比較基礎,主要講了賦值、算數操作符、關係操作符、邏輯操作符、按位操作符、移位操作符、三元操作符等基礎知識。本章只是記錄下遞增和遞減的相關知識。

自動遞增和遞減

遞增和遞減操作符不僅改變了變數,並且以變數的值作為生成的結果。這兩個操作符各有兩種使用方式,通常稱為字首式和字尾式,對於字首遞增和字首遞減(假設a是一個int值,如++a或--a),會先執行運算,再生成值,而對於字尾遞增和字尾遞減(如a++或a--),會先生成值,在執行運算,下面是一個例子:

public class AutoInc {

    public static void main(String[] args) {
        int i = 1;
        System.out.println("i: " + i); // 1
        System.out.println("++i: " + ++i); // 執行完運算後才得到值,故輸出2
        System.out.println("i++: " + i++); // 運算執行之前就得到值,故輸出2
        System.out.println("i: " + i); //  3
        System.out.println("--i: " + --i); // 執行完運算後才得到值,故輸出2
        System.out.println("i--: " + i--); // 運算執行之前就得到值,故輸出2
        System.out.println("i: " + i); // 1
    }
}

總結:對於字首形式,我們在執行完運算後才得到值。但對於字尾形式,則是在運算執行之前就得到值。

第四章:控制執行流程

  本章介紹了大多數程式語言都具有的基本特性:運算、操作符優先順序、型別以及選擇和迴圈等。例如布林表示式、迴圈如while、do-While、for、分支判斷如if-else以及選擇語句switch-case-break等。由於本章的內容都是非常基礎的語法知識,這裡不再贅述。

第五章:初始化和清理

  在Java中,通過提供構造器,類得設計者可以確保每個物件都會得到初始化。建立物件時,如果其類具有構造器,Java就會在使用者有能力操作物件之前自動呼叫相應的構造器,從而保證了初始化的進行。對於不再使用的記憶體資源,Java提供了垃圾回收器機制,垃圾回收器會自動地將其釋放。

  1. 為什麼不能以返回值區分過載方法?

比如下面兩個 方法,雖然他們有同樣的方法名稱和形參列表,但卻很容易區分它們:

public void f(int i);
public int f(int i) { return i; }

只要編譯器可以根據語境明確判斷出語義,比如在 int x = f(1)中,那麼的確可以據此區分過載方法。不過,有時我們並不關心方法的返回值,我們想要的是方法呼叫的其它效果(這通常被稱為“為了副作用而呼叫”),這時你可能會呼叫方法而忽略其返回值,如這樣呼叫方法:f(1),此使Java如何才能判斷你呼叫的哪一個f(int i)方法呢?因此,根據方法的返回值來區分過載是行不通的。

  1. 靜態資料的初始化
    無論你建立多少個物件,靜態資料都只佔用一份儲存區域。static關鍵字不能應用於區域性變數,因此它只能作用於域。如果一個域是靜態的基本型別域,且沒有對他進行初始化,那麼它就會獲得基本型別的標準初始值,如果它是一個物件引用,那麼它的預設初始值就是null。

靜態資料初始化示例如下:

public class StaticInitialization {
    public static void main(String[] args) {
        System.out.println("Creating new Cupboard() in main");
        new Cupboard();
        System.out.println("Creating new Cupboard in main");
        new Cupboard();
        table.f2(1);
        cupboard.f3(1);
    }
    static Table table = new Table();
    static Cupboard cupboard = new Cupboard();
}

class Bowl {
    Bowl(int marker) {
        System.out.println("Bowl(" + marker + ")");
    }
    void f1(int marker) {
        System.out.println("f1(" + marker + ")");
    }
}

class Table {
    static Bowl bowl1 = new Bowl(1);
    Table() {
        System.out.println("Table()");
        bowl2.f1(1);
    }
    void f2(int marker) {
        System.out.println("f2(" + marker + ")");
    }
    static Bowl bowl2 = new Bowl(2);
}

class Cupboard {
    Bowl bowl3 = new Bowl(3);
    static Bowl bowl4 = new Bowl(4);
    Cupboard() {
        System.out.println("Cupboard");
        bowl4.f1(2);
    }
    void f3(int marker) {
        System.out.println("f3(" + marker + ")");
    }
    static Bowl bowl5 = new Bowl(5);
}
/* Output:
Bowl(1)
Bowl(2)
Table()
f1(1)
Bowl(4)
Bowl(5)
Bowl(3)
Cupboard
f1(2)
Creating new Cupboard() in main
Bowl(3)
Cupboard
f1(2)
Creating new Cupboard in main
Bowl(3)
Cupboard
f1(2)
f2(1)
f3(1)
*/

總結一下物件的建立過程,假設有個名為Dog的類:

  1. 即使沒有顯示地使用static關鍵字,構造器實際上也是靜態方法。因此,當首次建立型別為Dog的物件時(構造器可以看成靜態方法),或者Dog類得靜態方法/靜態域首次被訪問時,Java直譯器必須查詢類路徑,以定位Dog.class檔案。
  2. 然後載入Dog.class,有關靜態初始化的所有動作都會執行,因此,靜態初始化只在Class物件首次被載入的時候進行一次。
  3. 當用new Dog()建立物件的時候,首先將在堆上為Dog物件分配足夠的儲存空間。
  4. 這塊儲存空間會被清零,這就自動地將Dog物件中的所有基本型別資料都設定成了預設值,而引用則被設定成了null
  5. 執行所有出現於欄位定義處的初始化動作
  6. 執行構造器

3.finalize()的用途何在?
  無論物件是如何建立的,垃圾回收器都會負責釋放物件佔據的所有記憶體,這將對finalize()的需求限制到一種特殊情況,即通過某種建立物件方式以外的方式為物件分配了儲存空間,但Java中一切皆為物件,那這種特殊情況是怎麼回事呢?

  看來之所以要有finalize()方法,是由於在分配記憶體時可能採用了類似C語言中的做法,而非Java中的通常做法,這種情況主要發生在“本地方法”的情況下,本地方法是一種在Java中呼叫非Java程式碼的方式,本地方法目前只支援C和C++,但它們可以呼叫其他語言寫的程式碼,所以實際上可以呼叫任何程式碼。在非Java程式碼中,也許會呼叫C的malloc()函式系列來分配儲存空間,而且除非呼叫了free()函式,否則儲存空間將永遠得不到釋放,從而造成記憶體洩漏,當然,free()是C和C++中的函式,所以需要在finalize()中用本地方法呼叫它。

記住,無論是“垃圾回收”還是“終結”,都不保證一定會發生,如果Java虛擬機器(JVM)並未面臨記憶體耗盡的情形,它是不會浪費時間去執行垃圾回收以恢復記憶體的。

如下例,示範了finalize()可能的使用方式:

public class TerminationCondition {
    public static void main(String[] args) {
        Book novel = new Book(true);
        // proper cleanup
        novel.checkIn();
        // Drop the reference, forget to clean up
        new Book(true);
        // 強制進行終結動作,並呼叫finalize()
        System.gc();
    }
}

class Book {
    boolean checkOut = false;
    Book(boolean checkOut) {
        this.checkOut = checkOut;
    }
    void checkIn() {
        checkOut = false;
    }
    @Override
    protected void finalize() {
        if (checkOut) {
            System.out.println("Error: checked out");
            // 你應該總是假設基類的finalize()也要做某些重要的事情,因此要用super來呼叫它
            // super.finalize();
        }
    }
}

本例的總結條件是:所有的Book物件在被當作垃圾回收前都應該被簽入(check in),但在main()方法中,由於程式設計師的錯誤,有一本書未被簽入,要是沒有finalize()來驗證終結條件,將很難發現這種缺陷。

第六章:訪問許可權控制

  本章討論了類是如何被構建成類庫的:首先,介紹了一組類是如何被打包到一個類庫中的;其次,類是如何控制對其成員訪問的。在Java中,關鍵字package、包的命名模式和關鍵字import,可以使你對名稱進行完全的控制,因此名稱衝突的問題是很容易避免的。

  控制對成員的訪問許可權有兩個原因:第一是為了使使用者不要碰觸那些他們不該碰觸的部分,這些部分對於類內部的操作是必要的,但是它並不屬於客戶端程式設計師所需介面的一部分。因此將方法和域指定為private,對客戶端程式設計師而言是一種服務。二是為了讓類庫設計者可以更改類的內部工作方式,而不必擔心這樣會對客戶端程式設計師產生重大的影響。

第七章:複用類

  在本章介紹了兩種程式碼重用機制,分別是組合和繼承。在新的類中產生現有類的物件,由於新的類是由現有類的物件組成,所以這種方法稱為組合。該方法只是複用了現有程式程式碼的功能。第二種方式則是按照現有類的型別來建立新類,無需改變現有類的形式,採用現有類的形式並在其中新增新的程式碼,這種方式稱為繼承。
  在使用繼承時,由於匯出類具有基類介面,因此它可以向上轉型至基類,這對多型來說至關重要。

final關鍵字

可能使用到final的三種情況:屬性,方法和類。

  1. final屬性:對於基本型別,final使數值恆定不變;而用於物件引用,final使引用恆定不變。一但引用被初始化指向一個物件,就無法再把它改為指向另外一物件,然而,物件其自身卻是可以被修改的。
  2. final方法:把方法鎖定,以防任何繼承類修改它的含義。(類中所有的private方法都是隱式地指定為是final的,由於無法取用private方法,所以也就無法在匯出類中覆蓋它。當然你可以對private方法新增final修飾,但這並不能給該方法增加任何額外的意義)
  3. final類:當將某個類的整體定義為final時,就表明了你不打算繼承該類,而且也不允許別人這麼做 。換句話說,出於某種考慮,你對該類的設計永不需要做任何變動,或者出於安全的考慮,你不希望它有子類。(由於final類禁止繼承,所以final類中的所有方法都隱式指定為是final的,因為無法覆蓋他們。在final類中可以給方法新增final修飾詞,但這並不會增添任何意義。)

第八章:多型

  “封裝”通過合併特徵和行為來建立新的資料型別。“實現隱藏”則通過將細節“私有化”把介面和實現分離開來。多型的作用則是消除型別之間的耦合關係,由於繼承允許將物件視為他自己本身的型別或其基型別來加以處理,因此它允許將許多種型別(從同一基類匯出的)視為同一型別來處理,而同一份程式碼也就可以毫無差別地執行在這些不同型別之上了。

方法呼叫繫結

將一個方法呼叫 同 一個方法主體關聯起來被稱作繫結。若在程式執行前進行繫結,就叫做前期繫結(程式導向語言的預設繫結方式)。若在程式執行時根據物件的型別進行繫結就叫做後期繫結(也叫動態繫結和執行時繫結)。

Java中除了static方法和final方法(private方法屬於final方法)之外,其他的所有方法都是後期繫結。由於Java中所有方法都是通過動態繫結來實現多型,我們就可以編寫只與基類打交道的程式程式碼,並且這些程式碼對所有的匯出類都可以正確執行。或者換一種說法,傳送訊息給某個物件,讓該物件去斷定應該做什麼事。

構造器和多型

基類的構造器總是在匯出類的構造過程中被呼叫,而且按照繼承層次逐漸向上連結,以使每個基類的構造器都能得到呼叫,這樣做是有意義的,因為構造器具有一項特殊任務:檢查物件是都被正確構造。匯出類只能訪問它自己的成員,不能訪問基類中的成員(基類成員通常是private型別)。只有基類的構造器才具有恰當的知識和許可權來對自己的元素進行初始化。因此,必須令所有的構造器都得到呼叫,否咋就不能可能正確構造完整物件。這正是編譯器為什麼要強制每個匯出類部分都必須呼叫構造器的原因。

讓我們來看看下面這個例子,他展示了組合、繼承以及多型在構建順序上的作用:

public class Sandwich extends PortableLunch{
    private Bread b = new Bread();
    private Cheese c = new Cheese();
    private Lettuce l = new Lettuce();
    Sandwich() {
        System.out.println("sandwich()");
    }
    public static void main(String[] args) {
        new Sandwich();
    }
}

class Meal {
    Meal() {
        System.out.println("Meal()");
    }
}
class Bread {
    Bread() {
        System.out.println("Bread()");
    }
}
class Cheese {
    Cheese() {
        System.out.println("Cheese()");
    }
}
class Lettuce {
    Lettuce() {
        System.out.println("Lettuce()");
    }
}
class Lunch extends Meal {
    Lunch() {
        System.out.println("Lunch()");
    }
}
class PortableLunch extends Lunch {
    PortableLunch() {
        System.out.println("PortableLunch()");
    }
}
/* Output:
Meal()
Lunch()
PortableLunch()
Bread()
Cheese()
Lettuce()
sandwich()
 */

複雜物件呼叫構造器要遵照如下順序:

  1. 呼叫基類的構造器。這個步驟會不斷地反覆遞迴下去,首先是構造這種層次結構的根,然後是下一層匯出類,等等,直到最底層的匯出類。
  2. 按宣告順序呼叫成員的初始化方法
  3. 呼叫匯出類的構造器主體

構造器內部的多型方法的行為:構造器呼叫的層次結構帶來了一個有趣的兩難問題,如果在一個構造器的內部呼叫正在構造的物件的某個動態繫結方法,那會發生什麼情況呢?一個動態繫結的方法呼叫會向外深入到繼承層次結構內部,它可以呼叫匯出類裡的方法。如果我們是在構造器內部這樣做,那麼就可能會呼叫某個方法,而這個方法所操作的成員變數可能還未進行初始化——這肯定會招致災難,如下例:

public class PolyConstructors {
    public static void main(String[] args) {
        new RoundGlyph(5);
    }
}
class Glyph{
    void draw() {
        System.out.println("Glyph.draw()");
    }
    Glyph() {
        System.out.println("Glyph() before draw()");
        draw();
        System.out.println("Glyph() after draw()");
    }
}

class RoundGlyph extends Glyph {
    private int radius = 1;
    RoundGlyph(int r) {
        this.radius = r;
        System.out.println("RoundGlyph.RoundGlyph(), radius = " + radius);
    }

    @Override
    void draw() {
        System.out.println("RoundGlyph.draw(), radius = " + radius);
    }
}
/* Output:
Glyph() before draw()
RoundGlyph.draw(), radius = 0
Glyph() after draw()
RoundGlyph.RoundGlyph(), radius = 5
 */

由該示例可以看出,上面說的初始化順序並不完整,初始化實際過程的第一步應該是:在其它任何事物發生之前,將分配給物件的儲存空間初始化成二進位制的零。

構造器的編寫準則:用盡可能簡單的方法使物件進入正常狀態,如果可以的話,避免呼叫其他方法。在構造器內唯一能夠安全呼叫的那些方法就是基類中的final方法(也適用於private方法)。

第九章:介面

介面也可以包含域,但是這些域隱式地是static和final的(因此介面就成為了一種很便捷的用來建立常量組的工具)。你可以選擇在介面中顯示地將方法宣告為public的,但即使你不這麼做,它們也是public的。因此,當要實現一個介面時,在介面中被定義的方法必須被定位為是public的;否則,它們將只能得到預設的包訪問許可權,這樣在方法被繼承的過程中,其可訪問許可權就降低了,這是Java編譯器所不允許的。

如果要從一個非介面的類繼承,那麼只能從一個類去繼承。其餘的基本元素都必須是都必須是介面。需要將所有的介面名都置於implements關鍵字之後,用逗號將它們一一隔開。可以繼承任意多個介面,並可以向上轉型為每個介面,因為每一個介面都是一個獨立型別。下面這個例子展示了一個具體類組合數個介面之後產生了一個新類。

interface CanFight {
    void fight();
}

interface CanSwim {
    void swim();
}

interface CanFly {
    void fly();
}

class ActionCharacter {
    public void fight() {}
}

/**
 * 當通過這種方式將一個具體類和多個介面組合在一起時,這個具體類必須放在前面,
 * 後面跟著的才是介面(否則編譯器會報錯)
 */
class Hero extends ActionCharacter
        implements CanFight, CanFly, CanSwim {

    @Override
    public void swim() { }

    @Override
    public void fly() { }
}

public class Adventure {
    public static void t(CanFight x) { x.fight(); }
    public static void f(CanFly x) { x.fly(); }
    public static void s(CanSwim x) { x.swim(); }
    public static void a(ActionCharacter x) { x.fight(); }

    public static void main(String[] args) {
        Hero h = new Hero();
        t(h);
        f(h);
        s(h);
        a(h);
    }
}

該例也展示了使用介面的兩個核心原因:

  1. 為了能夠向上轉型為多個基型別(以及由此而帶來的靈活性)
  2. 防止客戶端程式設計師建立該類的物件,並確保這僅僅是建立一個介面

我們也可以通過繼承來擴充套件介面;通過繼承,可以很容易地在介面中新增新的方法宣告,還可以通過繼承在新介面中組合數個介面。如下:

interface Monster {
    void menace();
}

interface DangerousMonster extends Monster {
    void destroy();
}

interface Lethal {
    void kill();
}

class DragonZilla implements DangerousMonster {
    @Override
    public void menace() {}

    @Override
    public void destroy() {}
}

/**
 * 改語法僅適用於介面繼承
 */
interface Vampire extends DangerousMonster, Lethal {
    void drinkBlood();
} 

class VeryBadVampire implements Vampire {
    @Override
    public void menace() {}

    @Override
    public void destroy() {}

    @Override
    public void kill() {}

    @Override
    public void drinkBlood() {}
}

public class HorrorShow {
    static void u(Monster b) { b.menace(); }
    static void v(DangerousMonster d) {
        d.menace(); 
        d.destroy();
    }
    static void w (Lethal l) {
        l.kill();
    }

    public static void main(String[] args) {
        DangerousMonster barny = new DragonZilla();
        u(barny);
        v(barny);
        Vampire vlad = new VeryBadVampire();
        u(vlad);
        v(vlad);
        w(vlad);
    }
}

由於介面是實現多重繼承的途徑,而生成遵循某個介面的物件的典型方式就是工廠方法設計模式。這與直接呼叫構造器不同,我們在工廠物件上呼叫的時建立方法,而該工廠物件將生成介面的某個實現的物件。理論上,我們的程式碼將完全與介面的實現分離,這就使得我我們可以透明地將某個實現替換成另一個實現,下面的例項展示了工廠方法的結構:

interface Service {
    void method1();
    void method2();
}

interface ServiceFactory {
    Service getService();
}

class Implementation1 implements Service {
    Implementation1() { }

    @Override
    public void method1() {
        System.out.println("Implementation1 method1");
    }
    @Override
    public void method2() {
        System.out.println("Implementation1 method2");
    }
}


class Implementation1Factory implements ServiceFactory {
    @Override
    public Service getService() {
        return new Implementation1();
    }
}

class Implementation2 implements Service {
    Implementation2() { }

    @Override
    public void method1() {
        System.out.println("Implementation2 method1");
    }
    @Override
    public void method2() {
        System.out.println("Implementation2 method2");
    }
}


class Implementation2Factory implements ServiceFactory {
    @Override
    public Service getService() {
        return new Implementation2();
    }
}

public class Factories {
    public static void serviceConsumer(ServiceFactory factory) {
        Service s = factory.getService();
        s.method1();
        s.method2();
    }

    public static void main(String[] args) {
        serviceConsumer(new Implementation1Factory());
        serviceConsumer(new Implementation2Factory());
    }

}

為什麼我們想要新增這種額外級別的間接性呢?一個常見的原因就是想要建立框架。

第十章:內部類

可以將一個類得定義放在另一個類得定義內部,這就是內部類。

連結到外部類

在最初,內部類看起來就像是一種程式碼隱藏機制;其實它還有其他用途。當生成一個內部類的物件時,此物件與製造它的外圍物件之間就有了一種聯絡,所以它能訪問其外圍物件的所有成員,而不需要任何特殊條件。此外,內部類還擁有其外圍類的所有元素的訪問權。如下:

interface Selector {
    // 檢查元素是否到末尾
    boolean end();
    // 訪問當前物件
    Object current();
    // 移動到序列中的下一個物件
    void next();
}

public class Sequence {
    private Object[] items;
    private int next = 0;

    public Sequence(int size) {
        this.items = new Object[size];
    }

    public void add(Object o) {
        if (next < items.length) {
            items[next++] = o;
        }
    }

    // 內部類可以訪問外圍類的方法和欄位
    private class SequenceSelector implements Selector {
        private int i = 0;

        @Override
        public boolean end() {
            // 內部類自動擁有對其外圍類所有成員的訪問權
            return i == items.length;
        }

        @Override
        public Object current() {
            return items[i];
        }

        @Override
        public void next() {
            if (i < items.length) {
                i++;
            }
        }
    }

    public Selector selector() {
        return new SequenceSelector();
    }

    public static void main(String[] args) {
        Sequence sequence = new Sequence(10);
        for (int i = 0; i < 10; i++) {
            sequence.add(Integer.toString(i));
        }
        Selector selector = sequence.selector();
        while (!selector.end()) {
            System.out.print(selector.current() + " ");
            selector.next();
        }
    }
}

使用.this 和 .new

  1. 如果你需要生成對外部物件的引用,可以使用外部類的名字後面緊跟原點和this。這樣產生的引用會自動地具有正確的型別,這一點在編譯器就會被知曉並受到檢查,因此沒有任何執行時開銷,如下:

    public class DoThis {
        void f() {
            System.out.println("DoThis.f()");
        }
    
        public class Inner {
            public DoThis outer() {
                // 使用.this語法,生成外部類物件的引用
                return DoThis.this;
            }
        }
    
        public Inner inner(){
            return new Inner();
        }
    
        public static void main(String[] args) {
            DoThis dt = new DoThis();
            Inner inner = dt.inner();
            inner.outer().f();
        }
    }
    
  2. 有時你可能想要告知某些其他物件,去建立某個內部類的物件,你必須在new表示式中提供對外部類物件的引用,這時需要使用.new語法,如下:

    public class DotNew {
    
        public class Inner {}
    
        public static void main(String[] args) {
            DotNew dotNew = new DotNew();
            // 使用.new 語法生成內部類的物件
            Inner inner = dotNew.new Inner();
        }
    }
    
  3. 在擁有外部類物件之前是不可能建立內部類物件的。這是因為內部類物件會暗暗地連線到建立到它的外部類物件上。但是,如果你建立的時巢狀類(靜態內部類),那麼他就不需要對外部類物件的引用。如下:

public class Parcel3 {
	// 靜態內部類
   static class Contents {
        private int i = 11;
        public int value() {
            return i;
        }
    }

    public static void main(String[] args) {
        Parcel3.Contents contents = new Parcel3.Contents();
        System.out.println(contents.value());
    }

}

在方法和作用域內的內部類

可以在一個方法裡面或者在任意的作用域內定義內部類,這麼做有兩個理由:

  1. 如前所示,你實現了某型別的介面,於是可以建立並返回對其的引用
  2. 你要解決一個複雜的問題,想建立一個類來輔助你的解決方案,但是又不希望這個類是公用的。

下面的這些例子,先前的程式碼將被修改,以用來實現:

  1. 一個定義在方法中的類
  2. 一個定義在作用域內的類,此作用域在方法的內部
  3. 一個實現了介面的匿名類
  4. 一個匿名類,它擴充套件了非預設構造器的類
  5. 一個匿名類,它執行欄位初始化
  6. 一個匿名類,它通過例項初始化實現構造(匿名類不可能有構造器)
    先建立兩個介面:
public interface Contents {
    int value();
}

public interface Destination {
    String readLabel();
}

示例1:展示了在方法的作用域內(為不是在其它類的作用域內),建立一個完整的類,這被稱作區域性內部類。

public class Parcel6 {

    public Destination destination(String s) {
        // 內部類PDestination是destination()方法的一部分,而不是Parcel6的一部分
        // 所以,在destination()方法之外,不能訪問PDestination
        class PDestination implements Destination {
            private String label;
            private PDestination(String whereTo) {
                label = whereTo;
            }
            @Override
            public String readLabel() {
                return label;
            }
        }
        return new PDestination(s);
    }

    public static void main(String[] args) {
        Parcel6 parcel6 = new Parcel6();
        Destination d = parcel6.destination("Tasmania");
    }
}

示例2:下面的示例展示瞭如何在任意的作用域內嵌入一個內部類

public class Parcel7 {
    private void internalTracking(boolean b) {
        if (b) {
            class TrackingSlip {
                private String id;
                TrackingSlip(String s) {
                    id = s;
                }
                String getSlip() {
                    return id;
                }
            }
            TrackingSlip ts = new TrackingSlip("slip");
            String s = ts.getSlip();
            System.out.println(s);
        }
        // 不能在這裡使用,因為已經超出作用域
//        TrackingSlip ts = new TrackingSlip("slip");
    }
    public void track()  {internalTracking(true);}

    public static void main(String[] args) {
        Parcel7 p = new Parcel7();
        p.track();
    }
}

匿名內部類

示例3:匿名內部類

public class Parcel8 {

    /**
     * contents()方法將返回值的生成與表示這個返回值的類的定義放在一起,這個類是匿名的,它沒有名字
     */
    public Contents contents() {
       // 在這個匿名內部類中,使用了預設的構造器來生成Contents()
        return new Contents() {
            private int i = 11;
            @Override
            public int value() {
                return i;
            }
        }; // 這個分號是必須的
    }

    public static void main(String[] args) {
        Parcel8 parcel8 = new Parcel8();
        Contents c = parcel8.contents();
        System.out.println(c.value());
    }
}

示例4:一個匿名類,它擴充套件了有非預設構造器的類

public class Parcel9 {
    public Wrapping wrapping(int x) {
        // 只需要簡單的傳遞合適的引數給基類的構造器即可,這裡是將x傳進ew Wrapping(x)
        return new Wrapping(x) {
            public int value() {
                return super.value() * 47;
            }
        };
    }

    public static void main(String[] args) {
        Parcel9 p = new Parcel9();
        Wrapping w = p.wrapping(10);
        System.out.println(w.value());
    }
}

/**
 * 儘管Wrapping只是一個具有具體實現的普通類,但它還是可以被其匯出類當作公共“介面”來使用
 */
public class Wrapping {

    private int i;
    public Wrapping(int x) {
        i = x;
    }

    public int value() {
        return i;
    }
}

示例5:一個匿名類,它執行欄位初始化

public class Parcel10 {
    // 如果定義一個匿名內部類,並且希望它使用一個在其外部定義的物件,那麼編譯器會要求
    // 其引數是final的,如果你忘記寫了,這個引數也是預設為final的
    public Destination destination(final String dest) {
        return new Destination() {
            private String label = dest;
            @Override
            public String readLabel() {
                return label;
            }
        };
    }

    public static void main(String[] args) {
        Parcel10 p = new Parcel10();
        Destination d = p.destination("Tasmania");
    }

}

示例6:如果知識簡單地給一個欄位賦值,那麼示例四中的方法就很好了。但是,如果想做一些類似構造器的行為,該怎麼辦呢?在匿名類中不可能有命名構造器(因為它根本沒名字),但通過例項初始化,就能夠達到為匿名內部類建立一個構造器的效果,如下:

abstract class Base {
    public Base(int i) {
        System.out.println("Base Constructor, i = " + i);
    }
    public abstract void f();
}

public class AnonymousConstructor {
    public static Base getBase(int i) {
        return new Base(i) {
            // 例項初始化的效果類似於構造器
            {
                System.out.println("Inside instance initializer");
            }
            @Override
            public void f() {
                System.out.println("In anonymous f()");
            }
        };
    }

    public static void main(String[] args) {
        Base base = getBase(47);
        base.f();
    }

}

再訪工廠方法

匿名內部類與正規的繼承相比有些受限,因為匿名內部類既可以擴充套件類,也可以實現介面,但是不能兩者兼備。而且如果是實現介面,也只能實現一個介面。使用匿名內部類重寫工廠方法:

interface Service {
    void method1();
    void method2();
}

interface ServiceFactory {
    Service getService();
}

class Implementation1 implements Service {
    private Implementation1() {}

    @Override
    public void method1() {
        System.out.println("Implementation1 method1");
    }

    @Override
    public void method2() {
        System.out.println("Implementation1 method2");
    }

    // jdk1.8之後,可以使用lambda表示式來簡寫: () -> new Implementation1();
    public static ServiceFactory factory = new ServiceFactory() {
        @Override
        public Service getService() {
            return new Implementation1();
        }
    };
}

class Implementation2 implements Service {
    private Implementation2() {}

    @Override
    public void method1() {
        System.out.println("Implementation2 method1");
    }

    @Override
    public void method2() {
        System.out.println("Implementation2 method2");
    }

    // jdk1.8之後,可以使用lambda表示式來簡寫: () -> new Implementation2();
    public static ServiceFactory factory = new ServiceFactory() {
        @Override
        public Service getService() {
            return new Implementation2();
        }
    };
}

public class Factories {
    public static void serviceConsumer(ServiceFactory factory) {
        Service s = factory.getService();
        s.method1();
        s.method2();
    }

    public static void main(String[] args) {
        serviceConsumer(Implementation1.factory);
        serviceConsumer(Implementation2.factory);
    }
}

為什麼需要內部類?

  1. 內部類提供了某種進入其外圍類的視窗
  2. 每個內部類對能獨立地繼承自一個(介面的)實現,所以無論外圍類是否已經繼承了某個(介面得)實現,對於內部類都沒影響。
  3. 介面解決了部分問題,而內部類有效地實現了“多重繼承”。也就是說,內部類允許繼承多個非介面型別(類或抽象類)
    示例如下:
class D {}
abstract class E {}
class Z extends D {
    E makeE() {
        return new E() {};
    }
}

public class MultiImplementation {
    static void taskD(D d) {};
    static void taskE(E e) {};

    public static void main(String[] args) {
        Z z = new Z();
        taskD(z);
        taskE(z.makeE());
    }
}

閉包與回撥:閉包是一個可呼叫的物件,它記錄了一些資訊,這些資訊來自於建立它的作用域。通過這個定義,可以看出內部類是物件導向的閉包,因為它不僅包含外圍類物件(建立內部類的作用域)的資訊,還自動擁有一個指向外圍類物件的引用,在此作用域內,內部類有權操作所有的成員,包括private成員。

回撥:通過回撥,物件能夠攜帶一些資訊,這些資訊允許它在稍後的某個時刻呼叫初始的物件。在C/C++中回撥通過指標實現,由於Java中沒有包括指標,但我們可以通過內部類提供閉包的功能來實現,如下例:

interface Incrementable {
    void increment();
}

class Callee1 implements Incrementable {
    private int i = 0;

    @Override
    public void increment() {
        i++;
        System.out.println(i);
    }
}

class MyIncrement {
    public void increment() {
        System.out.println("Other operation");
    }

    static void f(MyIncrement mi) {
        mi.increment();
    }
}

class Callee2 extends MyIncrement {
    private int i = 0;

    @Override
    public void increment() {
        super.increment();
        i++;
        System.out.println(i);
    }

    private class Closure implements Incrementable {

        @Override
        public void increment() {
            Callee2.this.increment();
        }
    }

    Incrementable getCallBackReference () {
        return new Closure();
    }
}

class Caller {
    private Incrementable callbackReference;
    Caller(Incrementable cbh) {
        callbackReference = cbh;
    }
    void go() {
        callbackReference.increment();
    }
}

public class Callbacks {
    public static void main(String[] args) {
        Callee1 c1 = new Callee1();
        Callee2 c2 = new Callee2();
        MyIncrement.f(c2);
        Caller caller1 = new Caller(c1);
        Caller caller2 = new Caller(c2.getCallBackReference());
        caller1.go();
        caller1.go();
        caller2.go();
        caller2.go();
    }
}
/** outpput:
 * Other operation
 * 1
 * 1
 * 2
 * Other operation
 * 2
 * Other operation
 * 3
 */

  限於篇幅,本文先對前10章進行記錄,《Java程式設計思想》這本書在講解封裝、繼承、多型、介面和內部類時,寫了很多有助於我們理解的示例程式碼,其中也用到了很多設計模式,目前已經提及到的設計模式有:單例模式、策略模式、介面卡模式、代理模式,命令模式、模板方法模式以及工廠方法等示例程式碼。

相關文章