伴随多态的可互换对象(Java)

来源:Java编程思想、Java核心技术 卷I

术语

  • 绑定:将 一个方法调用 同 一个方法主体 关联起来 被称作绑定。
  • 前期绑定:在程序执行之前进行绑定
  • 后期绑定:在程序运行时根据对象的类型进行绑定,也叫动态绑定或运行时绑定
  • 覆盖(重写):导出类改变其基类现有方法的行为
  • 继承:基于已存在的类构造一个新类,继承已存在的类就是复用这些类的方法和域,在此基础上还可以添加一些新的方法和域

在处理类型的层次结构时,经常想把一个对象不当做它所属的特定类型来对待,而是将其当做其基类的对象来对待,这使得人们可以编写出不依赖于特定类型的代码。列:有如下“几何形”类结构

现有类ShapeController对Shape类对象进行调用

public class ShapeController{
    public void doSomething(Shape shape){
        shape.draw();
        shape.erase();
    }

    public static void main(String[] args){
        ShapeController shapeController=new ShapeController();
        shapeController.doSomething(new Circle());
        shapeController.doSomething(new Square());
        shapeController.doSomething(new Triangle());
    }
}

这种代码设计在Java中经常遇见,其中的doSomething(Shape shape)中的shape对象也被叫做泛化对象,这种将导出类看做是它的基类的过程称为向上转型(upcasting),在doSomething()时,所有的传入对象都会被看成是一个Shape对象,那么问题来了:

要知道这里的Circle、Square以及Triangle都有draw()与erase()方法,不管调用doSomething()时传入的是Circle、Square还是Triangle对象,其向上转型机制都会将其看做Shape对象,编译器是如何知道哪一段代码应该被执行的呢?

这个问题的答案,也是面向对象程序设计的最重要的妙诀:编译器不可能产生传统意义上的函数调用。

  • 一个非面向对象的编译器产生的函数调用会引起所谓的前期绑定(C语言只有一种方法调用,就是前期绑定),这么做意味着编译器将产生一个具体函数名字的调用,而运行时将这个调用解析到将要被执行的代码的绝对地址。
  • 然而在OOP(面向对象程序)中,程序直到运行时才能够确定代码的地址,所以当消息发送到一个泛化对象时,必须采用其他的机制。
  • 为了解决这个问题,面向对象程序设计语言使用了后期绑定的概念。当向对象发送消息时,被调用的代码直到运行时才能确定(C++中使用关键字virtual来实现一个方法具备后期绑定属性,而java中动态绑定是默认行为,不需要添加额为的关键字来实现)
  • 为了执行后期绑定,就意味着编程语言必须具有某种机制(能获得对象“类型信息”的机制),以便在运行时能判断对象的类型,从而调用恰当的方法,也就是说,编译器一直不知道对象的类型,但是方法调用机制能找到正确的方法体。

Java中除了static和final(private属于final方法)之外,其他所有的方法都是后期绑定的,这意味着通常情况下,我们不必判定是否应该进行后期绑定,它会自动发生。

  • 如果某个方法是静态(static)的,它的行为就不具有多态性,因为静态方法是与类、而非与单个的对象相关联的
  • 将一个方法声明为final,可以有效的“关闭”动态绑定,禁止了子类修改该方法的语义,即该方法是唯一的
  • 此外,任何域的访问操作都将由编译器解析,因此不是多态的。比如(B extends A):在A和B中均有i属性,A.i与B.i是分配了不同的存储空间,B实际上包含两个称为i的域(它自己的和从A处得到的),然而B.i时所产生的默认域并不是A中的i域,因此为了得到A.i,必须显式的指明super.i

方法调用机制

弄清楚如何在对象上应用方法调用非常重要,假设要调用x.f(args),隐式参数x声明为类C的一个对象,下面是调用过程的详细描述:

  1. 编译器查看对象的声明类型和方法名,编译器会一一列举出C类中名为f的方法和其超类中的访问属性为public的且名为f的方法(超类的私有方法不可访问)。
  2. 接下来,编译器将查看调用方法时提供的参数类型,如果在所有名为f的方法中存在一个与提供的参数args类型完全匹配,就选择这个方法,这个过程被称为重载解析,如果没有找到参数类型匹配的方法,或者有多个方法与之匹配,就会报出一个错误。
  3. 如果是private、static或final方法或构造器,那么编译器将可以准确的知道应该调用哪个方法,这种调用方法称为静态绑定
  4. 当程序运行,并且采用动态绑定调用方法时,虚拟机一定调用最合适的方法,假设x的实际类型是C,B是C的超类(C extends B),如果C类中存在要调用的方法f,则直接调用它,否则将在C类的超类B类中查找合适的f方法,依次类推。

方法表

在方法调用机制中,每次调用方法都要进行搜索,时间开销很大,因此虚拟机预先为每个类创建了一个方法表,其中列出了所有方法的签名和实际调用的方法,这样一来,在真正调用方法的时候,虚拟机仅需查询这个表就行了。

列:类Circle的方法表类似于

Circle:

         draw() ->Circle.draw()

         erase() ->Circle.erase()

         draw() ->Shape.draw()

         erase() ->Shape.erase()

         超类Object中继承而的方法...

Circle c=new Circle();
c.draw();

执行c.draw()的解析过程为:

  1. 首先,虚拟机提取c的实际类型的方法表,即Circle的方法表。
  2. 接下来,虚拟机搜索定义draw签名的类,此时虚拟机已经知道应该调用哪个方法了。
  3. 最后,虚拟机调用方法。

强调

  • 动态绑定有一个非常重要的特性:无需对现存的代码进行修改,就可以对程序进行扩展。
  • 在覆盖一个方法的时候,子类方法不能低于超类方法的可见性。
  • private、static以及final声明的方法以及构造器的调用方式为静态绑定,只有不是private、static以及final声明的方法,才会采用动态绑定。
  • 如果将一个类声明为final,只有其中的方法自动地成为final,而不包括域。

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值