<span style="font-family: Arial, Helvetica, sans-serif;">Java 多态机制解析</span>
面向对象编程有三个特点:封装、继承和多态
封装:隐藏了类的内部实现机制,从而可是实现在不影响使用者的前提下改变类的内部结构,保护了内部数据。
继承:为了重用父类代码,同时为多态做准备。
多态:向对象发送消息,让该对象自行决定采取何种行为。
方法的重写、重载和动态连接构成多态性。
|
我们可以这样定义: Dog dog = new Dog(); Animal a = new Dog(); |
子类是父类的改进和扩充,所以子类在功能上比父类强大,属性也比父类更独特。
定义一个父类的引用指向一个子类对象,既可以调用使用子类强大的功能又可以抽取父类的共性。
所以,父类类型的引用可以调用父类中所有的非私有的属性和方法,对于子类中定义而父类中没有的成员,它是无可奈何的。
同时,对于一个在父类中定义,且在子类中没有被重写的方法,才可以被父类类型的引用调用。
如果在子类中重写(Override)了父类中定义的方法,在父类类型的引用变量只能调用子类中的方法,不能调用父类中的方法,这就是动态连接。
对于多态,总结如下: 1、使用父类类型的引用指向子类对象 2、该引用只能调用父类中定义的方法和变量 3、如果子类中重写了父类中的一个方法,那么调用这个方法的时候,将会调用子类中的这个方法。(动态连接,动态调用) 4、变量不能被重写(或覆盖),“重写”的概念只针对方法,若重写父类中的变量编译时将会报错。
再解多态: 多态是通过一下两种方式实现的: 1、接口和实现接口的并覆盖接口中同一方法的几个不同类实现的 2、父类和继承父类的并覆盖父类中同一方法的几个不同子类实现的 多态性: 发送消息给某个对象,让该对象自行决定该调用何种行为。 通过将子类对象引用变量赋值给超类对象引用变量来实现动态方法调用。
JAVA里没有多继承,一个类之能有一个父类。而继承的表现就是多态。一个父类可以有多个子类,而在子类里可以重写父类的方法(例如方法print()),这样每个子类里重写的代码不一样,自然表现形式就不一样。这样用父类的变量去引用不同的子类,在调用这个相同的方法print()的时候得到的结果和表现形式就不一样了,这就是多态,相同的消息(也就是调用相同的方法)会有不同的结果。 多态性应遵守的原则: 当父类对象引用变量引用子类对象时,被引用对象的类型而不是引用引用变量的类型决定了调用谁的方法,即子类对象的类型决定了调用谁的方法,但是这个被调用的方法必须是在父类中定义过的,并且被子类覆盖了的方法。
Java多态性实现机制: SUN目前的JVM实现机制,类实例的引用就是指向一个句柄(handle)的指针,这个句柄是一对指针: 一个指针指向一张表格,实际上这个表格也有两个指针(一个指针指向一个包含了对象的方法表,另外一个指向类对象,表明该对象所属的类型); 另一个指针指向一块从java堆中为分配出来内存空间。
1、通过将子类对象引用赋值给超类对象引用,来实现动态方法调用。 class Automobile{ public void play(){……} …… } class Car extends Automobile{ //重写了父类中的方法 public void play(){……} ...... } 我们可以使用: Car c = new Car(); Automobile a = c; //将子类对象引用赋值给父类对象引用 或者写成: Automobile a = new Car(); a.play(); //将调用子类中被重写过的方法play
分析: 1、为什么子类类型的对象的引用可以赋值给父类类型对象的引用? 自动实现向上转型。 通过该语句,编译器会自动将子类实例向上移动,成为通用类型Automobile。 2、a.play()将执行子类还是父类中的方法? 子类的。 在运行期间,将根据a对象这个实例的类型来获取相应的方法,所有才有多态性。一个基类的对象引用被赋予不同的子类对象引用,执行该方法时将表现出不同的行为。 在a=c执行后,仍然存在两个句柄,a和c,但是他们拥有同一块数据内存和不同的函数表。 3、不能把父类类型引用变量赋值给子类类型引用变量。 因为Java中可以自动进行向上转型,但是如果需要向下转型的话,需要进行强制类型转换。 Car c = new Car(); Automobile a = new Automobile(); a = c; //OK c = a; //Error c = (Car)a; //OK 所以,要想调用子类含有而父类不含有的方法,就需要向下转型,进行强制类型转换。 4、记住一个简单而又复杂的规则:一个类型的引用变量只能引用该类型自身含有的属性和方法。 你可能说这个规则不对的,因为父类引用指向子类对象的时候,最后执行的是子类的方法的。 Java的所有函数,除了被声明为final的,都是用后期绑定。 对于抽象类的多态: 如果子类继承的超类是一个抽象类,虽然抽象类不能通过new操作符实例化, 但是可以创建抽象类的对象引用指向子类对象,以实现运行时多态性。不过,抽象类的子类必须覆盖实现超类中的所有的抽象方法,否则子类必须被abstract修饰符修饰,当然也就不能被实例化了。
从对象的内存角度来理解试试. 假设现在有一个父类Father,它里面的变量需要占用1M内存.有一个它的子类Son,它里面的变量需要占用0.5M内存. 现在通过代码来看看内存的分配情况: Father f = new Father();//系统将分配1M内存. Son s = new Son();//系统将分配1.5M内存!因为子类中有一个隐藏的引用super 会指向父类实例,所以在实例化子类之前会先实例化一个父类,也就是说会先执行父类的构造函数.由于s中包含了父类的实例,所以s可以调用父类的方法. Son s1 = s;//s1指向那1.5M的内存. Father f1 = (Father)s;//这时f1会指向那1.5M内存中的1M内存,即是说,f1只是指向了s中实例的父类实例对象,所以f1只能调用父类的方法(存储在1M内存中),而不能调用子类的方法(存储在0.5M内存中). Son s2 = (Son)f;//这句代码运行时会报ClassCastException.因为f中只有1M内存,而子类的引用都必须要有1.5M的内存,所以无法转换. Son s3 = (Son)f1;//这句可以通过运行,这时s3指向那1.5M的内存.由于f1是由s转换过来的,所以它是有1.5M的内存的,只是它指向的只有1M内存. 示例: class Father{ 这里如果Fathor类有一个show()方法,那么形成方法覆盖,此时就可以这么写:obj.show(),此刻形成了多态. 没有方法覆盖,那你这里只能解释为父类引用去访问一个子类的方法,当然,父类引用没有这么大范围的权限,当然会报错
PS:多态实际上是一种机制,在编译时刻,会生成一张虚拟表,来记录所有覆盖的方法,没有被覆盖的方法是不会记录到这张表的.若一个父类引用调用了没有覆盖的子类方法,那么是不符合该表的,那么编译时刻就会报错. 在执行程序的时候,虚拟机会去这张虚拟表中找覆盖的方法,比如引用中实际上存的是一个子类对象引用,那么就会去找子类中的相应的覆盖的方法来执行。 |