如何講清楚 Java 物件導向的問題與知識?(類與物件,封裝,繼承,多型,介面,內部類...)

BWH_Steven發表於2021-01-28

寫在最前面

這個專案是從20年末就立好的 flag,經過幾年的學習,回過頭再去看很多知識點又有新的理解。所以趁著找實習的準備,結合以前的學習儲備,建立一個主要針對應屆生和初學者的 Java 開源知識專案,專注 Java 後端面試題 + 解析 + 重點知識詳解 + 精選文章的開源專案,希望它能伴隨你我一直進步!

說明:此專案我確實有很用心在做,內容全部是我參考了諸多博主(已註明出處),資料,N本書籍,以及結合自己理解,重新繪圖,重新組織語言等等所制。個人之力綿薄,或有不足之處,在所難免,但更新/完善會一直進行。大家的每一個 Star 都是對我的鼓勵 !希望大家能喜歡。

注:所有涉及圖片未使用網路圖床,文章等均開源提供給大家。

專案名: Java-Ideal-Interview

Github 地址: Java-Ideal-Interview - Github

Gitee 地址:Java-Ideal-Interview - Gitee(碼雲)

持續更新中,線上閱讀將會在後期提供,若認為 Gitee 或 Github 閱讀不便,可克隆到本地配合 Typora 等編輯器舒適閱讀

若 Github 克隆速度過慢,可選擇使用國內 Gitee 倉庫

二 Java物件導向

1. 類和物件

1.1 什麼是程式導向?什麼又是物件導向?

程式導向——步驟化

  • 程式導向就是分析出實現需求所需要的步驟,通過函式(方法)一步一步實現這些步驟,接著依次呼叫即可

物件導向——行為化(概念相對抽象,可結合下面的例子理解)

  • 物件導向是把整個需求按照特點、功能劃分,將這些存在共性的部分封裝成類(類例項化後才是物件),建立了物件不是為了完成某一個步驟,而是描述某個事物在解決問題的步驟中的行為

1.1.1 能舉個例子談談你對程式導向和麵向物件的理解嗎

例如我們設計一個桌球遊戲(略過開球,只考慮中間過程)

A:程式導向方式思考:

把下述的步驟通過函式一步一步實現,這個需求就完成了。(只為演示概念,不細究邏輯問題)。

① palyer1 擊球 —— ② 實現畫面擊球效果 —— ③ 判斷是否進球及有效 —— ④ palyer2擊球

⑤ 實現畫面擊球效果 —— ⑥ 判斷是否進球及有效 —— ⑦ 返回步驟 1—— ⑧ 輸出遊戲結果

B:物件導向方式思考:

經過觀察我們可以看到,其實在上面的流程中存在很多共性的地方,所以我們將這些共性部分全集中起來,做成一個通用的結構

  1. 玩家系統:包括 palyer1 和 palyer2

  2. 擊球效果系統:負責展示給使用者遊戲時的畫面

  3. 規則系統:判斷是否犯規,輸贏等

我們將繁瑣的步驟,通過行為、功能,模組化,這就是物件導向,我們甚至可以利用該程式,分別快速實現8球和斯諾克的不同遊戲(只需要修改規則、地圖和球色即可,玩家系統,擊球效果系統都是一致的)

1.1.2 程式導向和麵向物件的優缺點

A:程式導向

優點:效能上它是優於物件導向的,因為類在呼叫的時候需要例項化,開銷過大。

缺點:不易維護、複用、擴充套件

用途:微控制器、嵌入式開發、Linux/Unix等對效能要求較高的地方

B:物件導向

優點:易維護、易複用、易擴充套件,由於物件導向有封裝繼承多型性的特性,可以設計出低耦合的系統,使系統更加靈活、更加易於維護

缺點:一般來說效能比程式導向低

低耦合:簡單的理解就是說,模組與模組之間儘可能的獨立,兩者之間的關係儘可能簡單,儘量使其獨立的完成成一些子功能,這避免了牽一髮而動全身的問題。這一部分我們會在物件導向學習結束後進行系統的整理和總結。

總結:只通過教科書後的例題是無法體會到程式導向所存在的問題的,在一些小例程中,程式導向感覺反而會更加的簡單,但是一旦面臨較大的專案,我們需要編寫N個功能相似的函式,函式越來越多,程式碼量越來越多,你就知道這是一場噩夢了。

說明:關於效能的問題,這裡只是在籠統意義上來說,具體效能優劣,需要結合具體程式,環境等進行比對

1.2 說一說類、物件、成員變數和成員方法的關係和理解

:一組相關的屬性和行為的集合,是一個抽象的概念。

物件:該類事物的具體表現形式,具體存在的個體。

成員變數:事物的屬性

成員方法:事物的行為

上面我們說了這幾個概念,那麼到底應該怎麼理解呢?

類就是對一些具有共性特徵,並且行為相似的個體的描述。

比如小李和老張都有姓名、年齡、身高、體重等一些屬性,並且兩人都能夠進行聊天、運動等相似的行為

由於這兩個人具有這些共性的地方,所以我們把它抽象出來,定義為一個——人類,而小李、老王正是這個類中的個體(物件),而每一個個體才是真正具體的存在,光提到人類,你只知道應該有哪些屬性行為,但你不知道他具體的一些屬性值,比如你知道他屬於 “人類” 所以他應該擁有姓名,年齡等屬性,但你並不知道他具體叫什麼,年齡多大了。而小李和老王這兩個具體的物件,卻能夠實實在在的知道老王今年30歲了、身高175等值。

所以可以得出結果:類是物件的抽象,而物件是類的具體例項。類是抽象的,不佔用記憶體,而真正根據類例項化出具體的物件,就需要佔用記憶體空間了。

1.3 成員變數和區域性變數有什麼區別?

A:在類中的位置不同

  • 成員變數:類中方法外

  • 區域性變數:程式碼塊,方法定義中或者方法宣告上(方法引數)

B:在記憶體中的位置不同

  • 成員變數:在堆中

  • 區域性變數:在棧中

C:生命週期不同

  • 成員變數:隨著物件的建立而存在,隨著物件的消失而消失

  • 區域性變數:隨著方法的呼叫而存在,隨著方法的呼叫完畢而消失

D:初始化值不同

  • 成員變數:有預設值(構造方法對它的值進行初始化)

  • 區域性變數:沒有預設值,必須定義,賦值,然後才能使用

1.3.1 為什麼區域性變數存在於棧中而不是堆中

有一個問題,在我們學習 Java 中記憶體分配的時候,有這樣一句話,“堆記憶體用來存放 new 建立的物件和陣列”。 換句話說物件存在於堆中,而成員變數又存在於類中,而且物件是類具體的個體,所以成員變數也存在於堆中,那麼問題就來了,同理,是不是方法也和成員變數一樣存在於物件中,而區域性變數又定義在方法中,豈不就是說,區域性變數也存在於堆中呢?這明顯與我們上面的定義有區別

解釋:一個類可以建立 n 個不同的物件,當我們 new 一個物件後,這個物件實體,已經在堆上分配了記憶體空間,由於類的成員變數在不同的物件中各不相同(例如,小李和老王的姓名不同),都需要自己各自的儲存空間,所以類的成員變數會隨著物件儲存在堆中,而由於類的方法是所有物件通用的,所以建立物件時,方法還未出現,只有宣告,方法裡面的區域性變數也並沒有被建立,只有等到物件使用方法的時候才會被壓入棧。

補充:類變數(靜態變數)存在於方法區,引用型別的區域性變數宣告在棧,儲存在堆

1.4 訪問許可權修飾符 public、private、protected, 以及不寫(預設)時的區別

訪問許可權 子類 其他包
public
protect
default
private
  • public:公共的,可以被專案中所有的類訪問。
  • protected:受保護的,可以被這個類本身訪問;被同一個包中的類訪問;被它的子類(同一個包以及不同包中的子類)訪問。
  • default:預設的,可以被這個類本身訪問;被同一個包中的類訪問。
  • private:私有的,只能被這個類本身訪問。

1.5 類在初始化的時候做了些什麼?

public class Student {
    private String name = "BWH_Steven";
    private Integer age = 22;
    
    // 是個無參構造,為了演示初始化順序,特意加了兩個賦值語句
    public Student (){ 
        name = "阿文";
        age = 30;
    }
}

public class Test {
    public static void main(String[] args) {
        Student stu = new Student(); 
    }
}

例如: Student stu = new Student(); 其在記憶體中做了如下的事情:

首先載入 Student.class (編譯成位元組碼檔案)檔案進記憶體,在棧記憶體為 stu 變數開闢一塊空間,在堆記憶體為 Student 類例項化出的學生物件開闢空間,對學生物件的成員變數進行預設初始化(例如 name = null,age = 0 ),對學生物件的成員變數進行顯示初始化( 例如name = "BWH_Steven",age = 22),接著就會通過構造方法對學生物件的成員變數賦值(執行建構函式內,我們特意加的賦值語句 name = "阿文",age = 30)學生物件初始化完畢,把物件地址賦值給 stu 變數

1.6 static 關鍵字修飾的作用?

static方法就是沒有this的方法。在static方法內部不能呼叫非靜態方法,反過來是可以的。而且可以在沒有建立任何物件的前提下,僅僅通過類本身來呼叫static方法。這實際上正是static方法的主要用途。 —— 《Java程式設計思想》P86

可以知道,被 static 關鍵字修飾的方法或者變數不需要依賴於物件來進行訪問,只要類被載入了,就可以通過類名去進行訪問。也就是說,即使沒有建立物件也可以進行呼叫(方法/變數)

static可以用來修飾類的成員方法、類的成員變數,另外可以編寫static程式碼塊來優化程式效能。

1.6.1 什麼是靜態方法

static 修飾的方法一般叫做靜態方法,靜態方法不依賴於物件訪問,因此沒有 this 的概念(this 代表所在類的物件引用),正因如此靜態方法能夠訪問的成員變數和成員方法也都必須是靜態的

  • 例如在靜態方法 A 中 呼叫了非靜態成員 B,如果通過 類名.A 訪問靜態方法 A,此時物件還不存在,非靜態成員 B 自然也根本不存在,所以就會有問題。呼叫非靜態方法 C 也是如此,你不清楚這個方法 C 中是否呼叫了費靜態變數

1.6.2 什麼是靜態變數

static 修飾的變數也稱作靜態變數,靜態變數屬於類,所以也稱為類變數,儲存於方法區中的靜態區,隨著類的載入而載入,消失而消失,可以通過類名呼叫,也可以通過物件呼叫。

1.6.3 什麼是 靜態程式碼塊

靜態程式碼塊是在類中(方法中不行)使用static關鍵字和{} 宣告的程式碼塊

static {
	... 內容
}

執行: 靜態程式碼塊在類被載入的時候就執行了,而且只執行一次,並且優先於各種程式碼塊以及建構函式。

作用: 一般情況下,如果有些程式碼需要在專案啟動的時候就執行,這時候 就需要靜態程式碼塊。比如一個專案啟動需要載入的 很多配置檔案等資源,我們就可以都放入靜態程式碼塊中。

1.6.3.1 構造程式碼塊(補充)

概念:在java類中使用{}宣告的程式碼塊(和靜態程式碼塊的區別是少了static關鍵字)

執行: 構造程式碼塊在建立物件時被呼叫,每次建立物件都會呼叫一次,但是優先於建構函式執行。

作用 :和建構函式的作用類似,都能對物件進行初始化,並且只建立一個物件,構造程式碼塊都會執行一次。但是反過來,建構函式則不一定每個物件建立時都執行(多個建構函式情況下,建立物件時傳入的引數不同則初始化使用對應的建構函式)。

因為每個構造方法執行前, 首先執行構造程式碼塊,所以可以把多個構造方法中相同的程式碼可以放到這裡,

2. 物件導向三大特徵

2.1 封裝

封裝的概念

封裝是指隱藏物件的屬性和實現細節,僅對外提供公共訪問方式

  • 簡單的來說就是我將不想給別人看的資料,以及別人無需知道的內部細節, “鎖起來” ,我們只留下一些入口,使其與外部發生聯絡。

我們如何給我們的資料 “上鎖” 呢?

  • 我們使用,public、private、protected 等許可權修飾符 在類的內部設定了邊界,這些不同程度的 ”鎖“ 決定了緊跟其後被定義的東西能夠被誰使用。

封裝的好處

隱藏實現細節,提供公共的訪問方式,提高了程式碼的複用性,提高安全性

好處1:隱藏實現細節,提供公共的訪問方式

隱藏實現細節怎麼理解呢?

  • 我們將一些功能封裝到類中,而客戶端的程式設計師,不需要知道類中的這個方法的邏輯原理,類程式設計師只需要給他一個對外的介面,客戶端程式設計師只需要能夠呼叫這個方法即可,

  • 例如:夏天宿舍很熱,我們(使用者)只需要操作遙控器即可使用空調,並不需要了解空調內部是如何執行的

提供公共的訪問方式又怎麼理解呢?

我們先來看一段標準案例

public class Student {
	//定義成私有成員變數(private)
    private String name;
    private int age;
	
    //無參構造
    public Student() {
        super();
    }

    //帶參構造
    public Student(String name, int age) {
        super();
        this.name = name;
        this.age = age;
    }
	
    //成員變數的set和get方法(與外界聯絡的橋樑)
    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public int getAge() {
        return age;
    }
}
public class StudentTest {
    public static void main(String[] args) {
        //建立學生類物件 s
        Student s = new Student();
            
        //物件s呼叫類中的公共方法setName()和setAge()
        //set方法給成員變數賦值
        s.setName("BWH_Steven");
        s.setAge(20);
        
        //get方法獲取成員變數
        System.out.println(s.getName() + s.getAge());
    }
}

我們可以看到在上面的案例中,成員變數都使用 private 修飾,而下面的 get 和 set 方法均使用了public修飾,其實被private修飾的屬性就是我們想要鎖起來的資料,而 set、get 方法就是我們開啟這把鎖的鑰匙

被private所修飾的內容是,除型別建立者和型別的內部方法之外的任何人都不能訪問的元素,所以我們這些資料就被我們通過private “鎖” 了起來,而我們外界是可以通過建立物件來呼叫一個類中的公共方法的,所以被 public修飾的 set 和 get 方法外界所能訪問的,而這兩個方法又可以直接訪問我們的私有成員變數,所以 set 和 get 方法就成為了私有成員與外界溝通的鑰匙。

好處2:提高了程式碼的複用性

功能被封裝成了類,通過基類與派生類之間的一些機制(組合和繼承),來提高程式碼的複用性

好處3:提高安全性(此處待修改)

關於安全性的問題,實際上還是存在爭議的,我們先看一種說法:

public class Student {

    private String name;
    private int age;

    public Student() {
        super();
    }

    public Student(String name, int age) {
        super();
        this.name = name;
        this.age = age;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    //在setAge()方法中加入了校驗的內容
    //不合法資料是不允許傳遞給成員變數的
    public void setAge(int age) {
        if (age < 0 || age > 120) {
            System.out.println("Error");
        }else {
            this.age = age;
        }     
    }

    public int getAge() {
        return age;
    }
}

public class StudentTest {
    public static void main(String[] args) {
        Student s = new Student();
        System.out.println(s.getName() + s.getAge());
        
        //錯誤的方式!!!
        s.name = "BWH_Steven";
        s.age = 20;
        System.out.println(s.getName() + s.getAge());
        
       	//正確的方式!!!
        s.setName("BWH_Steven");
        s.setAge(20);
    }
}

我們用private來修飾我們的成員變數不是沒有任何依據的,如果我們的成員變數修飾符改為public,這句意味著外界可以使用物件直接訪問,修改這個成員變數,這可能會造成一些重大的問題

例如:外界通過物件去給成員變數賦值,可以賦值一些非法的資料,這明顯是不合理的。所以在賦值之前應該先對資料進行判斷。StudenTest 是一個測試類,測試類一般只建立物件,呼叫方法,所以這個判斷應該定義在Student類中。需要使用邏輯語句,而邏輯語句應該定義在方法中。所以在Student類中提供一個方法來對資料進行校驗但是如果偏偏不呼叫方法來賦值,還是通過 物件名.變數 直接賦值,這樣我們的方法內的邏輯就沒有起作用所以我們必須強制要求使用我的方法,而不能直接呼叫成員變數這也正是我們使用 private 修飾成員變數的原因!

注:此處舉例為 JavaBean 類,一般很少在 set get 中去新增一些邏輯,一般都是一種簡單的賦值,而且諸多框架和不錯的專案均使用了這種規範方法。

2.2 繼承

繼承就是在一個已有類的基礎上派生出新類(例如動物類可以派生出狗類和貓類),子類繼承父類的特徵和行為,使得子類物件(例項)具有父類的例項域和方法,或子類從父類繼承方法,使得子類具有父類相同的行為

提高了程式碼的複用性,提高了程式碼的維護性(通過少量的修改,滿足不斷變化的具體要求),讓類與類產生了一個關係,是多型的前提。但是缺點也很顯著:讓類的耦合性增強,這樣某個類的改變就會影響其他和該類相關的類。

特點:Java只支援單繼承,不支援多繼承(C++支援),但是Java支援多層繼承(繼承體系)形象的說就是:兒子繼承爸爸,爸爸繼承爺爺,兒子可以通過爸爸繼承爺爺。

注意:

A: 子類只能繼承父類所有非私有成員(成員方法和成員變數)

B:子類不能繼承父類的構造方法,但是可以通過super關鍵字去訪問方法

C: 不要為了部分功能而繼承(多層繼承會使得子類繼承多餘的方法)

2.3 多型

多型是同一個行為具有多個不同表現形式或形態的能力,例如:黑白印表機和彩色印表機相同的列印行為卻有著不同的列印效果,

  • 物件型別和引用型別之間存在著繼承(類)/ 實現(介面)的關係;

  • 當使用多型方式呼叫方法時,首先檢查父類中是否有該方法,如果沒有,則編譯錯誤;如果有,再去呼叫子類的同名方法。

  • 如果子類重寫了父類的方法,最終執行的是子類覆蓋的方法,如果沒有則執行的是父類的方法。

3. 其他

3.3 抽象類和介面

3.3.1 談談你對抽象類和介面的認識

抽象類:我們建立一個動物類,並且在這個類中建立動物物件,但是當你提到動物類,你並不知道我說的是什麼動物,只有看到了具體的動物,你才知道這是什麼動物,所以說動物本身並不是一個具體的事物,而是一個抽象的事物。只有真正的貓,狗才是具體的動物,同理我們也可以推理不同的動物,他們的行為習慣應該是不一樣的,所以我們不應該在動物類中給出具體體現,而是給出一個宣告即可。

介面:常見的貓狗案例,貓和狗它們僅僅提供一些基本功能。但有一些不是動物本身就具備的,比如:貓鑽火圈,狗跳高等功能是在後面的培養中訓練出來的,這種額外的功能,java提供了介面表示。

3.3.1.1 為什麼抽象類必須重寫所有抽象方法

“貓”和“狗”都是“動物”這個類的實體,比如動物都有eat() 這個方法,但是狗是吃肉的,貓是吃魚的。所以每個動物關於具體吃的方式是需要在子類中重寫的,不然的話,狗和貓不就一樣了嗎?

// Animal類

public abstract class Animal {

    public void sleep() {
        System.out.println("我趴著睡");
    }
    public abstract void eat(); 
}
// Dog類
public class Dog extends Animal {

    public Dog() {
        super();
    }
    
    @Override
    public void eat() {
        System.out.println("我實現了父類方法,狗吃肉");
    }
}
// Cat類
public class Cat extends Animal{
    public Cat() {
        super();
    }

    @Override
    public void eat() {
        System.out.println("我實現了父類方法,貓吃魚");
    }
}
// 測試類
public class AnimalTest {
    public static void main(String[] args) {
        Animal a1 = new Dog();
        a1.sleep();
        a1.eat();
        System.out.println("-------------------------");
        Animal a2 = new Cat();
        a2.sleep();
        a2.eat();
    }
}
執行結果:
我趴著睡
我實現了父類方法,狗吃肉
-------------------------
我趴著睡
我實現了父類方法,貓吃魚

通過上面的例子我們可以看到,Dog 和 Cat 兩個子類繼承 Animal,兩者 sleep() 方法是一樣的均繼承於 Animal 類,而 eat() 方法由於特性不同則在 Animal 類中定義為抽象方法,分別在子類中實現。

3.3.2 抽象類和介面的區別(重要)

我們從我們實際設計場景中來切入這個話題

先來舉一個簡單的例子:

狗都具有 eat() 、sleep() 方法,我們分別通過抽象類和介面定義這個抽象概念

// 通過抽象類定義
public abstract class Dog {
	public abstract void eat();
	public abstract void sleep();  
}
// 通過介面定義
public interface Dog {
    public abstract void eat();
    public abstract void sleep();
}

但是我們現在如果需要讓狗擁有一項特殊的技能——鑽火圈 DrillFireCircle(),如何增加這個行為呢?

思考:

  1. 將鑽火圈方法與前面兩個方法一同寫入抽象類中,但是這樣的話,但凡繼承這個抽象類狗都具有了鑽火圈技能,明顯不合適

  2. 將鑽火圈方法與前面兩個方法一同寫入介面中,當需要使用鑽火圈功能的時候,就必須實現 介面中的eat() 、sleep() 方法(重寫該介面中所有的方法)顯然也不合適

那麼該如何解決呢 ? 我們可以仔細想一想,eat和sleep都是狗本身所應該具有的一種行為,而鑽火圈這種行為則是後天訓練出來的,只能算是對狗類的一種附加或者延伸, 兩者不應該在同一個範疇內,所以我們考慮將這個單獨的行為,獨立的設計一個介面,其中包含DrillFireCircle()方法, Dog設計為一個抽象類, 其中又包括eat() 、sleep() 方法。

一個SpecialDog即可繼承Dog類並且實現DrillFireCircle()介面

下面給出程式碼:

// 定義介面,含有鑽火圈方法
public interface DrillFireCircle() {
    public abstract void drillFireCircle();
}

// 定義抽象類狗類
public abstract class Dog {
    public abstract void eat();
    public abstract void sleep();
}
 
// 繼承抽象類且實現介面
class SpecialDog extends Dog implements drillFireCircle {
    public void eat() {
      // ....
    }
    public void sleep() {
      // ....
    }
    public void drillFireCircle() () {
      // ....
    }
}

總結:繼承是一個 "是不是"的關係,而 介面 實現則是 "有沒有"的關係。如果一個類繼承了某個抽象類,則子類必定是抽象類的種類,而介面實現則是有沒有、具備不具備的關係,比如狗是否能鑽火圈,能則可以實現這個介面,不能就不實現這個介面。

3.4 談談幾種內部類和使用內部類的原因

3.4.1 幾種內部類

概述:把類定義在另一個類的內部,該類就被稱為內部類。

舉例:把類 Inner 定義在類 Outer 中,類 Inner 就被稱為內部類。

class Outer {
    class Inner {
    }
}

訪問規則:內部類可以直接訪問外部類的成員,包括私有。外部類要想訪問內部類成員,必須建立物件

內部類的分類:A:成員內部類、B:區域性內部類、C:靜態內部類、D:匿名內部類

3.4.1.1 成員內部類

成員內部類——就是位於外部類成員位置的類

特點:可以使用外部類中所有的成員變數和成員方法(包括private的)

A:格式

class Outer {
    private int age = 20;
    // 成員位置
    class Inner {
        public void show() {
            System.out.println(age);
        }
    }
}

class Test {
    public static void main(String[] ages) {
        // 成員內部類是非靜態的演示
        Outer.Inner oi = new Outer().new Inner();
        oi.show();
    }
}

B:建立物件時:

// 成員內部類不是靜態的:
外部類名.內部類名 物件名 = new 外部類名.new 內部類名();

// 成員內部類是靜態的:
外部類名.內部類名 物件名 = new 外部類名.內部類名();	

C:成員內部類常見修飾符:

a:private

如果我們的內部類不想輕易被任何人訪問,可以選擇使用private修飾內部類,這樣我們就無法通過建立物件的方法來訪問,想要訪問只需要在外部類中定義一個public修飾的方法,間接呼叫。這樣做的好處就是,我們可以在這個public 方法中增加一些判斷語句,起到資料安全的作用。

class Outer {
    private class Inner {
        public void show() {
            System.out.println(“密碼備份檔案”);
        }
    }
    
    public void method() {
    	if(你是管理員){
    		Inner i = new Inner();
    		i.show();
    	}else {
    		System.out.println(“你沒有許可權訪問”);
    	}
   	}
}

下面我們給出一個更加規範的寫法

class Outer {
    private class Inner {
        public void show() {
            System.out.println(“密碼備份檔案”);
        }
    }
    // 使用getXxx()獲取成員內部類,可以增加校驗語句(文中省略)
    public Inner getInner() {
		return new Inner();
   	}
    
    public static void main(String[] args) {
    	Outer outer = new Outer();
        Outer.Inner inner = outer.getInner();
        inner.show();
    }
}

b:static

這種被 static 所修飾的內部類,按位置分,屬於成員內部類,但也可以稱作靜態內部類,也常叫做巢狀內部類。具體內容我們在下面詳細講解。

D:成員內部類經典題(填空)

請在三個println 後括號中填空使得輸出25,20,18

class Outer {
	public int age = 18;	
	class Inner {
		public int age = 20;	
		public viod showAge() {
			int age  = 25;
			System.out.println(age);//空1
			System.out.println(this.age);//空2
			System.out.println(Outer.this.age);//空3
		}
	}
} 

3.4.1.2 區域性內部類

區域性內部類 —— 就是定義在一個方法或者一個作用域裡面的類

特點:主要是作用域發生了變化,只能在自身所在方法和屬性中被使用

A 格式:

class Outer {
    public void method(){
        class Inner {
        }
    }
}

B:訪問時:

// 在區域性位置,可以建立內部類物件,通過物件呼叫和內部類方法
class Outer {
    private int age = 20;
    public void method() {
        final int age2 = 30;
        class Inner {
            public void show() {
           	    System.out.println(age);
                // 從內部類中訪問方法內變數age2,需要將變數宣告為最終型別。
                System.out.println(age2);
            }
        }
        
        Inner i = new Inner();
        i.show();
    }
}

C: 為什麼區域性內部類訪問區域性變數必須加final修飾呢?

因為區域性變數是隨著方法的呼叫而呼叫,使用完畢就消失,而堆記憶體的資料並不會立即消失。

所以,堆記憶體還是用該變數,而該變數已經沒有了。為了讓該值還存在,就加final修飾。

原因是,當我們使用final修飾變數後,堆記憶體直接儲存的是值,而不是變數名。

(即上例 age2 的位置儲存著常量30 而不是 age2 這個變數名)

3.4.1.3 靜態內部類

我們所知道 static 是不能用來修飾類的,但是成員內部類可以看做外部類中的一個成員,所以可以用 static 修飾,這種用 static 修飾的內部類我們稱作靜態內部類,也稱作巢狀內部類。

特點:不能使用外部類的非static成員變數和成員方法

解釋:非靜態內部類編譯後會預設的儲存一個指向外部類的引用,而靜態類卻沒有。

簡單理解:即使沒有外部類物件,也可以建立靜態內部類物件,而外部類的非static成員必須依賴於物件的呼叫,靜態成員則可以直接使用類呼叫,不必依賴於外部類的物件,所以靜態內部類只能訪問靜態的外部屬性和方法。

class Outter {
    int age = 10;
    static age2 = 20;
    public Outter() {        
    }
     
    static class Inner {
        public method() {
            System.out.println(age);//錯誤
            System.out.println(age2);//正確
        }
    }
}

public class Test {
    public static void main(String[] args)  {
        Outter.Inner inner = new Outter.Inner();
        inner.method();
    }
}

3.4.1.4 匿名內部類

一個沒有名字的類,是內部類的簡化寫法

A 格式:

new 類名或者介面名() {
    重寫方法();
}

本質:其實是繼承該類或者實現介面的子類匿名物件

這也就是下例中,可以直接使用 new Inner() {}.show(); 的原因等於 子類物件.show();

interface Inter {
	public abstract void show();
}

class Outer {
    public void method(){
        new Inner() {
            public void show() {
                System.out.println("HelloWorld");
            }
        }.show();
    }
}

class Test {
	public static void main(String[] args)  {
    	Outer o = new Outer();
        o.method();
    }
}    

如果匿名內部類中有多個方法又該如何呼叫呢?

Inter i = new Inner() {  //多型,因為new Inner(){}代表的是介面的子類物件
	public void show() {
		System.out.println("HelloWorld");
	}
};

B:匿名內部類在開發中的使用

我們在開發的時候,會看到抽象類,或者介面作為引數。

而這個時候,實際需要的是一個子類物件。

如果該方法僅僅呼叫一次,我們就可以使用匿名內部類的格式簡化。

3.4.2 為什麼使用內部類

3.4.2.1 封裝性

作為一個類的編寫者,我們很顯然需要對這個類的使用訪問者的訪問許可權做出一定的限制,我們需要將一些我們不願意讓別人看到的操作隱藏起來,

如果我們的內部類不想輕易被任何人訪問,可以選擇使用private修飾內部類,這樣我們就無法通過建立物件的方法來訪問,想要訪問只需要在外部類中定義一個public修飾的方法,間接呼叫。

public interface Demo {
    void show();
}
class Outer {
    private class test implements Demo {
        public void show() {
            System.out.println("密碼備份檔案");
        }
    }
    
    public Demo getInner() {
        return new test();
    }
    
}

我們來看其測試

    public static void main(String[] args) {
    	Outer outer = new Outer();
        Demo d = outer.getInner();
        i.show();
    }

//執行結果
密碼備份檔案

這樣做的好處之一就是,我們可以在這個public方法中增加一些判斷語句,起到資料安全的作用。

其次呢,我們的對外可見的只是 getInner() 這個方法,它返回了一個Demo介面的一個例項,而我們真正的內部類的名稱就被隱藏起來了

3.4.2.1 實現多繼承

我們之前的學習知道,java是不可以實現多繼承的,一次只能繼承一個類,我們學習介面的時候,有提到可以用介面來實現多繼承的效果,即一個介面有多個實現,但是這裡也是有一點弊端的,那就是,一旦實現一個介面就必須實現裡面的所有方法,有時候就會出現一些累贅,但是使用內部類可以很好的解決這些問題

public class Demo1 {
    public String name() {
        return "BWH_Steven";
    }
}
public class Demo2 {
    public String email() {
        return "xxx.@163.com";
    }
}
public class MyDemo {

    private class test1 extends Demo1 {
        public String name() {
            return super.name();
        }
    }

    private class test2 extends Demo2  {
        public String email() {
            return super.email();
        }
    }

    public String name() {
        return new test1().name();
    }

    public String email() {
        return new test2().email();
    }

    public static void main(String args[]) {
        MyDemo md = new MyDemo();
        System.out.println("我的姓名:" + md.name());
        System.out.println("我的郵箱:" + md.email());
    }
}

我們編寫了兩個待繼承的類 Demo1 和 Demo2,在 MyDemo類中書寫了兩個內部類,test1 和test2 兩者分別繼承了Demo1 和 Demo2 類,這樣 MyDemo 中就間接的實現了多繼承

3.4.2.3 用匿名內部類實現回撥功能

我們用通俗講解就是說在Java中,通常就是編寫一個介面,然後你來實現這個介面,然後把這個介面的一個物件作以引數的形式傳到另一個程式方法中, 然後通過介面呼叫你的方法,匿名內部類就可以很好的展現了這一種回撥功能

public interface Demo {
    void demoMethod();
}
public class MyDemo{
    public test(Demo demo){
    	System.out.println("test method");
    }
    
    public static void main(String[] args) {
        MyDemo md = new MyDemo();
        //這裡我們使用匿名內部類的方式將介面物件作為引數傳遞到test方法中去了
        md.test(new Demo){
            public void demoMethod(){
                System.out.println("具體實現介面")
            }
        }
    }
}

3.4.2.4 解決繼承及實現介面出現同名方法的問題

public interface Demo {
    void test();
}
public class MyDemo {

    public void test() {
        System.out.println("父類的test方法");
    }
    
}
public class DemoTest extends MyDemo implements Demo {
    public void test() {
    }
}

這樣的話我就有點懵了,這樣如何區分這個方法是介面的還是繼承的,所以我們使用內部類解決這個問題

public class DemoTest extends MyDemo {

    private class inner implements Demo {
        public void test() {
            System.out.println("介面的test方法");
        }
    }
    
    public Demo getIn() {
        return new inner();
    }
    
    
    public static void main(String[] args) {
        //呼叫介面而來的test()方法
        DemoTest dt = new DemoTest();
        Demo d = dt.getIn();
        d.test();
        
        //呼叫繼承而來的test()方法
        dt.test();
    }
}

//執行結果
介面的test方法
父類的test方法

相關文章