首頁 >Java >java教程 >深入理解Java三大特性中的多態

深入理解Java三大特性中的多態

高洛峰
高洛峰原創
2017-01-19 14:12:031330瀏覽

Java三大特性

物件導向程式設計有三大特性:封裝、繼承、多型。

封裝隱藏了類別的內部實作機制,可以在不影響使用的情況下改變類別的內部結構,同時也保護了資料。對外界而已它的內部細節是隱藏的,暴露給外界的只是它的存取方法。

繼承是為了重複使用父類別程式碼。兩個類別若存在IS-A的關係就可以使用繼承。 ,同時繼承也為實現多態做了鋪墊。那什麼是多態呢?多態的實現機制又是什麼?請看我一一為你揭開:

所謂多態就是指程式中定義的引用變數所指向的具體類型和透過該引用變數發出的方法呼叫在程式設計時並不確定,而是在程式執行期間才確定,也就是一個引用變數倒底會指向哪個類別的實例對象,而該引用變數發出的方法呼叫到底是哪個類別中實現的方法,必須在由程式運行期間才能決定。因為在程式執行時才確定具體的類,這樣,不用修改原始程式碼,就可以讓引用變數綁定到各種不同的類別實作上,從而導致該引用呼叫的具體方法隨之改變,即不修改程式碼就可以改變程式執行時所綁定的具體程式碼,讓程式可以選擇多個運行狀態,這就是多態性。

例如你是個酒神,對酒情有獨鍾。某日回家發現桌上有幾個杯子裡面都裝了白酒,從外面看我們是不可能知道這是些什麼酒,只有喝了之後才能夠猜出來是何種酒。你一喝,這是劍南春、再喝這是五糧液、再喝這是酒鬼酒….在這裡我們可以描述成如下:

酒a = 劍南春

酒b = 五糧液

酒c = 酒

酒b = 五糧液

酒c = 酒

酒…

這裡所表現的就是多態。劍南春、五糧液、酒鬼酒都是酒的子類,我們只是透過酒這一個父類就能夠引用不同的子類,這就是多態──我們只有在運作的時候才會知道引用變數所指向的具體實例物件。

誠然,要理解多態性我們就必須明白什麼是「向轉型」。在繼承中我們簡單介紹了向上轉型,這裡就在囉嗦下:在上面的喝酒例子中,酒(Win)是父類,劍南春(JNC)、五糧液(WLY)、酒鬼酒(JGJ)是子類。我們定義如下程式碼:

JNC a = new  JNC();

對於這個程式碼我們非常容易理解無非就是實例化了一個劍南春的物件嘛!但是這樣呢?

Wine a = new JNC();

在這裡我們這樣理解,這裡定義了一個Wine 類型的a,它指向JNC物件實例。由於JNC是繼承與Wine,所以JNC可以自動向上轉化為Wine,所以a是可以指向JNC實例物件的。這樣做有一個非常大的好處,在繼承中我們知道子類別是父類別的擴展,它可以提供比父類別更強大的功能,如果我們定義了一個指向子類別的父類別引用類型,那麼它除了能夠引用父類別的共通性外,還可以使用子類別強大的功能。

但是向上轉型存在一些缺憾,那就是它必定會導致一些方法和屬性的遺失,而導致我們不能夠取得它們。所以父類別的參考可以呼叫父類別中定義的所有屬性和方法,對於只存在與子類別中的方法和屬性它就望塵莫及了---1。

public class Wine {
 public void fun1(){
  System.out.println("Wine 的Fun.....");
  fun2();
 }
  
 public void fun2(){
  System.out.println("Wine 的Fun2...");
 }
}
 
public class JNC extends Wine{
 /**
  * @desc 子类重载父类方法
  *  父类中不存在该方法,向上转型后,父类是不能引用该方法的
  * @param a
  * @return void
  */
 public void fun1(String a){
  System.out.println("JNC 的 Fun1...");
  fun2();
 }
  
 /**
  * 子类重写父类方法
  * 指向子类的父类引用调用fun2时,必定是调用该方法
  */
 public void fun2(){
  System.out.println("JNC 的Fun2...");
 }
}
 
public class Test {
 public static void main(String[] args) {
  Wine a = new JNC();
  a.fun1();
 }
}
-------------------------------------------------
Output:
Wine 的Fun.....
JNC 的Fun2...

從程式的運行結果中我們發現,a.fun1()首先是運行父類Wine中的fun1() .然後再運行子類JNC中的fun2() 。

分析:在這個程式中子類別JNC重載了父類別Wine的方法fun1() ,重寫fun2() ,而且重載後的fun1(String a)與 fun1()不是同一個方法,由於父類別中沒有該方法,向上轉型後會遺失該方法,所以執行JNC的Wine類型參考是不能引用fun1(String a)方法。而子類別JNC重寫了fun2() ,那麼指向JNC的Wine引用會呼叫JNC中fun2()方法。

所以對於多態我們可以總結如下:

指向子類的父類引用由於向上轉型了,它只能訪問父類中擁有的方法和屬性,而對於子類中存在而父類中不存在的方法,該引用是不能使用的,儘管是重載該方法。若子類別重寫了父類別中的某些方法,在呼叫該些方法的時候,必定是使用子類別中定義的這些方法(動態連線、動態呼叫)。

對於物件導向而已,多態分為編譯時多型和執行時多型。其中編輯時多態是靜態的,主要是指方法的重載,它是根據參數列表的不同來區分不同的函數,透過編輯之後會變成兩個不同的函數,在運行時談不上多態。而運行時多態是動態的,它是透過動態綁定來實現的,也就是我們所說的多態性。

🎜多態的實現🎜🎜2.1實現條件🎜

在刚刚开始就提到了继承在为多态的实现做了准备。子类Child继承父类Father,我们可以编写一个指向子类的父类类型引用,该引用既可以处理父类Father对象,也可以处理子类Child对象,当相同的消息发送给子类或者父类对象时,该对象就会根据自己所属的引用而执行不同的行为,这就是多态。即多态性就是相同的消息使得不同的类做出不同的响应。

Java实现多态有三个必要条件:继承、重写、向上转型。

      继承:在多态中必须存在有继承关系的子类和父类。

      重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。

      向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具备技能调用父类的方法和子类的方法。

只有满足了上述三个条件,我们才能够在同一个继承结构中使用统一的逻辑实现代码处理不同的对象,从而达到执行不同的行为。

对于Java而言,它多态的实现机制遵循一个原则:当超类对象引用变量引用子类对象时,被引用对象的类型而不是引用变量的类型决定了调用谁的成员方法,但是这个被调用的方法必须是在超类中定义过的,也就是说被子类覆盖的方法。

2.2实现形式

在Java中有两种形式可以实现多态。继承和接口。

2.2.1、基于继承实现的多态

基于继承的实现机制主要表现在父类和继承该父类的一个或多个子类对某些方法的重写,多个子类对同一方法的重写可以表现出不同的行为。

public class Wine {
 private String name;
  
 public String getName() {
  return name;
 }
 
 public void setName(String name) {
  this.name = name;
 }
 
 public Wine(){
 }
  
 public String drink(){
  return "喝的是 " + getName();
 }
  
 /**
  * 重写toString()
  */
 public String toString(){
  return null;
 }
}
 
public class JNC extends Wine{
 public JNC(){
  setName("JNC");
 }
  
 /**
  * 重写父类方法,实现多态
  */
 public String drink(){
  return "喝的是 " + getName();
 }
  
 /**
  * 重写toString()
  */
 public String toString(){
  return "Wine : " + getName();
 }
}
 
public class JGJ extends Wine{
 public JGJ(){
  setName("JGJ");
 }
  
 /**
  * 重写父类方法,实现多态
  */
 public String drink(){
  return "喝的是 " + getName();
 }
  
 /**
  * 重写toString()
  */
 public String toString(){
  return "Wine : " + getName();
 }
}
 
public class Test {
 public static void main(String[] args) {
  //定义父类数组
  Wine[] wines = new Wine[2];
  //定义两个子类
  JNC jnc = new JNC();
  JGJ jgj = new JGJ();
   
  //父类引用子类对象
  wines[0] = jnc;
  wines[1] = jgj;
   
  for(int i = 0 ; i < 2 ; i++){
   System.out.println(wines[i].toString() + "--" + wines[i].drink());
  }
  System.out.println("-------------------------------");
 
 }
}
OUTPUT:
Wine : JNC--喝的是 JNC
Wine : JGJ--喝的是 JGJ
-------------------------------

在上面的代码中JNC、JGJ继承Wine,并且重写了drink() 、toString()方法,程序运行结果是调用子类中方法,输出JNC、JGJ的名称,这就是多态的表现。不同的对象可以执行相同的行为,但是他们都需要通过自己的实现方式来执行,这就要得益于向上转型了。

我们都知道所有的类都继承自超类Object,toString()方法也是Object中方法,当我们这样写时:

Object o = new JGJ();
 
  System.out.println(o.toString());

 输出的结果是Wine : JGJ。

Object、Wine、JGJ三者继承链关系是:JGJ—>Wine—>Object。所以我们可以这样说:当子类重写父类的方法被调用时,只有对象继承链中的最末端的方法才会被调用。

但是注意如果这样写:

Object o = new Wine();
 
System.out.println(o.toString());

输出的结果应该是Null,因为JGJ并不存在于该对象继承链中。

所以基于继承实现的多态可以总结如下:对于引用子类的父类类型,在处理该引用时,它适用于继承该父类的所有子类,子类对象的不同,对方法的实现也就不同,执行相同动作产生的行为也就不同。

如果父类是抽象类,那么子类必须要实现父类中所有的抽象方法,这样该父类所有的子类一定存在统一的对外接口,但其内部的具体实现可以各异。这样我们就可以使用顶层类提供的统一接口来处理该层次的方法。

2.2.2、基于接口实现的多态

继承是通过重写父类的同一方法的几个不同子类来体现的,那么就可就是通过实现接口并覆盖接口中同一方法的几不同的类体现的。

在接口的多态中,指向接口的引用必须是指定这实现了该接口的一个类的实例程序,在运行时,根据对象引用的实际类型来执行对应的方法。

继承都是单继承,只能为一组相关的类提供一致的服务接口。但是接口可以是多继承多实现,它能够利用一组相关或者不相关的接口进行组合与扩充,能够对外提供一致的服务接口。所以它相对于继承来说有更好的灵活性。

三、经典实例。

通过上面的讲述,可以说是对多态有了一定的了解。现在趁热打铁,看一个实例。该实例是有关多态的经典例子,摘自:http://blog.csdn.net/thinkGhoster/archive/2008/04/19/2307001.aspx。

public class A {
 public String show(D obj) {
  return ("A and D");
 }
 
 public String show(A obj) {
  return ("A and A");
 }
 
}
 
public class B extends A{
 public String show(B obj){
  return ("B and B");
 }
  
 public String show(A obj){
  return ("B and A");
 }
}
 
public class C extends B{
 
}
 
public class D extends B{
 
}
 
public class Test {
 public static void main(String[] args) {
  A a1 = new A();
  A a2 = new B();
  B b = new B();
  C c = new C();
  D d = new D();
   
  System.out.println("1--" + a1.show(b));
  System.out.println("2--" + a1.show(c));
  System.out.println("3--" + a1.show(d));
  System.out.println("4--" + a2.show(b));
  System.out.println("5--" + a2.show(c));
  System.out.println("6--" + a2.show(d));
  System.out.println("7--" + b.show(b));
  System.out.println("8--" + b.show(c));
  System.out.println("9--" + b.show(d)); 
 }
}

运行结果:

1--A and A
2--A and A
3--A and D
4--B and A
5--B and A
6--A and D
7--B and B
8--B and B
9--A and D

在这里看结果1、2、3还好理解,从4开始就开始糊涂了,对于4来说为什么输出不是“B and B”呢?

首先我们先看一句话:当超类对象引用变量引用子类对象时,被引用对象的类型而不是引用变量的类型决定了调用谁的成员方法,但是这个被调用的方法必须是在超类中定义过的,也就是说被子类覆盖的方法。这句话对多态进行了一个概括。其实在继承链中对象方法的调用存在一个优先级:this.show(O) 、super.show(O)、this.show((super)O) 、super.show((super)O) 。

分析:

从上面的程序中我们可以看出A、B、C、D存在如下关系。

首先我們分析5,a2.show(c) ,a2是A類型的引用變量,所以this就代表了A,a2.show(c) ,它在A類中找發現沒有找到,於是到A的超類別中找(super),由於A沒有超類別(Object除外),所以跳到第三級,也就是this.show((super)O) ,C的超類別有B、A,所以(super)O為B、A,this同樣是A,這裡在A中找到了show(A obj),同時由於a2是B類的一個引用且B類重寫了show(A obj) ,因此最終會調用子類B類別的show(A obj)方法,結果也就是B and A。

按照同樣的方法我也可以確認其他的答案。

方法已經找到了但是我們這裡還是存在一點疑問,我們還是來看這句話:當超類別物件引用變數引用子類別物件時,被引用物件的類型而不是引用變數的類型決定了呼叫誰的成員方法,但是這個被呼叫的方法必須是在超類別中定義過的,也就是說被子類別覆蓋的方法。這我們用一個例子來說明這句話所代表的含義:a2.show(b) ;

這裡a2是引用變量,為A類型,它引用的是B對象,因此按照上面那句話的意思是說有B來決定呼叫誰的方法,所以a2.show(b)應該要呼叫B中的show(B obj) ,產生的結果應該是“B and B”,但是為什麼會與前面的運行結果產生差異呢?這裡我們忽略了後面那句話“但是這兒被呼叫的方法必須是在超類別中定義過的”,那麼show(B obj)在A類中存在嗎?根本就不存在!所以這句話在這裡不適用?那麼難道是這句話錯誤了?非也!其實這句話還隱含這句話:它還是要按照繼承鏈中呼叫方法的優先權來確認。所以它才會在A類別中找到show(A obj) ,同時由於B重寫了該方法所以才會呼叫B類別中的方法,否則就會呼叫A類別中的方法。

所以多態性機制遵循的原則概括為:當超類別物件引用變數引用子類別物件時,被引用物件的類型而不是引用變數的類型決定了呼叫誰的成員方法,但是這個被呼叫的方法必須是在超類別中定義過的,也就是說被子類別覆寫的方法,但是它仍然要根據繼承鏈中方法呼叫的優先權來確認方法,該優先權為:this.show(O) 、super.show (O) 、this.show((super)O) 、super.show((super)O) 。

總結

以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或工作能帶來一定的幫助,如果有疑問大家可以留言交流。

更多深入理解Java三大特性中的多態相關文章請關注PHP中文網!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn