Java中的三大特性 – 超詳細篇
前言
大家好啊,我是湯圓,今天給大家帶來的是《Java中的三大特性 – 超詳細篇》,希望對大家有幫助,謝謝
這一節的內容可能有點多,大家可以選擇性的來看
簡介
Java的三大特性:封裝、繼承、多態
乍一聽,好像很高大上,其實當你真正用的時候,會發現高大上的還在後面呢。。。
熱身
在正式講解三大特性之前,先普及幾個知識
1. 訪問許可權修飾符
Java中關於訪問許可權的四個修飾符,表格如下
private | friendly(默認) | protected | public | |
---|---|---|---|---|
當前類訪問許可權 | √ | √ | √ | √ |
包訪問許可權 | × | √ | √ | √ |
子類訪問許可權 | × | × | √ | √ |
其他類訪問許可權 | × | × | × | √ |
其中比較尷尬的是protected修飾符,有點卡在中間,不上不下的感覺
因為它不適合用來修飾屬性
假設用它修飾屬性,那麼任何一個人都可以通過繼承這個類,來直接訪問到這個類的屬性,從而破壞”封裝性”
2. 抽象類(abstract)
什麼是抽象類?
抽象類就是用abstract修飾,且不能被直接初始化的類,但是可以通過子類來初始化
比如:Father father = new Son()
對應的,抽象方法就是用abstract修飾的方法
抽象方法是一種很特殊的方法,它沒有方法體,即方法實現程式碼為空,比如abstract public void fun();
抽象方法一般在子類中進行實現,它就好像是在說:我不寫程式碼,我只是聲明一個方法名,剩下的交給我的子孫後代(繼承類)去做
抽象類有一個很重要的特點:抽象類可以沒有抽象方法,但是如果一個類有抽象方法,那麼這個類肯定是抽象類
為什麼會有抽象類
解耦,使程式碼結構更加清晰
因為抽象類不能被直接創建為對象,它只是作為一個通用介面來供別人實現和調用,所以這樣就使得抽象的程式碼更加清晰(它只聲明方法,不實現方法)
就好比,老闆和員工,老闆負責分發任務,員工負責去具體的實現任務
好了,關於抽象類,先介紹到這裡,更詳細的後面的章節再深入
3. 重載(overloading)和覆寫(overwriting)
重載和覆寫是兩個很容易混淆的概念
重載:同一個類中,一個方法的多種表現形式(參數類型不同,參數個數不同)
覆寫:繼承設計中,子類覆蓋父類的方法(也可以叫做重寫,不過這樣跟重載有點混淆,所以個人喜歡叫做覆寫)
他們之間的區別如下
重載 | 覆寫 | |
---|---|---|
訪問許可權 | 可以不同 | 可以不同(但是子類的可見性不能比父類的低) |
方法返回值 | 可以不同 | 相同 |
參數類型 | 不同(充分條件) | 相同 |
參數個數 | 不同(充分條件) | 相同 |
這裡要注意幾點
- 覆寫時,子類的方法訪問許可權不能低於父類,比如父類方法為public,那麼子類也只能為public
- 重載時,訪問許可權和方法返回值,不能作為用來判斷一個方法是否為重載的依據;只能說重載允許不同的訪問許可權和返回值
覆寫示範
程式碼示範如下,
// 覆寫一:正確示範
@Override
public void fun(){
System.out.println("son fun");
}
// 覆寫二:錯誤示範,訪問許可權低了
@Override
private void fun(){
// 報錯:'fun()' in 'SonDemo' clashes with 'fun()' in 'Father'; attempting to assign weaker access privileges ('private'); was 'public'
System.out.println("son fun");
}
@Override這個是幹嘛的?之前沒見過啊
這個修飾符用來說明這個方法是覆寫方法,不寫也可以,系統會自己識別方法是不是覆寫的
那為啥還要多此一舉呢?用系統默認的識別機制不好嗎?
要多此一舉;不好;
因為加了註解,程式碼可讀性更高,程式碼更加規範,別人看了程式碼後,立馬就知道這個方法是覆寫方法
重載示範
重載用圖展示可能會更加清晰
圖示解釋:
參數類型和參數個數,只要滿足其一,就可以說這個方法被重載了
訪問許可權和方法返回值用虛線框,是為了說明他們兩個只是重載的一個附加表現形式(可有可無),不能作為重載的判斷依據
下面用程式碼演示下
// 基礎方法
public void fun1(int a){
}
// 重載一:參數個數不同
public void fun1(){
}
// 重載二:參數類型不同
public void fun1(float a){
}
// 重載三:錯誤示範,僅僅用訪問許可權的不同來重載
private void fun1(int a){
// 編譯報錯:'fun1(int)' is already defined
}
// 重載四:錯誤示範,僅僅用返回值的不同來重載
public int fun1(int a){
// 編譯報錯:'fun1(int)' is already defined
return 0;
}
下面進入正文,開始順序介紹這三大特性
正文
1. 封裝(Encapsulation)
就是把類的屬性私有化(private修飾),再通過公有方法(public)進行訪問和修改
為什麼要封裝呢?
-
追蹤變化:可以在set方法中,編寫程式碼來追蹤屬性的改變記錄
public void setName(String name) { System.out.println("名字即將被修改"); System.out.println("舊名字:" + this.name); System.out.println("新名字:" + name); this.name = name; }
-
修改底層實現:在修改屬性名時,不會影響外部介面對屬性的訪問
比如:name屬性改為firstName和lastName,name就可以在get方法中修改返回值為firstName+lastName,對外介面沒變化
// 修改前 private String name; public String getName() { return name; } // 修改後 private String firstName; private String lastName; // 方法名不用變,只是方法內容作了修改 public String getName() { return firstName + lastName; }
-
校驗數據:可以在set方法中,校驗傳來的數據是否符合屬性值的設定範圍,防止無效數據的亂入
public void setAge(int age) throws Exception { if(age>1000 || age<0){ throw new Exception("年齡不符合規範,0~1000"); } this.age = age; }
2. 繼承(Inheritance)
如果子類繼承了父類,那麼子類就可以復用父類的方法和屬性,並且可以在此基礎上新增方法和屬性
這裡要注意的一點是:Java是單繼承語言,即每個類只能有一個父類
這裡還要普及一個常識:如果一個類沒有指定父類(即沒有繼承任何類),那麼這個類默認繼承Object類
為什麼要用繼承呢?
為了程式碼復用,減少重複工作
單繼承不會太局限嗎?為啥不用多繼承?
因為多繼承會導致”致命方塊”問題(因為像撲克牌的方塊符號)
- 比如A同時繼承B和C,然後B和C各自繼承D
- B和C各自覆寫了D的fun方法
- 那這時A該調用哪個類的fun方法呢
下面用圖來說話
那為什麼叫致命方塊,而不是致命三角形呢?那個D類好像是多餘的
不多餘
這個D類其實就是上面講到的抽象類的作用:將共有的部分fun()
抽象出來(或者提供一個基礎的實現),然後子類分別去實現各自的,這也是多態的一種體現(下面會將多態)
如果沒有D類,那麼B和C的fun()
就會存在重複程式碼,這時你可能就想要搞一個父類出來了,這個父類就是D類
那要怎麼判斷繼承類設計得好不好呢?
通過is-a關係來判斷
is-a關係指的是一個是另一個的關係,男人是人(說得通),人是男人(一半說得通)
用is-a關係可以很好地體現你的繼承類設計的好還是壞
- 如果子類都可以說是一個父類,那麼這個繼承關係設計的就很好(男人是人,is-a關係)
- 如果子類和父類只是包含或者引用的關係,那麼這個繼承關係就很糟糕(貓是貓籠,包含關係)
有沒有什麼辦法可以阻止類的繼承?就像private修飾符用來封裝屬性,其他人訪問不到一樣
有啊,final修飾符可以阻止類的繼承
這裡重點講一下final修飾符
final可以用來修飾屬性、方法、類,表示他們是常量,不可被修改的
final修飾屬性:屬性是常量,必須在定義時初始化,或者構造函數中初始化
final修飾方法:方法不能被覆寫
final修飾類:類不能被繼承
說到final,有必要提一下內聯
內聯指的是,如果一個方法內容很短,且沒有被其他類覆寫時,方法名會被直接替換為方法內容
比如:final getName()這個方法可以內聯為name屬性
再比如:getSum(){return a+b},會直接被內聯為a+b
為什麼會有內聯這個東西呢?
因為這樣可以提高效率(細節:CPU在處理方法調用的指令時,使用的分支轉移會擾亂預取指令的策略,這個比較底層,這裡先簡單介紹,後面章節再深入)
那它有沒有什麼缺點呢?
有,如果一個方法內容過長,又誤被當做內聯處理,那麼就會影響性能
比如你的程式碼多個地方都調用這個方法,那麼你的程式碼就會膨脹變得很大,從而影響性能
那有沒有辦法可以解決呢?
有,虛擬機的即時編譯技術
即時編譯會進行判斷,如果一個方法內容很長,且被多次調用,那麼它會自動關閉內聯機制,防止程式碼膨脹
3. 多態(Polymorphism)
字面理解,就是多種形態,在Java中,多態指的是,一個類可以有多種表現形態
多態主要是 用來創建可擴展的程式
像我們上面提到的繼承就是屬於多態的一種
還有一種就是介面(interface)
介面類一種是比抽象類更加抽象的類
因為抽象類起碼還可以實現方法,但是介面類沒得選,就只能定義方法,不能實現
不過從Java8開始,介面支援定義默認方法和靜態方法
介面的默認方法(default修飾符)和靜態方法(static修飾符),會包含方法內容,這樣別人可以直接調用介面類的方法(後面章節再細講)
這樣你會發現介面變得很像抽象類了,不過介面支援多實現(即一個類可以同時實現多個類,但是一個類同時只能繼承一個類)
這樣一來,Java相當於間接地實現了多繼承
下圖說明繼承和實現的區別:單繼承,多實現
多態一般用在哪些場景呢?
場景很多,這裡說兩個最常用的
- 場景一:方法的參數,即方法定義時,父類作為方法的形參,然後調用時傳入子類作為方法的實參
- 場景二:方法的返回值,即方法定義時,父類作為方法的返回值,然後在方法內部實際返回子類
程式碼示範如下:
public class PolyphorismDemo {
public static void main(String[] args) {
PolyphorismDemo demo = new PolyphorismDemo();
//場景一:形參,將貓(子類)賦值給動物(父類)
demo.fun(new Cat());
//場景二:返回值,將貓賦值給動物
Animal animal = demo.fun2();
}
public void fun(Animal animal){
}
public Animal fun2(){
return new Cat();
}
}
class Animal{
}
class Cat extends Animal{
}
總結
其中還有很多知識點沒總結,太多了,看起來會不方便,所以其他的內容會陸續放到後面章節來講
這裡先簡單列出來,比如:
- equals和hashcode的關係
- instanceof和getClass()的區別
- 靜態綁定和動態綁定
- Java8的默認方法和靜態方法
- 等等等
後記
最後,感謝大家的觀看,謝謝