下面几个类用于确定今天晚餐要喝的酒以及酒的温度。
class Sommelier {
Wine recommend(String meal) { ... }
}
abstract class Wine {
// 推荐酒的温度
abstract float temperature();
}
class RedWine extends Wine {
// 红酒的温度通常略高于白酒
float temperature() { return 63; }
}
class WhiteWine extends Wine {
float temperature() { return 47; }
}
class Bordeaux extends RedWine {
float temperature() { return 64; }
}
class Riesling extends WhiteWine {
// 继承WhiteWine类的温度
}
下面的例子利用上面的类推荐一种酒:
void example1() {
Wine wine = sommelier.recommend("duck");
float temp = wine.temperature();
}
example1的第二个调用中,对于wine对象我们唯一可以肯定的是它是一个Wine,但可以是Bordeaux,也可以是Riesling或其他。另外,我们可以肯定wine对象不可能是Wine类本身的实例,因为Wine类是一个抽象类。编译源代码,源代码中的wine.temperature()调用将变成“invokevirtual Wine/temperature ()F”(class文件实际包含的是该文本表示形式的二进制代码,这种文本化的指令描述方法称为Oolong方法),它表示的是一个方法调用――一个普通的(虚拟)方法调用,而不是一个静态调用。它调用的方法是Wine对象的temperature,右边的“()F”参数称为签名(signature),“()F”这个签名中的空括号表示方法不需要输入参数,F表示返回值是一个浮点数。
JVM执行到该语句时,它调用的不一定是Wine定义的temperature方法。实际上,在本例中,JVM不可能调用Wine定义的temperature方法,因为该temperature方法是一个虚拟方法。JVM首先检查该对象所属的类,寻找一个符合invokevirtual语句指定的名称、签名特征的方法,如果找不到,则检查该类的超类,然后是超类的超类,直至找到一个合适的方法实现为止。
在本例中,如果实际创建的对象是一个Bordeaux,则JVM调用Bordeaux类定义的temperature()F,该temperature()F方法将返回64。如果对象是一个Riesling,JVM在Riesling类中找不到适当的方法,所以继续查找WhiteWine类,在WhiteWine类中找到了一个合适的temperature()F方法,该方法的返回值是47。
因此,查找可用方法的过程就是沿着类的继承树通过字符串匹配寻找合适方法的过程。了解这一原理有助于理解哪些修改不至于影响二进制兼容性。
首先,重新排列类里面的方法显然不会影响到二进制兼容性――这在C++程序中一般是不允许的,因为C++程序利用数值性偏移量而不是名称来确定要调用的方法。延迟绑定的关键优势正是在此,如果Java也使用方法在类里面的偏移量来确定要调用的方法,必然极大地限制二进制兼容机制的发挥,即使极小的改动也可能导致大量的代码需要重新编译。
?说明:也许有人会认为C++的处理方式要比Java的快,理由是根据数值性偏移量寻找方法肯定要比字符串匹配快。这种说法有一定道理,但只说明了类刚刚装入时的情况,此后Java的JIT编译器处理的也是数值性偏移量,而不再靠字符串匹配的办法寻找方法,因为类装入内存之后不可能再改变,所以这时的JIT编译器根本无须顾虑到二进制兼容问题。因此,至少在方法调用这一点上,Java没有理由一定比C++慢。
其次,还有很重要的一点是:不仅仅编译时需要检查类的继承关系,而且运行时JVM还要检查类的继承关系。