Java中的动态绑定详解

本来觉得动态绑定的知识点不多, <Thinking In Java>里面讲的也确实不多, 但是看了几个例子之后才发现自己也是一知半解. 要讲动态绑定,自然也得讲方法的重写与隐藏, 此处做一个读书笔记吧, 一是备忘, 二是整合下知识.关于动态绑定的实质机制,如果还有更深入学习的兴趣,可以看去看JVM虚拟机方面的书,由于这方面博主理解的不深, 就不做详细的描述了.


在讲解动态绑定之前, 先让我们简单过一遍重载, 重写 与 隐藏 的概念, 已经掌握的童鞋可以忽略这一段.

先提一下,"方法签名"相同的意思是"方法名+参数列表"(参数类型 个数 顺序)都要相同!

1. 重载: 同一个类内, 定义多个具有相同方法名, 但是参数列表不同的方法是属于重载. 重载的方法, 方法签名是不一样的.

2. 重写: 子类定义了与父类具有相同方法签名的方法(即子类覆盖了父类的方法). 子类和父类的方法签名必须一致.

3. 隐藏: 只有成员变量(不管是否是静态变量) 和 静态方法可以被隐藏, 隐藏方法的语法与重写相同, 同样要求方法签名一致.注意两者的语法虽然相同, 但是  不能将隐藏视为覆盖.


动态绑定的概念:

将一个方法调用同一个方法主体连接到一起就称为“绑定”(Binding).动态绑定意味着绑定在运行期间进行,以对象的类型为基础.    

(引自  <<Thinking in Java>>)

与动态绑定相对的,自然是静态绑定,它意味着,方法的关联在编译期就已经确定了.


关于动态绑定的机制, 我们先给出结论:

1. 使用了什么类的句柄,就只能调用那个句柄类中存在的方法,不管句柄指向的具体类型是什么.(除非我们强制转换句柄类型, 那是另外一回事了)

下面的代码编译就不能通过,因为使用了什么类的句柄,就只能调用那个句柄类中存在的方法.

<span style="font-family:SimSun;">//使用了什么类的句柄, 就只能调用那个句柄类中存在的方法.
class SonBinding extends FatherBinding{
    public void play(){
	    System.out.println("Father-play");
	}
}

public class FatherBinding{
    public void play(int a){
	    System.out.println("Father-play");
	}
	
	public static void main(String[] args){
	    FatherBinding fd = new SonBinding();
		fd.play();//尽管子类中存在play()方法, 但是我们使用的是FatherBinding的句柄, FatherBinding中并不存在play()方法, 编译不能通过
}</span>

2. 动态绑定的时候需要通过查询实例的方法表来确定关联的方法(这就是为什么书中说"以对象的类型为基础").

       "一个通过父类指向子类的实例,只能调用父类中存在方法. 一个实例的所有实例方法的实际入口地址存储在方法区的一张虚表中, 如果一个        方法没有被重写(即没被覆盖),那么该方法的入口地址属于父类方法,如果被重写了(被覆盖了),那么入口地址就会被替换为覆盖后的地址."        (引自csdn sxiaobei 有改动)

上面红色标记的话也可以理解为:" 使用一个被声明为父类句柄,却指向一个子类实例的句柄来调用方法时候(向上转型),只能够调用父类中存在的方法.如果父类中的方法没有被重写,那么就调用父类的方法,如果被重写了,就调用重写后的方法. "


结合例子来看动态绑定的机制:

<span style="font-family:SimSun;">class SonBinding extends FatherBinding{
    public void play(){
	   System.out.println("Son-play");
	}
}

public class FatherBinding{
    public void play(){
	    System.out.println("Father-play");
	}
	
	public static void main(String[] args){
	    FatherBinding fd = new SonBinding();//将一个SonBinding的实例赋给了FatherBing的句柄
		fd.play();//动态绑定, play()被关联到了Son类的play方法, 输出为: Son-play
	}
}</span>


解析:

上面的例子中, 我们虽然将FatherBinding的句柄指向了SonBinding的实例, 但是按照第一点, fd句柄只能调用FatherBinding类中存在的方法, 显然play()是存在的. 那么为什么输出的却是Son-play呢?  "以对象的类型为基础", 我们查询的是SonBinding的方法表. 请注意, SonBinding类中覆盖了FatherBinding的play()方法, 按照第二点, play()方法的入口地址已经被替换为覆盖后的地址, SonBinding的方法表中play()的入口地址不再是属于父类方法, 而是属于子类方法, 那么查询完匹配到的方法自然是子类的play, 输出就是Son-play. 


再来看一个例子:

<span style="font-family:SimSun;">//子类并没有重写play方法
class SonBinding extends FatherBinding{}

public class FatherBinding{
    public void play(){
	    System.out.println("Father-play");
	}
	
	public static void main(String[] args){
	    FatherBinding fd = new SonBinding();
		fd.play();//输出为Father-play
	}
}</span>
解析:同样 fd 句柄只能调用FatherBinding类中存在的方法, 这里play()显然也是存在的. 以对象为基础, 查询SonBinding的方法表( SonBinding继承了FatherBinding, 方法表中自然有play()方法 ), 这里SonBinding并没有覆盖play(), 因此play()的入口地址依然属于父类, 调用的是父类的play(), 输出为 Father-play.


接着再让我们看一个很特殊的例子, 这个例子就是我们在最开始的时候要明确重写, 重载 的概念的原因

<span style="font-family:SimSun;">/**
*  @author csdn libertine1993
*/
class Father{ 
   protected void play(int c){
       System.out.println("Father's Play with int c callled");
   }
}

class Son extends Father{
    protected void play(char c){ //这里Father和Son的play的签名不一样, 既不是重写也不是重载
       System.out.println("Son's Play with char c callled");
    } 
}


public class Main{
    public static void main(String[] args){
	    Father fd = new Son();  //将Son的对象句柄赋值给Father句柄 
		char c = 'c';
		fd.play(c);        //输出为Father's Play with int c called
	}
}
</span>

说这个例子奇怪, 奇怪点有二, 我们一点一点解析.

1) 按照第一点, 使用了什么类的句柄, 就只能调用那个句柄类中存在的方法. 这里Father类明显没有play(char)的方法, 然而编译通过了.岂不与第一点矛盾?

解析: 

这并不矛盾.实际上 编译的时候(注意是编译的时候), JVM虽然没能在Father类中找到完美匹配play(char)的方法, 但是char 比较特殊可以被自动转型为 int (有兴趣可以继续深入). 这样但是编译的时候, 把play(char) 转型为 play(int), 显然play(int)是存在的,  因此编译通过了. 如果你把Father的play的参数换成String , 那就编译不通过了.


2)"以对象为基础", 查找Son的方法表, 能够找到play(char), 输出不应该是Son's Play with char c called 吗?

解析: 

请注意使用了什么类的句柄, 就只能调用那个句柄类中存在的方法. 在Son的方法表中, 我们查找的是father的play, 考虑到Son类中play的参数是char ,而Father中play的参数是int, 因此父类和子类的play的函数签名是不一样的, 因此Son并没有覆盖Father的play方法(当然也不属于Java的重载, 因为是在不同一个类中). 那么, 在Son中, play(int)的入口地址依然属于父类, 因此调用的自然是Father的play(int).



最后做一点点补充, 关于动态绑定我们还需要知道:

1.被static, final, private修饰的方法不适用动态绑定, 都是静态绑定的.

2.static方法可以被继承,但是不能被子类覆盖( 但可以被隐藏 ), 因此当我们向上转型的时候, 调用的是父类的方法.

3.只有实例的方法可以被重写(static方法是类方法, 只能被隐藏)

上面3点可以看下面的例子

<span style="font-family:SimSun;">/**
*  测试隐藏静态方法是否适用于动态绑定
*  @author csdn libertine1993
*/
class StaFather{
    public static void method(){
	    System.out.println("father-method");
	}
}

public class StaSon extends StaFather{
    public static void method(){
	    System.out.println("son-method");
	}
	
	public static void main(String[] args){
	    StaFather stafd = new StaSon();
        stafd.method();		//输出为father-method, 不适用动态绑定
	}
}</span>

4.与方法不同,在处理java类中的成员变量(实例变量和类变量)时,并不是采用运行时绑定,而是一般意义上的静态绑定。所以在向上转型的情况下,对象的方法可以找到子类,而对象的属性(成员变量)还是父类的属性(子类对父类成员变量的隐藏)。

附上博主提问的帖子, 有兴趣的可以到这里去查看原汁原味的回答:

http://bbs.csdn.net/topics/391808405

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值