文章目錄
- 一、多型
- 1.1 向上轉型
- 1.2 方法的重寫
- 1.3 動態系結
- 1.4 向下轉型
- 1.5 理解多型
- 二、抽象類
- 2.1 語法規則
- 2.2 抽象類的作用
上一節我們學習了包,以及面向物件的基本特征之一:繼承
【Java】包和繼承
在此基礎上,這一節,將介紹面向物件的其他的兩個基本特征:
多型和
抽象類,
一、多型
什么是多型?
按照字面意思來說就是”多種狀態“,
官方的解釋是,多型指為不同資料型別的物體提供統一的介面,多型型別可以將自身所支持的操作套用到其他型別的值上,
說到這里,友友們也許開始 emo 了,我好像看懂了,我有好像妹看懂,,,

陽間的解釋是,我和我爸說:”爸!我要吃面!“然后我爸 DuangDuangDuang 的做了一碗面,做完后,”啪!“放我面前,我曉得了我吃的是小蔥拌面,
又有一日,我和我爸說:”爸!我要吃面!“然后我爸 DuangDuangDuang 的做了一碗面,做完后,”啪!“放我面前,我曉得了我吃的是榨菜肉絲面,
又有一日,我和我爸說:”爸!我要吃面!“然后我爸 DuangDuangDuang 的做了一碗面,做完后,”啪!“放我面前,我曉得了我吃的是排骨面,
總結下來,不管我吃的什么面,他們都是面的一種,是面的不同的表現形式,這就是多型,

具體的解釋是,有了繼承才有了多型,多型是指各種各樣的子類繼承了父類,不滿足于父類的某一方法,想要在原來的基礎上加點新東西,于是使用了重寫的方法覆寫了父類的方法,最后的結局是父類中的某一方法,在繼承該父類的各個子類當中表現出來各種各樣的行為,使得同一個方法或屬性在父類和子類們中具有不同的含義,另外實作多型還需要在創建子類時,用父類來 new 子類,即發生了向上轉型,
總結下來,想要實作多型,有三個必要條件:繼承、重寫以及向上轉型,
1.1 向上轉型
向上轉型,實際上就是把子類物件賦值給父類物件的參考,只有這樣該參考才既能可以呼叫父類的方法,又能呼叫子類的方法,
向上轉型發生的時機:
- 直接賦值
- 方法的傳參
- 方法的回傳值
方法一:直接賦值
📑代碼示例:
public class TestDemo {
public static void main(String[] args) {
//直接賦值
Animal animal1 = new Bird(2,"麻雀");
//詳細步驟
/*Animal animal = new Animal(2,"動物");
Bird bird = new Bird(2,"麻雀");
animal = bird;*/
}
}
💬代碼解釋:
這里的 animal 和 animal1 都是父類(Animal)的參考,指向一個子類 (Bird) 的實體,這就是
直接賦值版本的向上轉型,
方法二:方法的傳參
📑代碼示例:
public class TestDemo {
public static void func(Animal animal2) {
//......
}
public static void main(String[] args) {
Bird bird = new Bird(2,"麻雀");
func(bird);
}
}
💬代碼解釋:
這里呼叫了 func() 方法,實參是 Bird(子類) 類的參考,形參接收型別是 Animal(父類) 類,這就是
方法的傳參版本的向上轉型,
方法三:方法的回傳值
📑代碼示例:
public class TestDemo {
public static Bird func2() {
Bird bird = new Bird(2,"麻雀");
return bird;
}
public static void main(String[] args) {
Animal animal3 = func2();
}
}
💬代碼解釋:
func() 方法的回傳值型別是 Bird(子類),用 Animal(父類)這一型別接收,這就是
方法的回傳值版本的向上轉型,
實際上,這三種方法本質上并沒有什么區別,
1.2 方法的重寫
實作多型,重寫也是必不可少,非常重要的
在之前介紹方法的那一節里,我們有介紹過什么是多載,多載和重寫是需要進行區分的,
【Java】方法的使用
多載是編譯時的多型性,使在同一個類中有多個同名,引數個數或型別不同的方法,呼叫該方法時通過傳遞不同的個數或型別引數來決定具體使用的方法,
重寫是運行時的多型性,限于擁有繼承關系的子父類,子類并不想原封不動地繼承父類的方法,而是想作一定的修改,這就需要采用方法的重寫,方法重寫又稱方法覆寫,方法覆寫,
重寫條件:
- 方法名必須要相同
- 方法的回傳值要相同(特殊情況除外)
- 方法的引數串列相同
📑代碼示例:
class Animal {
public int age;
public String name;
public Animal(int age, String name) {
this.age = age;
this.name = name;
}
public void eat() {
System.out.println(this.age +"歲的" + this.name + " 會吃飯");
}
}
class Bird extends Animal{
public Bird(int age, String name) {
super(age, name);
}
@Override
public void eat() {
System.out.println("吃吃吃!!!");
}
}
💬代碼解釋:
- Bird 類繼承了 Animal 類,并且重寫了父類(Animal) 中的 eat() 方法,
- 重寫的快捷鍵是
Ctrl + o,選擇需要重寫的方法,- 針對重寫的方法, 可以使用
@Override注解來顯式指定,可以對重寫的方法進行一定的校驗,例如當重寫的方法名字寫錯時,有了該注解,就會報編譯錯誤,示意該方法名字有誤,并非重寫的方法,
注意事項:
static修飾的靜態方法不能重寫,因為沒有辦法達到多型的效果,當進行了我們以為的重寫時,向上轉型后,用父類參考變數來呼叫我們重寫的靜態方法時,會發現只會呼叫原來父類的靜態方法,而并非我們所期待的子類的靜態方法,- 被
final修飾的方法不能被重寫,重寫后會產生編譯錯誤,當一方法被 final 修飾后,該方法就被稱為密封方法,該方法是不能夠被重寫的,- 重寫的父類的方法不能被
private關鍵字修飾,子類重寫父類的方法時,該方法的訪問修飾限定符權限一定要大于等于父類的權限,構造方法是不能被重寫的,重寫的方法,方法名是一樣的,而父類的構造方法的方法名和父類的類名是一樣的,倘使構造方法可以重寫,將就意味著子類和父類同名,
雖然構造方法不能進行重寫,但是構造方法中是可以呼叫重寫的方法的,那么這其中又會有什么需要注意的呢?

📑代碼示例:
class Animal {
public String name;
public Animal(String name) {
eat();
this.name = name;
}
public void eat() {
System.out.println(this.name + "會吃飯");
}
}
class Bird extends Animal{
public Bird(String name) {
super(name);
}
@Override
public void eat() {
System.out.println(this.name + "正在吃吃吃!!!");
}
}
public class TestDemo {
public static void main(String[] args) {
Bird bird = new Bird("阿花");
}
}
🏸 代碼結果:

代碼中,new 了一個 Bird 型別的物件,傳了引數"阿花",為何最后結果中,小鳥"阿花"卻不配擁有姓名,,,

淵源就在于在父類的構造方法的第一行對重寫的方法 eat() 進行了呼叫,讓我們來追蹤一下,,,
程序追蹤:
- 實體化了一個Bird 型別的物件,呼叫了有一個引數的構造方法,將“阿花”傳了過去
- 到達了子類的構造方法時,需要先幫父類進行構造,又把“阿花”傳給了父類的構造方法
- 父類的構造方法內第一行就呼叫了重寫方法 eat(),于是呼叫了 Bird 類里面的 eat() 方法
- 然而此時父類還沒構造完畢,“阿花”還沒有真正的擁有姓名時,就去執行 eat() 方法
- 結果中沒有“阿花”,,,
因此想要“阿花”擁有姓名,一定要把重用方法的呼叫放在賦值之后,
這個坑在選擇題中可能會考到,因此還需看仔細,不要掉坑里了,
1.3 動態系結
動態系結后,物件在呼叫方法的時候能夠自己判定改呼叫誰的方法是自己的方法還是父類的方法,
📑代碼示例:
實體一:
class Animal {
public int age;
public String name;
public void eat() {
System.out.println("Animal會吃飯");
}
}
class Bird extends Animal{
public void eatBird() {
System.out.println("小鳥吃吃吃!!!");
}
}
public class TestDemo {
public static void main(String[] args) {
Animal animal = new Bird();
animal.eat();
}
}
🏸 代碼結果:

實體二:
class Animal {
public int age;
public String name;
public void eat() {
System.out.println("Animal會吃飯");
}
}
class Bird extends Animal{
@Override
public void eat() {
System.out.println("小鳥吃吃吃!!!");
}
}
public class TestDemo {
public static void main(String[] args) {
Animal animal = new Bird();
animal.eat();
}
}
🏸 代碼結果:

💬代碼解釋:
- 實體一和實體二的區別就在于是否呼叫了
重寫方法,- 實體一中,沒有重寫方法,因此通過 Animal 類訪問 eat() 方法,理所應當的呼叫父類的 eat() 方法,
- 實體二中,有呼叫子類和父類同名的重寫方法 eat() 方法,在次通過 Animal 類訪問 eat() 方法,呼叫的是子類的 eat() 方法,
- 實際上,實體二在編譯的時候仍然呼叫的是父類的方法,運行的時候,實際上呼叫的是子類的方法,這就是動態系結,
動態系結的條件:
- 發生向上轉型,即把子類物件賦值給父類物件的參考
- 通過父類參考來呼叫子類和父類同名的重寫方法 eat() 方法
1.4 向下轉型
既然有向上轉型,那么自然也會有向下轉型,即父類物件轉換成子類物件,相對而言,用的范圍更小,不那么常見,
📑代碼示例:
class Animal {
public int age;
public String name;
public void eat() {
System.out.println("Animal會吃飯");
}
}
class Bird extends Animal{
@Override
public void eat() {
System.out.println("小鳥吃吃吃!!!");
}
}
public class TestDemo {
public static void main(String[] args) {
Animal animal = new Bird();
Bird bird = (Bird) animal;
bird.eat();
}
}
🏸 代碼結果:

💬代碼解釋:
向下轉型的前提是要先進行向上轉型,如代碼所示,需要保證 Animal 參考的是 Bird 型別的物件,否則根本沒有辦法將 Animal 進行強制型別轉換,
雖然如此,向下轉型使用起來仍然容易出錯,如若將上面的 main 函式改成這樣…
📑代碼示例:
public class TestDemo {
public static void main(String[] args) {
Animal animal1 = new Animal();
Bird bird = (Bird) animal1;
bird.fly();
}
}
🏸 代碼結果:

代碼解釋:
- 此時,animal1 這個參考指向的物件是 Animal 型別的,致使強制型別轉換出錯,animal1 這個參考沒有辦法轉換成 Bird 型別,
- 為了讓向下轉型更加的安全,對于該代碼,我們有必要先判斷一下,animal1 的本質是否為 Bird 型別,如果不是,就根本不進行強制型別轉換,進行向下轉型,需要使用到關鍵字
instanceof,該關鍵字就是用來判斷一個參考是否是某個類的實體,是回傳 true ,提高了向下轉型的安全性,具體代碼為: if (animal1 instanceof Bird) { //…}
1.5 理解多型
通過上面的對多型基礎條件的將講解,想必諸君心中慢慢開始了解多型的含義,在此,將對多型進行舉例總結,更加深入理解多型,
📑代碼示例:
利用多型來畫圖形
實體一:
class Shape {
public void draw() {
}
}
class Cycle extends Shape {
@Override
public void draw() {
System.out.println("畫一個圓形○");
}
}
class Rect extends Shape {
@Override
public void draw() {
System.out.println("畫一個矩形□");
}
}
public class TestDemo {
public static void paint(Shape shape) {
shape.draw();
}
public static void main(String[] args) {
Cycle cycle = new Cycle();
paint(cycle);
paint(new Rect());
}
}
🏸 代碼結果:

💬代碼解釋:
- 這里用
paint方法來接受傳遞進來的子類物件,形參型別是父類 Shape 類,從而實作了多型的條件之一,向上轉型,通過 shape 參考來呼叫子類和父類都重寫的方法 draw(),從而列印出各種圖形,即表現出各種狀態,這就是多型,- TestDemo 類以外的部分就是
類的實作者完成的部分,TestDemo 類以內的部分就是類的呼叫者完成的部分
實體二:
//類的實作者部分同上,又加了一個畫三角形
public class TestDemo {
public static void paint() {
Shape[] shapes = {new Cycle(),new Rect(),new Rect(),new Triangle()};
for (Shape shape1:shapes) {
shape1.draw();
}
}
public static void main(String[] args) {
paint();
}
}
🏸 代碼結果:

多型的優點:
- 多型是封裝的更進一步, 讓類呼叫者對類的使用成本進一步降低,即類的呼叫者只需要知道物件具有什么方法就行,不需要對物件的型別過分了解
- 通過實體二,我們發現多型可以避免使用多余的 if-else 陳述句,如果沒有多型,我們就需要創建一個字串陣列,字串為想要列印的圖形,將他們通過一道道分支陳述句進行判斷是否為想要列印的圖形,
- 使用多型,使得代碼的改動成本降低,
二、抽象類
在利用多型畫圖形的實體一中,父類 Shape 中的 draw 方法并沒有實際作業,畫圖的作業都被繼承這個父類的子類中的重寫 draw 方法包了,向父類中 draw 方法這樣沒有實際用途的方法,可以將其設計成抽象方法,而抽象類就是包含這個抽象方法的類,
2.1 語法規則
📑代碼示例:
abstract class Shape {
public abstract void draw();
}
abstract class A extends Shape {
public abstract void draw2();
}
class B extends A {
@Override
public void draw() {
}
@Override
public void draw2() {
}
}
💬代碼解釋:
- 在 draw 方法前加上
abstract關鍵字, 表示這是一個抽象方法. 包含抽象方法的類也要被abstract關鍵字修飾, 表示這是一個抽象類- 抽象方法沒有方法體(沒有
{ }, 不能執行具體代碼)- 抽象類
不可以被實體化,其存在就是為了被繼承- 抽象類中也可以定義成員變數和成員方法,也被重寫被子類呼叫(用 super 關鍵字)
- 一個類繼承了抽象類,那么該類需要
重寫所有的抽象方法,如果不重寫就會編譯出錯,重寫的快捷方法處了Ctrl + o以外,也可以將游標放置在報錯代碼處,選擇Implement methods(Alt + Shift + Enter)
*abstract不能final共存 ,被final修飾的類不能擁有子類,而抽象類就是要成為父類的;不能和private共存,抽象方法被其修飾就沒有辦法被重寫;不能和static共存,static 修飾后是不依賴于物件的,直接呼叫類名就行,那么抽象方法就沒有存在的必要- 抽象類 A 繼承了抽象類 Shape,普通類 B 繼承了 A ,則 B 里要將 A 和 Shape 中所有的抽象方法都重寫
- 抽象類也可以發生向上轉型,實作多型
2.2 抽象類的作用
- 寫成抽象類,看代碼時就知道這是抽象方法,而知道這個方法是在子類中實作的,所以有
提示作用- 使用抽象類好比多了一重編譯校驗,防止在父類被誤用,起到
警示作用
完!
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/301804.html
標籤:其他
