上一篇說了Java面向?qū)ο笾械睦^承關(guān)系笤虫,在繼承中說到:調(diào)用對象中的成員變量時壳贪,根據(jù)引用類型來決定調(diào)用誰,而調(diào)用成員方法時由于多態(tài)的存在哎榴,具體調(diào)用誰的方法需要根據(jù)new出來的對象決定型豁,這篇主要描述的是Java中的多態(tài)以及利用多態(tài)形成的接口
多態(tài)
當(dāng)時在學(xué)習(xí)C++時,要使用多態(tài)需要定義函數(shù)為virtual尚蝌,也就是虛函數(shù)偷遗。類中存在虛函數(shù)時,對象會有一個虛函數(shù)表的頭指針驼壶,虛函數(shù)表會存儲虛函數(shù)的地址氏豌,在使用父類的指針或者引用來調(diào)用方法時會根據(jù)虛函數(shù)表中的函數(shù)地址來調(diào)用函數(shù),會形成多態(tài)热凹。
當(dāng)時學(xué)習(xí)C++時對多態(tài)有一個非常精煉的定義:基類的指針指向不同的派生類泵喘,其行為不同。這里行為不同指的是調(diào)用同一個虛函數(shù)時般妙,會調(diào)用不同的派生類函數(shù)纪铺。這里我們說形成多態(tài)的幾個基本條件:1)指針或者引用類型是基類;2)需要指向派生類碟渺;3)調(diào)用的函數(shù)必須是基類重寫的函數(shù)鲜锚。
public class Parent{
public void sayHelllo(){
System.out.println("Hello Parent");
}
public void sayHello(String name){
System.out.println("Hello" + name);
}
}
public class Child extends Parent{
public void sayHello(){
System.out.println("Hello Child");
}
}
根據(jù)上述的繼承關(guān)系,我們來看下面幾個實例代碼苫拍,分析一下哪些是多態(tài)
Parent obj = new Child();
obj.sayHello();
該實例構(gòu)成了多態(tài)芜繁,它滿足了多態(tài)的三個條件:Parent
類型的 obj
引用指向了 new
出來的Child子類、并且調(diào)用了二者共有的方法绒极。
Parent obj = new Child();
obj.sayHello("Tom");
這個例子沒有構(gòu)成多態(tài)骏令,雖然它滿足基類的引用指向派生類,但是它調(diào)用了父類特有的方法垄提。
Parent obj = new Parent();
obj.sayHello();
這個例子也不滿足多態(tài)榔袋,它使用父類的引用指向了父類,這里就是一個正常的類方法調(diào)用,它會調(diào)用父類的方法
Child obj = new Child();
obj.sayHello();
這個例子也不滿足多態(tài)铡俐,它使用子類的引用指向了子類凰兑,這里就是一個正常的類方法調(diào)用,它會調(diào)用子類的方法
那么多態(tài)有什么好處呢?引入多態(tài)實質(zhì)上也是為了避免重復(fù)的代碼审丘,而且程序更具有擴展性吏够,我們通過println函數(shù)來說明這個問題。
public void println(Object x) {
String s = String.valueOf(x);
synchronized (this) {
print(s);
newLine();
}
}
//Class String
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}
函數(shù)println實現(xiàn)了一個傳入Object的重載,該函數(shù)調(diào)用了String類的靜態(tài)方法 valueOf
, 進一步跟到String類中發(fā)現(xiàn)稿饰,該方法只是調(diào)用了類的 toString
方法锦秒,傳入的obj可以是任意繼承Object的類(在Java中只要是對象就一定是繼承自O(shè)bject),只要類重寫了 toString
方法就可以直接打印。這樣一個函數(shù)就實現(xiàn)了重用喉镰,相比于需要后來的人額外重載println函數(shù)來說旅择,要方便很多。
類類型轉(zhuǎn)化
上面的println 函數(shù)侣姆,它需要傳入的是Object類的引用生真,但是在調(diào)用該方法時,從來都沒有進行過類型轉(zhuǎn)化捺宗,都是直接傳的柱蟀,這里是需要進行類型轉(zhuǎn)化的,在由子類轉(zhuǎn)到父類的時候蚜厉,Java進行了隱式類型轉(zhuǎn)化长已。大轉(zhuǎn)小一定是安全的(這里的大轉(zhuǎn)小是對象的內(nèi)存包含關(guān)系),子類一定可以包含父類的成員昼牛,所以即使轉(zhuǎn)化為父類也不存在問題术瓮。而父類引用指向的內(nèi)存不一定就是包含了子類成員,所以小轉(zhuǎn)大不安全贰健。
為什么要進行小轉(zhuǎn)大呢胞四?雖然多態(tài)給了我們很大的方便,但是多態(tài)最大的問題就是父類引用無法看到子類的成員伶椿,也就是無法使用子類中的成員辜伟。這個時候如果要使用子類的成員就必須進行小轉(zhuǎn)大的操作。之前說過小轉(zhuǎn)大不安全脊另,由于父類可能有多個實現(xiàn)類导狡,我們無法確定傳進來的參數(shù)就是我們需要的子類的對象,所以java引入了一個關(guān)鍵字 instanceof
來判斷是否可以進行安全的轉(zhuǎn)化尝蠕,只要傳進來的對象引用是目標(biāo)類的對象或者父類對象它就會返回true烘豌,比如下面的例子
Object obj = "hello"
System.out.println(obj instanceof String); //true
System.out.println(obj instanceof Object); //true
System.out.println(obj instanceof StringBuffer); //false
System.out.println(obj instanceof CharSequence); //true
抽象方法和抽象類
我們說有了多態(tài)可以使代碼重用性更高。但是某些時候我們針對幾個有共性的類看彼,抽象出了更高層面的基類,但是發(fā)現(xiàn)基類雖然有一些共性的內(nèi)容囚聚,但是有些共有的方法不知道如何實現(xiàn)靖榕,比如說教科書上經(jīng)常舉例的動物類,由于不知道具體的動物是什么顽铸,所以也無法判斷該動物是食草還是食肉茁计。所以一般將動物的 eat
定義為抽象方法,擁有抽象方法的類一定必須是抽象基類。
抽象方法是不需要寫實現(xiàn)的方法星压,它只需提供一個函數(shù)的原型践剂。而抽象類不能創(chuàng)建實例,必須有派生類重寫抽象方法娜膘。為什么抽象類不能創(chuàng)建對象呢逊脯?對象調(diào)用方法本質(zhì)上是根據(jù)函數(shù)表找到函數(shù)對應(yīng)代碼所在的內(nèi)存地址,而抽象方法是未實現(xiàn)的方法竣贪,自然就無法給出方法的地址了军洼,如果創(chuàng)建了對象,而我的對象又想調(diào)用這個抽象方法那不就沖突了嗎演怎。所以規(guī)定無法實例化抽象類匕争。
抽象方法的定義使用關(guān)鍵字 abstract
,例如
public abstract class Life{
public abstract void happy();
}
public class Cat{
public void happy(){
System.out.println("貓吃魚");
}
}
public class Cat{
public void happy(){
System.out.println("狗吃肉");
}
}
public class Altman{
public void happy(){
System.out.println("奧特曼打小怪獸");
}
}
上面定義了一個抽象類Life 代表世間的生物爷耀,你要問生物的幸福是什么甘桑,可能沒有人給你答案,不同的生物有不同的回答歹叮,但是具體到同一種生物扇住,可能就有答案了,這里簡單的給出了答案:幸福就是貓吃魚狗吃肉奧特曼愛打小怪獸盗胀。
使用抽象類需要注意下面幾點:
- 不能直接創(chuàng)建抽象類的對象艘蹋,必須使用實現(xiàn)類來創(chuàng)建對象
- 實現(xiàn)類必須實現(xiàn)抽象類的所有抽象方法,否則該實現(xiàn)類也必須是抽象類
- 抽象類可以有自己的構(gòu)造方法票灰,該方法僅供子類構(gòu)造時使用
- 抽象類可以沒有抽象方法女阀,但是有抽象方法的一定要是抽象類
接口
接口就是一套公共的規(guī)范標(biāo)準(zhǔn),只要符合標(biāo)準(zhǔn)就能通用屑迂,比如說USB接口浸策,只要一個設(shè)備使用了USB接口,那么我的電腦不管你的設(shè)備是什么惹盼,插上就應(yīng)該能用庸汗。在代碼中接口就是多個類的公共規(guī)范。
Java中接口也是一個引用類型手报。接口與抽象類非常相似蚯舱,同樣不能創(chuàng)建對象,必須創(chuàng)建實現(xiàn)類的方法掩蛤。但是接口與抽象類還是有一些不同的枉昏。 抽象類也是一個類,它是從底層類中抽象出來的更高層級的類揍鸟,但是接口一般用來聯(lián)系多個類兄裂,是多個類需要實現(xiàn)的一個共同的標(biāo)準(zhǔn)。是從頂層一層層擴展出來的。
接口的一個常見的使用場景就是回調(diào)晰奖,比如說常見的窗口消息處理函數(shù)谈撒。這個場景C++中一般使用函數(shù)指針,而Java中主要使用接口匾南。
接口使用關(guān)鍵字 interface
來定義, 比如
public interface USB{
public final String deviceType = "USB";
public abstract void open();
public abstract void close();
}
接口中常見的一個成員是抽象方法啃匿,抽象方法也是由實現(xiàn)類來實現(xiàn),注意事項也與之前的抽象類相同午衰。除了有抽象方法立宜,接口中也可以有常量。
接口中的抽象方法是沒有方法體的臊岸,它需要實現(xiàn)類來實現(xiàn)橙数,所以實現(xiàn)類與接口中發(fā)生重寫現(xiàn)象時會調(diào)用實現(xiàn)類,那么常量呢?
public class Mouse implements USB{
public final String deviceType = "鼠標(biāo)";
public void open(){
}
public void close(){
}
}
public class Demo{
public static void main(String[] args){
USB usb = new Mouse();
System.out.println(usb.deviceType);
}
}
常量的調(diào)用遵循之前說的重載中的屬性成員調(diào)用的方式帅戒。使用的是什么類型的引用灯帮,調(diào)用哪個類型中的成員。
與抽象類中另一個重要的不同是逻住,接口運行多繼承钟哥,那么在接口的多繼承中是否會出現(xiàn)沖突的問題呢
public interface Storage{
public final String deviceType = "存儲設(shè)備";
public abstract void write();
public abstract void read();
}
public class MobileHardDisk implements USB, Storage{
public void open(){
}
public void close(){
}
public void write(){
}
public void read(){
}
}
public class Demo{
public static void main(String[] args){
MobileHardDisk mhd = new MobileHardDisk();
System.out.println(mhd.deviceType);
}
}
編譯上述代碼時會發(fā)現(xiàn)報錯了,提示 USB 中的變量 deviceType 和 Storage 中的變量 deviceType 都匹配
瞎访,也就是說Java中仍然沒有完全避免沖突問題腻贰。
接口中的默認方法
有的時候可能會出現(xiàn)這樣的情景,當(dāng)項目完成后扒秸,可能客戶需求有變播演,導(dǎo)致接口中可能會添加一個方法,如果使用抽象方法伴奥,那么接口所有的實現(xiàn)類都得重復(fù)實現(xiàn)某個方法写烤,比如說上述的代碼中,USB接口需要添加一個方法通知PC設(shè)備我這是什么類型的USB設(shè)備拾徙,以便操作系統(tǒng)匹配對應(yīng)的驅(qū)動洲炊。那么可能USB的實現(xiàn)類都需要添加一個,這樣可能會引入大量重復(fù)代碼尼啡,針對這個問題暂衡,從Java 8開始引入了默認方法。
默認方法為了解決接口升級的問題玄叠,接口中新增默認方法時古徒,不用修改之前的實現(xiàn)類。
默認方法的使用如下:
public interface USB{
public final String deviceType = "USB";
public abstract void open();
public abstract void close();
public default String getType(){
return this.deviceType;
}
}
默認方法同樣可以被所有的實現(xiàn)類覆蓋重寫读恃。
接口中的靜態(tài)方法
從Java 8中開始,允許在接口中定義靜態(tài)方法,靜態(tài)方法可以使用實現(xiàn)類的對象進行調(diào)用寺惫,也可以使用接口名直接調(diào)用
接口中的私有方法
從Java 9開始運行在接口中定義私有方法疹吃,私有方法可以解決在默認方法中存在大量重復(fù)代碼的情況。
雖然Java為接口中新增了這么多屬性和擴展西雀,但是我認為不到萬不得已萨驶,不要隨便亂用這些東西,畢竟接口中應(yīng)該定義一系列需要實現(xiàn)的標(biāo)準(zhǔn)艇肴,而不是自己去實現(xiàn)這些標(biāo)準(zhǔn)腔呜。
最后總結(jié)一下使用接口的一些注意事項:
- 接口沒有靜態(tài)代碼塊或者構(gòu)造方法
- 一個類的父類只能是一個,但是類可以實現(xiàn)多個接口
- 如果類實現(xiàn)的多個接口中有重名的默認方法再悼,那么實現(xiàn)類必須重寫這個實現(xiàn)方法核畴,不然會出現(xiàn)沖突。
- 如果接口的實現(xiàn)類中沒有實現(xiàn)所有的抽象方法冲九,那么這個類必須是抽象類
- 父類與接口中有重名的方法時谤草,優(yōu)先使用父類的方法,在Java中繼承關(guān)系優(yōu)于接口實現(xiàn)關(guān)系
- 接口與接口之間是多繼承的莺奸,如果多個父接口中存在同名的默認方法丑孩,子接口中需要重寫默認方法,不然會出現(xiàn)沖突
final關(guān)鍵字
之前提到過final關(guān)鍵字灭贷,用來表示常量温学,也就是無法在程序中改變的量。除了這種用法外甚疟,它還有其他的用法
- 修飾類仗岖,表示類不能有子類」潘可以將繼承關(guān)系理解為改變了這個類箩帚,既然final表示常量,不能修改黄痪,那么類自然也不能修改
- 修飾方法:被final修飾的方法不能被重寫
- 修飾成員變量:表示成員變量是常量紧帕,不能被修改
- 修飾局部變量:表示局部變量是常量,在對應(yīng)作用域內(nèi)不可被修改
<hr />