《Java編程思想》讀書筆記一

很早之前就買了《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編程思想》這本書在講解封裝、繼承、多態、接口和內部類時,寫了很多有助於我們理解的示例代碼,其中也用到了很多設計模式,目前已經提及到的設計模式有:單例模式、策略模式、適配器模式、代理模式,命令模式、模板方法模式以及工廠方法等示例代碼。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章