在物件導向的程式設計語言中,多型是繼資料抽象化和繼承之後的第三種基本特性。多態透過分離做什麼和怎麼做,從另一個角度將介面和實作分開。在一開始接觸多態性這個詞的時候,我們或許會因為這個詞本身而感到困惑,如果我們把多態改稱作“動態綁定”,相信很多人就能理解他的深層含義。通常的,我們把動態綁定也叫做後期綁定,運行時綁定。
通常,我們將一個方法呼叫同一個方法主體關聯起來稱作綁定。如果在程式執行前進行綁定,我們將這種綁定方法稱為前期綁定。在面向過程語言中,例如c,這種方法是預設的也是唯一的。如果我們在java中採用前期綁定,很有可能編譯器會因為在這龐大的繼承實作體系中去綁定哪個方法而感到困惑。解決的辦法就是動態綁定,這種後期綁定的方法,在運行的時候根據物件的類型進行綁定。
在java中,動態綁定是預設的行為。但是在類別中,普通的方法會採用這種動態綁定的方法,也有一些情況並不會自然的發生動態綁定。
如果一個屬性被final修飾,則意義是:在初始化之後不能被更改。
如果一個方法被final修飾,意義則是不能被覆寫。我們常常喜歡從宏觀的角度這樣說,但是我們真正的被final修飾的方法為什麼不能被覆蓋呢?因為final修飾詞其實其實是關閉了動態綁定。在java中被final修飾的內容不能採用動態綁定的方法,不能動態綁定就沒有多型態的概念,自然也就不能被覆寫。
其實我們很少把方法設定為私有。如果我們將private方法「覆蓋」掉,其實我們得到的只是一個新的方法。完全和父類別沒關係了。這一點要注意,或許面試的時候會被問到:在子類別中「覆蓋」父類私有方法是被允許而不報錯的,只不過完全是兩個沒關係的方法罷了。
#當我們了解了多態性之後可能會認為所有的事物都是可以多態地發生。其實並不是,如果我們直接存取某個域,這個存取會在編譯期進行解析,我們可以參考下面的範例:
package Polymorphic;/** * * @author QuinnNorris * 域不具有多态性 */public class polymorphics { /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub Super sup = new Sub(); System.out.println("sup.field = " + sup.field + ", sup.getField() = " + sup.getField()); Sub sub = new Sub(); System.out.println("sub.field = " + sub.field + ", sub.getField() = " + sub.getField() + ", sub.getSuperField() = " + sub.getSuperField()); } } class Super { public int field = 0; public int getField() { return field; } } class Sub extends Super { public int field = 1; public int getField() { return field; } public int getSuperField() { return super.field; } }
輸出結果:
sup.field = 0, sup.getField() = 1 sub.field = 1, sub.getField() = 1, sub.getSuperField() = 0
這個範例告訴我們,當我們呼叫一個方法時,去選擇執行哪個方法的主體是執行時動態選擇的。但是當我們直接存取實例域的時候,編譯器直接按照這個物件所表示的類型來存取。於此情況完全相同的還有靜態方法。所以我們可以做出這種總結:
普通方法:根據物件實體的類型動態綁定
域和靜態方法:根據物件所表現的型別前期綁定
通俗地講,普通的方法我們看new後面的是什麼型別;域和靜態方法我們看=前面聲明的是什麼類型。
儘管這看來好像是一個非常容易讓人混懸的問題。但是在實踐中,實際上從來(或很少)不會發生。首先,那些不把實例域設定為private的程式設計師基本上已經全都被炒魷魚了(實例域很少被修飾成public)。其次我們很少會將自己在子類別中建立的網域設定成和父類別一樣的名字。
通常,建構器是一個很獨特的存在。牽涉到多態的時候也是如此。儘管構造者並不具有多態性(實際上他們是有static來修飾的,儘管該static是被隱式聲明的),但是我們還是有必要理解構造器的工作原理。
父類別的建構器總是在子類別建構器呼叫的過程中被調用,而且依照繼承層次逐漸向上的連結,以使每個父類別的建構器都能被正確的呼叫。這樣做是很有必要的,因為構造器有一個特殊的任務,檢查物件是否被正確的構造。子類別方法只能存取自己的成員,不能存取父類別中的成員。只有基底類別的構造器才有適當的權限對自己的元素進行初始化。因此必須要讓每個構造器都能得到調用,否則不能建構出正確的完整的物件。
package Polymorphic; public class Father { /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub new F(); } }class A { A() { System.out.println("A"); } }class B extends A { B() { System.out.println("B"); } }class C extends B { C() { System.out.println("C"); } }class D { D() { System.out.println("D"); } }class E { E() { System.out.println("E"); } }class F extends C { private D d = new D(); private E e = new E(); F() { System.out.println("F"); } }
輸出結果:
A B C D E F
#看似偶然的「ABCDEF」的輸出結果,其實就是我們精心安排的。
這個範例非常直覺的說明了建構器的呼叫法則,有以下三個步驟:
调用父类构造器。这个步骤会反复递归进去,直到最祖先的类,依次向下调用构造器。
按声明顺序调用成员的初始化构造器方法。
调用子类构造器的主体。
可能我说了这个顺序,大家马上就会想到super。是的没错,super()确实可以显示的调用父类中自己想要调用的构造方法,但是super()必须放在构造器的第一行,这个是规定。我们的顺序是没有任何问题的,或者说其实在F的构造器中第一句是super()。只不过我们默认省略了。
java在se5中添加了协变返回类型,它表示在子类中的被覆盖方法可以返回父类这个方法的返回类型的某种子类。
package Polymorphic;/** * * @author QuinnNorris * 协变返回类型 */public class covariant { /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub A b = new B(); b.getC().print(); A a = new A(); a.getC().print(); } }class A{ public C getC() { return new C(); } }class B extends A{ public D getC(){ return new D(); } }class C{ public void print(){ System.out.println("C"); } }class D extends C{ public void print(){ System.out.println("D"); } }
输出结果:
D C
在上面的例子中,D类是继承于C类的,B类是继承于A类的,所以在B类覆盖的getC方法中,可以将返回类型协变成,C类的某个子类(D类)的类型。
通常,继承并不是我们的首选,能用组合的方法尽量用组合,这种手段更灵活,如果你的代码中is-a和is-like-a过多,你就应该考虑考虑是不是该换成has-a一些了。一条通用的准则是:用继承表达行为间的差异,并用字段表达状态上的变化。
而且在用继承的时候,我们会经常涉及到向上转型和向下转型。在java中,所有的转型都会得到检查。即使我们只是进行一次普通的加括号的类型转换,在进入运行期时仍然会对其进行检查,以便保证它的确是我们希望的那种类型。如果不是,就返回一个ClassCastException(类转型异常)。这种在运行期间对类型进行检查的行为称作”运行时类型识别(RTTI)“。
在面向对象的程序设计语言中,多态是继数据抽象和继承之后的第三种基本特性。多态通过分离做什么和怎么做,从另一个角度将接口和实现分离开来。在一开始接触多态这个词的时候,我们或许会因为这个词本身而感到困惑,如果我们把多态改称作“动态绑定”,相信很多人就能理解他的深层含义。通常的,我们把动态绑定也叫做后期绑定,运行时绑定。
通常,我们将一个方法调用同一个方法主体关联起来称作绑定。如果在程序执行前进行绑定,我们将这种绑定方法称作前期绑定。在面向过程语言中,比如c,这种方法是默认的也是唯一的。如果我们在java中采用前期绑定,很有可能编译器会因为在这庞大的继承实现体系中去绑定哪个方法而感到迷惑。解决的办法就是动态绑定,这种后期绑定的方法,在运行的时候根据对象的类型进行绑定。
在java中,动态绑定是默认的行为。但是在类中,普通的方法会采用这种动态绑定的方法,也有一些情况并不会自然的发生动态绑定。
如果一个属性被final修饰,则含义是:在初始化之后不能被更改。
如果一个方法被final修饰,含义则是不能被覆盖。我们常常喜欢从宏观的角度这样说,但是我们真正的被final修饰的方法为什么不能被覆盖呢?因为final修饰词其实实际上关闭了动态绑定。在java中被final修饰的内容不能采用动态绑定的方法,不能动态绑定就没有多态的概念,自然也就不能被覆盖。
其实我们很少把方法设定为私有。如果我们将private方法“覆盖”掉,其实我们获得的只是一个新的方法。完全和父类没关系了。这一点要注意,或许面试的时候会被问到:在子类中“覆盖”父类私有方法是被允许而不报错的,只不过完全是两个没关系的方法罢了。
当我们了解了多态性之后可能会认为所有的事物都是可以多态地发生。其实并不是,如果我们直接访问某个域,这个访问会在编译期进行解析,我们可以参考下面的例子:
package Polymorphic;/** * * @author QuinnNorris * 域不具有多态性 */public class polymorphics { /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub Super sup = new Sub(); System.out.println("sup.field = " + sup.field + ", sup.getField() = " + sup.getField()); Sub sub = new Sub(); System.out.println("sub.field = " + sub.field + ", sub.getField() = " + sub.getField() + ", sub.getSuperField() = " + sub.getSuperField()); } } class Super { public int field = 0; public int getField() { return field; } } class Sub extends Super { public int field = 1; public int getField() { return field; } public int getSuperField() { return super.field; } }
输出结果:
sup.field = 0, sup.getField() = 1 sub.field = 1, sub.getField() = 1, sub.getSuperField() = 0
这个例子告诉我们,当我们调用一个方法时,去选择执行哪个方法的主体是运行时动态选择的。但是当我们直接访问实例域的时候,编译器直接按照这个对象所表示的类型来访问。于此情况完全相同的还有静态方法。所以我们可以做出这种总结:
普通方法:根据对象实体的类型动态绑定
域和静态方法:根据对象所表现的类型前期绑定
通俗地讲,普通的方法我们看new后面的是什么类型;域和静态方法我们看=前面声明的是什么类型。
尽管这看来好像是一个非常容易让人混悬哦的问题。但是在实践中,实际上从来(或者说很少)不会发生。首先,那些不把实例域设置为private的程序员基本上已经全都被炒鱿鱼了(实例域很少被修饰成public)。其次我们很少会将自己在子类中创建的域设置成和父类一样的名字。
通常,构造器是一个很独特的存在。涉及到多态的时候也是如此。尽管构造器并不具有多态性(实际上他们是有static来修饰的,尽管该static是被隐式声明的),但是我们还是有必要理解一下构造器的工作原理。
父类的构造器总是在子类构造器调用的过程中被调用,而且按照继承层次逐渐向上的链接,以使每个父类的构造器都能被正确的调用。这样做是很有必要的,因为构造器有一项特殊的任务,检查对象是否被正确的构造。子类方法只能访问自己的成员,不能访问父类中的成员。只有基类的构造器才具有恰当的权限对自己的元素进行初始化。因此必须要让每个构造器都能得到调用,否则不能构造出正确的完整的对象。
package Polymorphic; public class Father { /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub new F(); } }class A { A() { System.out.println("A"); } }class B extends A { B() { System.out.println("B"); } }class C extends B { C() { System.out.println("C"); } }class D { D() { System.out.println("D"); } }class E { E() { System.out.println("E"); } }class F extends C { private D d = new D(); private E e = new E(); F() { System.out.println("F"); } }
输出结果:
A B C D E F
看似偶然的“ABCDEF”的输出结果,实际上是我们精心安排的。
这个例子非常直观的说明了构造器的调用法则,有以下三个步骤:
调用父类构造器。这个步骤会反复递归进去,直到最祖先的类,依次向下调用构造器。
按声明顺序调用成员的初始化构造器方法。
调用子类构造器的主体。
可能我说了这个顺序,大家马上就会想到super。是的没错,super()确实可以显示的调用父类中自己想要调用的构造方法,但是super()必须放在构造器的第一行,这个是规定。我们的顺序是没有任何问题的,或者说其实在F的构造器中第一句是super()。只不过我们默认省略了。
java在se5中添加了协变返回类型,它表示在子类中的被覆盖方法可以返回父类这个方法的返回类型的某种子类。
package Polymorphic;/** * * @author QuinnNorris * 协变返回类型 */public class covariant { /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub A b = new B(); b.getC().print(); A a = new A(); a.getC().print(); } }class A{ public C getC() { return new C(); } }class B extends A{ public D getC(){ return new D(); } }class C{ public void print(){ System.out.println("C"); } }class D extends C{ public void print(){ System.out.println("D"); } }
输出结果:
D C
在上面的例子中,D类是继承于C类的,B类是继承于A类的,所以在B类覆盖的getC方法中,可以将返回类型协变成,C类的某个子类(D类)的类型。
通常,继承并不是我们的首选,能用组合的方法尽量用组合,这种手段更灵活,如果你的代码中is-a和is-like-a过多,你就应该考虑考虑是不是该换成has-a一些了。一条通用的准则是:用继承表达行为间的差异,并用字段表达状态上的变化。
而且在用继承的时候,我们会经常涉及到向上转型和向下转型。在java中,所有的转型都会得到检查。即使我们只是进行一次普通的加括号的类型转换,在进入运行期时仍然会对其进行检查,以便保证它的确是我们希望的那种类型。如果不是,就返回一个ClassCastException(类转型异常)。这种在运行期间对类型进行检查的行为称作”运行时类型识别(RTTI)“。
以上就是java深入理解动态绑定的内容,更多相关内容请关注PHP中文网(www.php.cn)!