方法调用过程
假设在源码中有这样一行:
manager.setBonus(2300);
下面来看看javac编译器是如何处理的:
- 检查根据对象类型和函数名称,在该类成员方法及其父类中有调用权的成员方法中寻找到所有名字匹配的方法。在本例中,manager的类型,假定为CManager类,其父类为CEmployee。编译器会枚举出CManager类中名为setBonus的方法以及CEmployee中名为setBonus的public方法(注:父类的私有方法子类不可访问)
- 从中挑选出参数个数匹配、类型最为吻合的(优先考虑类型匹配,然后是经过强制转换后可以匹配的),如果找不到或者找到多个都报编译时错误。编译器检查调用该方法时传入的参数类型,这里为int,所以在第一步找到的方法中,优先选择参数为int类型的方法。如果没有合适的,将考虑参数类型转换兼容的方法,如果有多个方法及经过类型转换后匹配,则会报错。(子类在覆写(override)父类方法时,可将返回值设定为父类中相同方法返回值的子类型,且子类方法的访问权限不能低于父类方法)
- 找到最佳匹配方法后,根据方法的修饰关键字决定是动态绑定(运行时查对象方法表决定)还是静态绑定(直接生成调用函数的指令)
- 如果该方法由private、static或final关键字修饰,或者是构造方法,则生成静态绑定代码;
- 其余情况下,生成动态绑定代码,在运行时通过对象实例的引用找到其对应的方法表(类似C++的虚函数表)。
final关键字
final关键字可以用于修饰类、成员方法和成员变量,其主要用途:
- 修饰一个类,表明该类不可再被继承;
- 修饰成员方法,表明该方法不可被子类覆写(override)
- 修饰成员变量,表明该变量的值在类对象创建后不能被改动
final关键字修饰类时,该类不再允许被继承(即该类就为最终类)
public final class CChild extends CBase {
//...
}
final关键字修饰成员方法时,则该类的子类中无法再覆写(override)该方法。
public class CBase {
public final void setID(int id) {
//...
}
}
在此例中,CBase的任何子类都不能对setID方法进行覆写了。如果某个类中有一个名为func1的final方法,我们来进行如下分析:
其父类中若有func1方法且没设置final属性,虽然其子类的func1()方法被设置了final属性,但为了实现多态效果,依然会使用invokevirtual(按虚函数调用)的方式进行调用。
其子类不可能覆写该方法,所以可以在编译器静态绑定。虽然理论上如此,但Java8 SE中依然使用的是invokevirtual(按虚函数调用)的方式进行调用。
public class CNo1 { public final void func4() { System.out.println("CNo1::func4"); } } public class CDemo1 { public static void main(String[] args) { CNo1 n = new CNo1(); n.func4(); } } //javap看到的结果 Code: stack=2, locals=2, args_size=1 0: new #2 // class CNo1 3: dup 4: invokespecial #3 // Method CNo1."<init>":()V 7: astore_1 8: aload_1 9: invokevirtual #4 // Method CNo1.func4:()V 12: return
final关键字修饰成员变量时,表示该变量在对象构造后就不允许被改变了。
public class CTest {
private static final double PI = 3.141592653;
//...
}
final类中的所有方法将自动成为final方法,但其中的成员变量不会自动变成final类型变量。
类型转换
在类型转换方面,Java抛弃了C++定义的那套复杂的语法,返璞归真的使用了C的类型转换形式:
类型1 名称 = (类型1) 其他类型的对象;
一般只有两类转换是正常的:
- 数值类型的转换(如浮点型转换为整型)
- 有继承关系类间的转换
关于向上和向下转型
- 所谓向上转型,即将子类对象转换成父类对象。
- 这是实现多态的一种常见手法。
- 在Java中,此类形式无需使用显示类型转换。
- 所谓向下转型,即将父类对象转换成子类对象。
- 这中情况可能存在危险:如果被转换的对象真的是父类的,转换后调用子类特有的方法或访问子类特有的成员变量,则越界了。
- 在Java中,必须显示的使用类型转换的语法表明编写者了解该风险并坚持使用。
- 即便使用类型转换语法瞒过了编译器,在运行时执行该转换时也会被JVM发现并抛出ClassCastException异常
- 通常,向下转型仅用于以下情景:恢复子类对象所具有的身份。即一个子类对象可能由于实现多态等考量被向上转型为了父类的类型。而一旦再次使用子类特有的成员方法或成员变量时,才可以用向下转型的方法为其“平反昭雪”,恢复子类类型的身份。
- 通常而言,不要使用向下转型,即便是为了给子类“平反昭雪”。因为通常这意味着类的继承关系设置的不合理。一个设计良好的继承关系框架应该不需要任何的“平反昭雪”。
为了保证类型转换的安全,JVM也提供了类似C++的RTTI机制,判明转换是否能成功:
boolean bRet = 类对象 instanceof 类;
只有bRet返回true时,才能安全地进行转型。注意,如果类对象为null,则表达式返回false。
再强调一次,向下转型不是一个好的设计,应该避免使用!
与C++向下转型的对比
Manager* boss = dynamic_cast<Manager*>(staff);
if (boss)
{
//转型成功,进行后续操作...
}
C++是将转型与判断过程都集中在dynamic_cast过程中了,如果不成功,返回的boss为NULL,否则为对象的指针。
if (staff instanceof Manager) {
Manager boss = (Manager) staff;
//执行后续操作
}
Java中转换是否能成功的判定由instanceof完成,而类型转换则会直接进行转换,如果转换失败,要么是编译时报错,要么运行时抛异常。所以在转换前,必须先用instanceof投石问路。
abstract关键字与抽象类
在C++里有纯虚函数的概念,而包含纯虚函数的类无法实例化。在Java中,也有类似的语法:用abstract关键字修饰成员方法,则该方法成为一个抽象方法,抽象方法无需有实现,只要有声明即可。注意:包含抽象方法的类必须也要加上abstract关键字成为抽象类。
以著名的Shape为例,世界上有圆形、矩形等等,他们都是图形,但谁也无法定义“图形”,因为这是一个抽象的概念,因此,Shape类无需实例化。但所有图形都有一个draw的行为,即在画布上画出自己,因此考虑将其作为Shape类的成员方法。但由于Shape类是个抽象类,因此也没必要实现draw的动作,故该方法在Shape类中被定义为了抽象方法。
public abstract class Shape {
public abstract boolean draw(Cloth cloth);
}
需要注意的是:
- 抽象类不能实例化对象,但依然可以用该类型引用子类对象从而实现多态。如:
ArrayList<Shape> shapelists = new ArrayList<Shape>();
shapelists.add(new Circle(3, 2, 1));
shapelists.add(new Rectangle(6, 6, 4, 2));
//...
for (Shape currentShape : shapelists) {
currentShape.draw(cloth);
}
- 在抽象类中依然可以定义成员变量和非抽象的成员函数。
- 在继承抽象类后,可选择实现其中全部的抽象方法,此后,该子类可以被实例化。
- 在继承抽象类后,也可以选择不实现其中的某些抽象方法,这样,该子类也需被定义为抽象类,并且不能被实例化。
- 即便一个类中没有抽象方法,也可以被定义为抽象类,此时,该类无法被实例化。
再议访问标识
在C++中,访问标识要和继承类型结合起来,所以一共有9种情况。记得某一年期末考试题还考这玩意儿…
而到了Java中,由于继承类型全部是公有继承了,再配合三个关键字,一共实现了4种类型。三个关键字怎么实现了4种访问控制级别呢?当然是不加这三个关键字默认有一种喽。
- private:仅类内可见,继承后的子类都不可见。
- public:到处都可见
- protected:在类内、所有子类(只有公有继承)和包内都可见
- 默认(神马都不加):包内可见