詳解Java面向對象編程之多態
Java面向對象編程之多態
一.對於多態的理解:
通俗點理解,多態其實就是一詞多義,就是一種方法的多種狀態,即不同的類對象,調用同一個方法名,有不同的實現效果,如下面這段代碼塊:
public class Test { public static void main(String[] args) { Dog dog = new Dog("豆豆"); Cat cat = new Cat("花花"); dog.eat(); cat.eat(); } }
對象dog和cat看似都調用瞭eat方法,都沒有傳參,按理說輸出的結果應該一樣,但其實不是這樣的,讓我們來看一下輸出的結果:
這就是多態的一種表現,所屬不同類的不同對象調用同一個方法名,卻有著不同的實現效果。
二.多態的實現方法
Java中通過 方法重寫(也叫方法覆寫)、方法重載和接口實現多態(主要依賴於繼承機制+方法覆寫)
1.方法重載
方法重載十分好理解,就是子類和父類的方法名相同,但是參數個數或類型不一樣,返回值不作要求,這裡不再贅述
2.方法重寫
對於方法重寫,通常結合向上轉型和向下轉型兩種形式進行應用,其中向上轉型更為常見,向下轉型相對使用較少
(1)向上轉型:就是子類向父類轉,向上轉型最大的好處就是可以實現參數統一化,向上轉型可以表現在三個地方:
其一:產生對象時:
註意:用這種形式創建的實例化對象dog1,其能調用的方法范圍由父類Animal決定,即隻能調用Animal類中的方法,而不能調用子類獨有的方法,隻有當子類有對父類的方法重寫時,才調用子類重寫後的方法!!!
其二:方法參數的傳遞:
其三:方法返回值的傳遞
向上轉型的最大好處就是——參數統一化,父類引用可以接收子類所有對象
看下面這個例子:
完整代碼為:
public class Animal { public String name; public Animal(String a){ name = a; } public void eat(){ System.out.println("食物"); } public static void fun(Animal animal){ animal.eat(); } } public class Dog extends Animal{ public Dog(String name){ super(name); } public void eat(){ System.out.println("骨頭"); } } public class Cat extends Animal{ public Cat(String name){ super(name); } public void eat(){ System.out.println("魚"); } } public class Test { public static void main(String[] args) { Dog dog = new Dog("豆豆"); Cat cat = new Cat("花花"); dog.fun(dog); cat.fun(cat); } }
結果為:
拆開來分析:
fun方法的參數為Animal類的實例化對象
Animal的子類對象,可以直接傳入
也就是說,對於Animal類的所有子類實例化對象,均可以直接向fun方法傳參,避免瞭重復性的寫諸如 public static void fun(Dod d){}, public static void fun(Cat a){}等方法
(2)向下轉型:向上轉型是子類向父類轉,向下轉型則是將轉為的父類還原為子類,這裡用 還原 這個詞是因為能向下轉型的前提是:先發生向上轉型且我們需要使用子類獨有的方法時,才使用向下轉型,也很好理解,父類不一定是子類,隻有由子類轉成的才可以向下轉型還原,向下轉型的形式如下:
Animal animal = new Dog("豆豆"); Dog dog = (Dog)animal;
基本類似與強制類型轉換
註意:向下轉型是有風險的,可能無法強制轉換成功,這裡可以引用instanceof類,用if語句判斷,避免報錯
if(dog instanceof Dog){ }
(3)方法重寫的幾點註意要求:
- 隻能覆寫成員方法,不能重寫static靜態方法,但是方法重載是可以重載static方法的
- 子類進行方法重寫,子類方法的權限修飾符>=父類方法的權限修飾符,同時,子類並不能覆寫父類的private方法,對於父類的包訪問權限修飾方法,在不同包下的子類也不能覆寫
- 用final修飾的方法也不能覆寫哦(JDK中的String類就是一個final類)
- 返回值必須相同,或是向上轉型的,即覆寫的方法的返回值可以是父類方法返回類型的子類
- 註意方法覆寫與方法重載的區別,方法覆寫:子類與父類的方法名一樣,參數、返回值類型均一樣,如果隻返回值類型不一樣編譯會報錯
- 可使用@override 檢驗方法覆寫是否一樣
(4)最後是一道例題,容易掉坑:
public class A { public A(){ this.func(); } public void func(){ System.out.println("A"); } } class B extends A{ private int num; public B(int num){ this.num = num; } public void func(){ System.out.println("B的num==" + num); } public static void main(String[] args) { B b = new B(100); b.func(); } }
分析運行後會輸出什麼呢?
仔細想想,小心掉坑,答案在文章末尾給出
3.抽象類
對於方法的覆寫,一般的繼承關系下,子類是可以選擇覆寫也可以選擇不覆寫的,但在一些場景下,我們想對子類作出強制性覆寫要求,這就引出瞭抽象類的概念
(1)抽象類用abstract修飾,抽象類是普通類的超集,它隻是在普通類的基礎上多瞭抽象方法,抽象方法是沒有方法體的,形式如下:
(2)抽象類必須有子類繼承
(3)抽象類無法實例化對象,僅能用子類new相應的對象
(4)普通子類繼承抽象類,必須覆寫所有的抽象方法,當子類仍為抽象類時,可以選擇不覆寫,依舊保留抽象方法
(5)abstract修飾符不能和final同時使用,也不能和private同時使用
4.接口
上面講的抽象類雖然能實現方法的覆寫,但還是有缺陷的,比如抽象類還是遵循單繼承原則,一個類也隻能繼承一個抽象類,同時,在語義上,隻要繼承,就是A is B 的意思,有時候並不符合邏輯,故而又引出瞭接口這個概念
(1)接口的定義與使用
我們用關鍵字interface來定義接口,子類用關鍵字 implements來實現接口,同時,通常,在命名接口時,我們會用大寫的字母“I”開頭命名以示區別,如下面一段代碼的接口名為 IMessage ,而對於實現接口的子類命名我們通常用Impl作後綴
(2)接口的特點:
接口中隻有全局常量和抽象方法(JDK8之前,JDK8又擴展瞭default方法,瞭解即可),如:
public interface IMessage { public static final int a = 10; public abstract void print(); }
接口中隻有public權限,且全部為全局常量和抽象方法,故而,在接口內,public、static、final、abstract可以省略不寫,默認即為這些關鍵字,故上一段代碼可以直接寫成下面這段:
public interface IMessage { int a = 10; void print(); }
接口是沒有單繼承限制的,子類可以implements多個父接口,父接口之間用逗號隔開,如:
public class CImpl implements IB,IMessage{ public void print(){ } public void printf(){ }}
- 同時,接口之間也可以多繼承,一個接口可以extends多個父接口
- 接口同抽象類一樣,是不能直接實例化對象的,必須通過實現它的子類進行實例化
- 如果一個子類既有繼承的父類,也有實現的接口,則先繼承父類再實現父接口
(3)常用的JDK內置的兩大接口
a:Comparable接口
當使用Arrays.sort()方法排序時,當排序對象為自定義的類時,sort方法不知道應該按照對象的什麼屬性進行排序,故而待排序的自定義類需實現該接口,並將抽象方法compareTo覆寫,形式如下:
import java.util.Arrays; public class Person implements Comparable<Person> { // 兩個屬性,name和age private String name; private int age; // 有參構造 public Person(String name,int age){ this.name = name; this.age = age; } // 定義輸出 public String toString(){ return name + "的年齡是" + age; } // 覆寫compareTo方法 public int compareTo(Person o){ return (this.age - o.age); } public static void main(String[] args) { Person p1 = new Person("言希",18); Person p2 = new Person("溫衡",16); Person p3 = new Person("思莞",17); Person []p = new Person[]{p1,p2,p3}; // 用sort方法排序 Arrays.sort(p); System.out.println(Arrays.toString(p)); } }
輸出結果(按年齡升序):
b: Cloneable接口
Cloneable接口位於java.lang包中,顧名思義,就是用於克隆,在代碼中也就是復制新的對象,新對象的屬性方法都是從原對象中拷貝過來的,在實現該接口時,隻需要覆寫Object類提供的clone方法,如下面示例:
//實現Cloneable接口 public class Animall implements Cloneable{ private String name; // clone方法 protected Animall clone() throws CloneNotSupportedException { return (Animall)super.clone(); } public static void main(String[] args) throws CloneNotSupportedException{ Animall a1 = new Animall(); a1.name = "豆豆"; // a2由a1克隆而來 Animall a2 = a1.clone(); // 輸出a2,和a1一致 System.out.println(a2.name); // 但是a1並不是a2 System.out.println(a1 == a2); } }
結果如下:
補充:
- Cloneable接口是標記接口,即它本身並沒有任何抽象方法,當一個類實現瞭該接口,就表示該類具備瞭克隆能力
- clone方法的源代碼為protected native object clone() throws CloneNotSupportedException;,其中native也是一個關鍵字,表明是本地方法,即調用瞭C++的同名方法故而,我們還可以發現,native修飾的方法也是沒有方法體的
- 沒有方法體的一定是抽象方法 ⅹ錯誤,因為native方法也是沒有方法體的
好瞭,多態的內容基本就是這麼多瞭,java中的多態主要依賴於繼承和方法覆寫,而對於那些需要強制性覆寫的方法,我們又引出瞭抽象類,再鑒於抽象類有局限,我們又學習瞭接口,整體上內容就是這麼多瞭,註意的細節比較多,多敲代碼理解理解更棒。
最後,將文中那道例題答案奉上:
答案解析來啦
從main方法開始,執行的第一句為 B b = new B(100);,因為B繼承自A,故而執行B 的構造方法要先去執行A的構造方法,public A(){ this.func(); },這裡註意,雖然是A的構造,但對象是B的,故這裡的this,func()實際是B.func(),public void func(){System.out.println(“B的num==” + num);}
因為這裡其實還沒給num賦值成功,所以num現在還是默認值0,所以輸出瞭答案的第一句 B的num == 0,然後接著執行B的構造方法,將100賦值給瞭num,最後執行b.func,也就輸出瞭答案的第二句 B的num ==100
總結
本篇文章就到這裡瞭,希望能夠給你帶來幫助,也希望您能夠多多關註WalkonNet的更多內容!
推薦閱讀:
- Java語法之 Java 的多態、抽象類和接口
- 一篇文章帶你深入瞭解Java對象與Java類
- Java抽象類和接口使用梳理
- Java入門基礎之抽象類與接口
- 詳解Java深拷貝,淺拷貝和Cloneable接口