主頁 > 後端開發 > 《Java編程思想》讀書筆記一

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

2022-01-27 06:25:57 後端開發

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

本文原創自博客園文章,想了解Java相關知識,歡迎到我的博客踩踩~ 地址:https://www.cnblogs.com/reminis/

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/421603.html

標籤:Java

上一篇:如何防止元素的邊框在Chrome中消耗其填充?

下一篇:Java byte[]與List轉換工具 | Java工具類

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more