java进阶 - 多态

运行时多态性是面向对象程序设计代码重用的一个最强大机制,Java多态性的概念也可以被说成“一个接口,多个方法”。Java实现运行时多态性的基础是动态方法调度,它是一种在运行时而不是在编译期调用重载方法的机制。

方法的重写Overriding和重载Overloading是Java多态性的不同表现。重写Overriding是父类与子类之间多态性的一种表现,重载Overloading是一个类中多态性的一种表现。如果在子类中定义某方法与其父类有相同的名称和参数,我们说该方法被重写(Overriding)。子类的对象使用这个方法时,将调用子类中的定义,对它而言,父类中的定义如同被“屏蔽”了。如果在一个类中定义了多个同名的方法,它们或有不同的参数个数或有不同的参数类型,则称为方法的重载(Overloading)。Overloaded的方法是可以改变返回值的类型。方法的重写Overriding和重载Overloading是Java多态性的不同表现。当超类对象引用变量引用子类对象时,被引用对象的类型而不是引用变量的类型决定了调用谁的成员方法,但是这个被调用的方法必须是在超类中定义过的,也就是说被子类覆盖的方法。 (但是如果强制把超类转换成子类的话,就可以调用子类中新添加而超类没有的方法了。)



继承是子类使用父类的方法,而多态则是父类使用子类的方法。

其技术上的区别是绑定时期,晚期绑定一定是多态。


在Java中,父类的变量可以引用父类的实例,也可以引用子类的实例。

多态存在的三个必要条件:要有继承、要有重写、父类变量引用子类对象。

当使用多态方式调用方法时:

  • 首先检查父类中是否有该方法,如果没有,则编译错误;如果有,则检查子类是否覆盖了该方法。
  • 如果子类覆盖了该方法,就调用子类的方法,否则调用父类方法。
多态的一个好处是:当子类比较多时,也不需要定义多个变量,可以只定义一个父类类型的变量来引用不同子类的实例。

多态的概念

  多态==晚绑定

  不要把函数重载理解为多态。

  因为多态是一种运行期的行为,不是编译期的行为。

  多态:父类型的引用可以指向子类型的对象。

  比如 Parent p = new Child();

    当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;

    如果有,再去调用子类的该同名方法

  (注意此处,静态static方法属于特殊情况,静态方法只能继承,不能重写Override,如果子类中定义了同名同形式的静态方法,它对父类方法只起到隐藏的作用。调用的时候用谁的引用,则调用谁的版本。)

  (参考学习链接:http://docs.oracle.com/javase/tutorial/java/IandI/override.html)  

  如果想要调用子类中有而父类中没有的方法,需要进行强制类型转换,如上面的例子中,将p转换为子类Child类型的引用。

  因为当用父类的引用指向子类的对象,用父类引用调用方法时,找不到父类中不存在的方法。这时候需要进行向下的类型转换,将父类引用转换为子类引用。     

 

结合实例说明               

  下面举个例子(有问题的代码已注释):

  主要讲讲两种类型转换和两种编译时候的错误。  

复制代码
public class PolyTest
{
    public static void main(String[] args)
    {
        
        //向上类型转换
        Cat cat = new Cat();
        Animal animal = cat;
        animal.sing();

                
        //向下类型转换
        Animal a = new Cat();
        Cat c = (Cat)a;
        c.sing();
        c.eat();


        //编译错误
        //用父类引用调用父类不存在的方法
        //Animal a1 = new Cat();
        //a1.eat();
        
        //编译错误
        //向下类型转换时只能转向指向的对象类型        
        //Animal a2 = new Cat();
        //Cat c2 = (Dog)a2;
        


    }
}
class Animal
{
    public void sing()
    {
        System.out.println("Animal is singing!");
    }
}
class Dog extends Animal
{
    public void sing()
    {
        System.out.println("Dog is singing!");
    }
}
class Cat extends Animal
{
    public void sing()
    {
        System.out.println("Cat is singing!");
    }
    public void eat()
    {
        System.out.println("Cat is eating!");
    }
}
复制代码

  

  例子的执行结果:

  

  这段代码:  

 

  Cat类中定义了eat()方法,但是Animal类中没有这个方法,a1引用是Animal类的,所以找不到,编译时出错:

  

 

两种类型的类型转换

  (1)向上类型转换(Upcast):将子类型转换为父类型。

  对于向上的类型转换,不需要显示指定,即不需要加上前面的小括号和父类类型名。

  

 

  (2)向下类型转换(Downcast):将父类型转换为子类型。

  对于向下的类型转换,必须要显式指定,即必须要使用强制类型转换

  

 

  并且父类型的引用必须指向子类的对象,即指向谁才能转换成谁。

  不然也会编译出错:

 

 

  因为父类引用指向的是Cat类的对象,而要强制转换成Dog类,这是不可能的。


动态绑定

为了理解多态的本质,下面讲一下Java调用方法的详细流程。

1) 编译器查看对象的声明类型和方法名。

假设调用 obj.func(param),obj 为 Cat 类的对象。需要注意的是,有可能存在多个名字为func但参数签名不一样的方法。例如,可能存在方法 func(int) 和 func(String)。编译器将会一一列举所有 Cat 类中名为func的方法和其父类 Animal 中访问属性为 public 且名为func的方法。

这样,编译器就获得了所有可能被调用的候选方法列表。

2) 接下来,编泽器将检查调用方法时提供的参数签名。

如果在所有名为func的方法中存在一个与提供的参数签名完全匹配的方法,那么就选择这个方法。这个过程被称为 重载解析(overloading resolution)。例如,如果调用 func("hello"),编译器会选择 func(String),而不是 func(int)。由于自动类型转换的存在,例如 int 可以转换为 double,如果没有找到与调用方法参数签名相同的方法,就进行类型转换后再继续查找,如果最终没有匹配的类型或者有多个方法与之匹配,那么编译错误。

这样,编译器就获得了需要调用的方法名字和参数签名。

3) 如果方法的修饰符是private、static、final(static和final将在后续讲解),或者是构造方法,那么编译器将可以准确地知道应该调用哪个方法,我们将这种调用方式 称为 静态绑定(static binding)

与此对应的是,调用的方法依赖于对象的实际类型, 并在运行时实现动态绑。例如调用 func("hello"),编泽器将采用动态绑定的方式生成一条调用 func(String) 的指令。

4)当程序运行,并且釆用动态绑定调用方法时,JVM一定会调用与 obj 所引用对象的实际类型最合适的那个类的方法。我们已经假设 obj 的实际类型是 Cat,它是 Animal 的子类,如果 Cat 中定义了 func(String),就调用它,否则将在 Animal 类及其父类中寻找。

每次调用方法都要进行搜索,时间开销相当大,因此,JVM预先为每个类创建了一个 方法表(method lable),其中列出了所有方法的名称、参数签名和所属的类。这样一来,在真正调用方法的时候,虚拟机仅查找这个表就行了。在上面的例子中,JVM 搜索 Cat 类的方法表,以便寻找与调用 func("hello") 相匹配的方法。这个方法既有可能是 Cat.func(String),也有可能是 Animal.func(String)。注意,如果调用super.func("hello"),编译器将对父类的方法表迸行搜索。

假设 Animal 类包含cry()、getName()、getAge() 三个方法,那么它的方法表如下:
cry() -> Animal.cry()
getName() -> Animal.getName()
getAge() -> Animal.getAge()

实际上,Animal 也有默认的父类 Object(后续会讲解),会继承 Object 的方法,所以上面列举的方法并不完整。

假设 Cat 类覆盖了 Animal 类中的 cry() 方法,并且新增了一个方法 climbTree(),那么它的参数列表为:
cry() -> Cat.cry()
getName() -> Animal.getName()
getAge() -> Animal.getAge()
climbTree() -> Cat.climbTree()

在运行的时候,调用 obj.cry() 方法的过程如下:
  • JVM 首先访问 obj 的实际类型的方法表,可能是 Animal 类的方法表,也可能是 Cat 类及其子类的方法表。
  • JVM 在方法表中搜索与 cry() 匹配的方法,找到后,就知道它属于哪个类了。
  • JVM 调用该方法。

你可能感兴趣的:(java进阶)