In object-oriented programming languages, polymorphism is the third basic feature after data abstraction and inheritance. Polymorphism separates the interface and implementation from another perspective by separating what to do and how to do it. When we first come into contact with the word polymorphism, we may be confused by the word itself. If we change polymorphism to "dynamic binding", I believe many people will be able to understand its deeper meaning. Usually, we call dynamic binding late binding and runtime binding.
Usually, we call a method in the same The association of a method body is called a binding. If binding is performed before program execution, we call this binding method early binding. In procedural languages, such as C, this method is the default and only one. If we use early binding in Java, it is very likely that the compiler will be confused as to which method to bind in this huge inheritance implementation system. The solution is dynamic binding. This late binding method binds according to the type of the object at runtime.
In java, dynamic binding is the default behavior. However, in a class, ordinary methods will use this dynamic binding method, and there are also some situations where dynamic binding does not occur naturally.
If a property is modified by final, the meaning is: it cannot be changed after initialization.
If a method is modified by final, it means that it cannot be overridden. We often like to say this from a macro perspective, but why can't our methods that are actually modified by final be overridden? Because the final modifier actually turns off dynamic binding. In Java, the content modified by final cannot be dynamically bound. Without dynamic binding, there is no concept of polymorphism, and naturally it cannot be overridden.
In fact, we rarely set methods as private. If we "overwrite" the private method, what we actually get is a new method. It has nothing to do with the parent class at all. You should pay attention to this. You may be asked during the interview: "Overriding" the private methods of the parent class in the subclass is allowed without reporting an error. They are just two completely unrelated methods.
After we understand polymorphism, we may think that all things can occur polymorphically. Actually not, If we directly access a domain, this access will be parsed during compilation, we can refer to the following example:
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; } }
Output result:
sup.field = 0, sup.getField() = 1 sub.field = 1, sub.getField() = 1, sub.getSuperField() = 0Copy after login
This example tells us that when we call a method, the main body to choose which method to execute is dynamically selected at runtime. But when we directly access the instance field, the compiler directly accesses it according to the type represented by this object. The same situation exists for static methods. So we can make this summary:
Normal method: dynamic binding according to the type of object entity
Domain and static Method: Early binding based on the type represented by the object
#In layman's terms, for ordinary methods, we look at the type after new; for domain and static methods, we look at = front What type is declared.
Although this seems to be a very confusing question. But in practice, this never (or rarely) happens. First, programmers who don't set instance fields to private have basically been fired (instance fields are rarely modified to be public). Secondly, we rarely set the fields we create in the subclass to the same name as the parent class.
Generally, the constructor is a very unique existence. The same is true when it comes to polymorphism. Although constructors are not polymorphic (in fact they are decorated with static, although the static is implicitly declared), it is still necessary to understand how constructors work.
The constructor of the parent class is always called during the call of the constructor of the subclass, and according to inheritance The chaining is gradually upward so that the constructor of each parent class can be called correctly. This is necessary because the constructor has a special task, checking whether the object has been constructed correctly. Subclass methods can only access their own members, not members of the parent class. Only the base class constructor has the appropriate permissions to initialize its own elements. Therefore, every constructor must be called, otherwise a correct and complete object cannot be constructed.
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"); } }
##The seemingly accidental output of "ABCDEF" is actually our Carefully arranged.Output results:
A B C D E FCopy after loginCopy after login
This example very intuitively illustrates the constructor calling rule, which has the following three steps:
调用父类构造器。这个步骤会反复递归进去,直到最祖先的类,依次向下调用构造器。
按声明顺序调用成员的初始化构造器方法。
调用子类构造器的主体。
可能我说了这个顺序,大家马上就会想到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 CCopy after loginCopy after login
在上面的例子中,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() = 0Copy after login
这个例子告诉我们,当我们调用一个方法时,去选择执行哪个方法的主体是运行时动态选择的。但是当我们直接访问实例域的时候,编译器直接按照这个对象所表示的类型来访问。于此情况完全相同的还有静态方法。所以我们可以做出这种总结:
普通方法:根据对象实体的类型动态绑定
域和静态方法:根据对象所表现的类型前期绑定
通俗地讲,普通的方法我们看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 FCopy after loginCopy after login
看似偶然的“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 CCopy after loginCopy after login
在上面的例子中,D类是继承于C类的,B类是继承于A类的,所以在B类覆盖的getC方法中,可以将返回类型协变成,C类的某个子类(D类)的类型。
(四)继承设计
通常,继承并不是我们的首选,能用组合的方法尽量用组合,这种手段更灵活,如果你的代码中is-a和is-like-a过多,你就应该考虑考虑是不是该换成has-a一些了。一条通用的准则是:用继承表达行为间的差异,并用字段表达状态上的变化。
而且在用继承的时候,我们会经常涉及到向上转型和向下转型。在java中,所有的转型都会得到检查。即使我们只是进行一次普通的加括号的类型转换,在进入运行期时仍然会对其进行检查,以便保证它的确是我们希望的那种类型。如果不是,就返回一个ClassCastException(类转型异常)。这种在运行期间对类型进行检查的行为称作”运行时类型识别(RTTI)“。
以上就是java深入理解动态绑定的内容,更多相关内容请关注PHP中文网(www.php.cn)!