上一篇說了Java面向對象中的繼承關系,在繼承中說到:調用對象中的成員變量時,根據引用類型來決定調用誰,而調用成員方法時由于多態的存在,具體調用誰的方法需要根據new出來的對象決定,這篇主要描述的是Java中的多態以及利用多態形成的接口
多態
當時在學習C++時,要使用多態需要定義函數為virtual,也就是虛函數。類中存在虛函數時,對象會有一個虛函數表的頭指針,虛函數表會存儲虛函數的地址,在使用父類的指針或者引用來調用方法時會根據虛函數表中的函數地址來調用函數,會形成多態。
當時學習C++時對多態有一個非常精煉的定義:基類的指針指向不同的派生類,其行為不同。這里行為不同指的是調用同一個虛函數時,會調用不同的派生類函數。這里我們說形成多態的幾個基本條件:1)指針或者引用類型是基類;2)需要指向派生類;3)調用的函數必須是基類重寫的函數。
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");
}
}
根據上述的繼承關系,我們來看下面幾個實例代碼,分析一下哪些是多態
Parent obj = new Child();
obj.sayHello();
該實例構成了多態,它滿足了多態的三個條件:Parent
類型的 obj
引用指向了 new
出來的Child子類、并且調用了二者共有的方法。
Parent obj = new Child();
obj.sayHello("Tom");
這個例子沒有構成多態,雖然它滿足基類的引用指向派生類,但是它調用了父類特有的方法。
Parent obj = new Parent();
obj.sayHello();
這個例子也不滿足多態,它使用父類的引用指向了父類,這里就是一個正常的類方法調用,它會調用父類的方法
Child obj = new Child();
obj.sayHello();
這個例子也不滿足多態,它使用子類的引用指向了子類,這里就是一個正常的類方法調用,它會調用子類的方法
那么多態有什么好處呢?引入多態實質上也是為了避免重復的代碼,而且程序更具有擴展性,我們通過println函數來說明這個問題。
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();
}
函數println實現了一個傳入Object的重載,該函數調用了String類的靜態方法 valueOf
, 進一步跟到String類中發現,該方法只是調用了類的 toString
方法,傳入的obj可以是任意繼承Object的類(在Java中只要是對象就一定是繼承自Object),只要類重寫了 toString
方法就可以直接打印。這樣一個函數就實現了重用,相比于需要后來的人額外重載println函數來說,要方便很多。
類類型轉化
上面的println 函數,它需要傳入的是Object類的引用,但是在調用該方法時,從來都沒有進行過類型轉化,都是直接傳的,這里是需要進行類型轉化的,在由子類轉到父類的時候,Java進行了隱式類型轉化。大轉小一定是安全的(這里的大轉小是對象的內存包含關系),子類一定可以包含父類的成員,所以即使轉化為父類也不存在問題。而父類引用指向的內存不一定就是包含了子類成員,所以小轉大不安全。
為什么要進行小轉大呢?雖然多態給了我們很大的方便,但是多態最大的問題就是父類引用無法看到子類的成員,也就是無法使用子類中的成員。這個時候如果要使用子類的成員就必須進行小轉大的操作。之前說過小轉大不安全,由于父類可能有多個實現類,我們無法確定傳進來的參數就是我們需要的子類的對象,所以java引入了一個關鍵字 instanceof
來判斷是否可以進行安全的轉化,只要傳進來的對象引用是目標類的對象或者父類對象它就會返回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
抽象方法和抽象類
我們說有了多態可以使代碼重用性更高。但是某些時候我們針對幾個有共性的類,抽象出了更高層面的基類,但是發現基類雖然有一些共性的內容,但是有些共有的方法不知道如何實現,比如說教科書上經常舉例的動物類,由于不知道具體的動物是什么,所以也無法判斷該動物是食草還是食肉。所以一般將動物的 eat
定義為抽象方法,擁有抽象方法的類一定必須是抽象基類。
抽象方法是不需要寫實現的方法,它只需提供一個函數的原型。而抽象類不能創建實例,必須有派生類重寫抽象方法。為什么抽象類不能創建對象呢?對象調用方法本質上是根據函數表找到函數對應代碼所在的內存地址,而抽象方法是未實現的方法,自然就無法給出方法的地址了,如果創建了對象,而我的對象又想調用這個抽象方法那不就沖突了嗎。所以規定無法實例化抽象類。
抽象方法的定義使用關鍵字 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 代表世間的生物,你要問生物的幸福是什么,可能沒有人給你答案,不同的生物有不同的回答,但是具體到同一種生物,可能就有答案了,這里簡單的給出了答案:幸福就是貓吃魚狗吃肉奧特曼愛打小怪獸。
使用抽象類需要注意下面幾點:
- 不能直接創建抽象類的對象,必須使用實現類來創建對象
- 實現類必須實現抽象類的所有抽象方法,否則該實現類也必須是抽象類
- 抽象類可以有自己的構造方法,該方法僅供子類構造時使用
- 抽象類可以沒有抽象方法,但是有抽象方法的一定要是抽象類
接口
接口就是一套公共的規范標準,只要符合標準就能通用,比如說USB接口,只要一個設備使用了USB接口,那么我的電腦不管你的設備是什么,插上就應該能用。在代碼中接口就是多個類的公共規范。
Java中接口也是一個引用類型。接口與抽象類非常相似,同樣不能創建對象,必須創建實現類的方法。但是接口與抽象類還是有一些不同的。 抽象類也是一個類,它是從底層類中抽象出來的更高層級的類,但是接口一般用來聯系多個類,是多個類需要實現的一個共同的標準。是從頂層一層層擴展出來的。
接口的一個常見的使用場景就是回調,比如說常見的窗口消息處理函數。這個場景C++中一般使用函數指針,而Java中主要使用接口。
接口使用關鍵字 interface
來定義, 比如
public interface USB{
public final String deviceType = "USB";
public abstract void open();
public abstract void close();
}
接口中常見的一個成員是抽象方法,抽象方法也是由實現類來實現,注意事項也與之前的抽象類相同。除了有抽象方法,接口中也可以有常量。
接口中的抽象方法是沒有方法體的,它需要實現類來實現,所以實現類與接口中發生重寫現象時會調用實現類,那么常量呢?
public class Mouse implements USB{
public final String deviceType = "鼠標";
public void open(){
}
public void close(){
}
}
public class Demo{
public static void main(String[] args){
USB usb = new Mouse();
System.out.println(usb.deviceType);
}
}
常量的調用遵循之前說的重載中的屬性成員調用的方式。使用的是什么類型的引用,調用哪個類型中的成員。
與抽象類中另一個重要的不同是,接口運行多繼承,那么在接口的多繼承中是否會出現沖突的問題呢
public interface Storage{
public final String deviceType = "存儲設備";
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);
}
}
編譯上述代碼時會發現報錯了,提示 USB 中的變量 deviceType 和 Storage 中的變量 deviceType 都匹配
,也就是說Java中仍然沒有完全避免沖突問題。
接口中的默認方法
有的時候可能會出現這樣的情景,當項目完成后,可能客戶需求有變,導致接口中可能會添加一個方法,如果使用抽象方法,那么接口所有的實現類都得重復實現某個方法,比如說上述的代碼中,USB接口需要添加一個方法通知PC設備我這是什么類型的USB設備,以便操作系統匹配對應的驅動。那么可能USB的實現類都需要添加一個,這樣可能會引入大量重復代碼,針對這個問題,從Java 8開始引入了默認方法。
默認方法為了解決接口升級的問題,接口中新增默認方法時,不用修改之前的實現類。
默認方法的使用如下:
public interface USB{
public final String deviceType = "USB";
public abstract void open();
public abstract void close();
public default String getType(){
return this.deviceType;
}
}
默認方法同樣可以被所有的實現類覆蓋重寫。
接口中的靜態方法
從Java 8中開始,允許在接口中定義靜態方法,靜態方法可以使用實現類的對象進行調用,也可以使用接口名直接調用
接口中的私有方法
從Java 9開始運行在接口中定義私有方法,私有方法可以解決在默認方法中存在大量重復代碼的情況。
雖然Java為接口中新增了這么多屬性和擴展,但是我認為不到萬不得已,不要隨便亂用這些東西,畢竟接口中應該定義一系列需要實現的標準,而不是自己去實現這些標準。
最后總結一下使用接口的一些注意事項:
- 接口沒有靜態代碼塊或者構造方法
- 一個類的父類只能是一個,但是類可以實現多個接口
- 如果類實現的多個接口中有重名的默認方法,那么實現類必須重寫這個實現方法,不然會出現沖突。
- 如果接口的實現類中沒有實現所有的抽象方法,那么這個類必須是抽象類
- 父類與接口中有重名的方法時,優先使用父類的方法,在Java中繼承關系優于接口實現關系
- 接口與接口之間是多繼承的,如果多個父接口中存在同名的默認方法,子接口中需要重寫默認方法,不然會出現沖突
final關鍵字
之前提到過final關鍵字,用來表示常量,也就是無法在程序中改變的量。除了這種用法外,它還有其他的用法
- 修飾類,表示類不能有子類。可以將繼承關系理解為改變了這個類,既然final表示常量,不能修改,那么類自然也不能修改
- 修飾方法:被final修飾的方法不能被重寫
- 修飾成員變量:表示成員變量是常量,不能被修改
- 修飾局部變量:表示局部變量是常量,在對應作用域內不可被修改
<hr />