七、多态
多态中重要概念:函数调用(method–call)绑定方式,所谓“绑定”,就是建立method call(函数调用)和method body(函数本体)的关联。
后期绑定(动态绑定):绑定动作将在执行期才根据对象型别而进行。Java中的所有函数,除了被声明为final者,皆使用后期绑定。
代码演示为:
class Pet{
Pet(){
System.out.println("pet created");
}
@Override
public String toString() {
return "pet string";
}
//传入Pet,打印信息
public static void print(Pet pet){
System.out.println(pet);
}
}
class Dog extends Pet{
Dog(){
System.out.println("dog created");
}
@Override
public String toString() {
return "dog string";
}
}
class Cat extends Pet{
Cat(){
System.out.println("cat created");
}
@Override
public String toString() {
return "cat string";
}
}
class Cat1 extends Pet{
Cat1(){
System.out.println("cat1 created");
}
//cat1没有重写tostring方法
}
public class Main{
public static void main(String[] args){
Pet pet1 = new Pet();
Pet pet2 = new Dog();
Pet pet3 = new Cat();
Pet pet4 = new Cat1();
Pet.print(pet1);
Pet.print(pet2);
Pet.print(pet3);
Pet.print(pet4);
}
}
输出结果为:
pet created
pet created
dog created
pet created
cat created
pet created
cat1 created
pet string
dog string
cat string
pet string
说明两个问题:
1.当new出derived class对象时,调用自身构造函数之前会调用父类构造函数。
2.在 Pet中的print方法传入参数为Pet,但运行期java能够自动识别更准确的型别信息(是Dog,Cat还是Cat1),从而调用各自的tostring()方法。当没有重写tostring()方法时,如Cat1,调用其父类方法。为什么在运行期我们能够分别出他们的型别呢?这与“后期绑定”密不可分!
抽象类和抽象方法:
为抽象类产生任何对象都是不安全的,编译器会发出错误信息。
如果继承自一个抽象类,并且希望为新型别产生对象,那么得为base class中的所有抽象方法提供相应的定义。如果没有这么做,derived class也会成为一个抽象类,并且编译器强迫你使用abstract来修饰derived class。
小技巧:我们也可以将不含任何抽象方法的class声明为abstract。这样做的目的是:在这个class不具备“拥有抽象方法”的情况下,我们不希望class被产生出任何实体。
构造函数和多态:虽然构造函数不具有多态性格(但你还是可以拥有某种“虚拟构造函数”,详见12章)
当有继承时,复杂对象的调用顺序:
class Pet{
static{
new Out("pet static before");
}
Pet(){
System.out.println("pet created");
}
Object cat = new Out("pet");
@Override
public String toString() {
return "pet string";
}
//传入Pet,打印信息
public static void print(Pet pet){
System.out.println(pet);
}
}
class Cat extends Pet{
Cat(){
System.out.println("cat created");
}
static{
new Out("cat static after");
}
Object cat = new Out("cat");
@Override
public String toString() {
return "cat string";
}
}
class Out{
Out(String s){
System.out.println(s);
}
}
class SonOfCat extends Cat{
static{
new Out("static before");
}
Object sonofcat = new Out("sonofcat");
SonOfCat(){
System.out.println("sonofcat created");
}
static{
new Out("static after");
}
}
public class Main{
public static void main(String[] args){
Pet pet = new SonOfCat();
}
}
输出结果:
pet static before
cat static after
sonofcat static before
sonofcat static after
pet
pet created
cat
cat created
sonofcat
sonofcat created
这样我们就知道了,当有继承时,复杂对象的调用顺序:
1.首先向上追溯到最上层class,完成它的static初始化(函数和成员变量),逐步向下,完成至自身static块的初始化。
2.完成最上层的non–static初始化,并调用其构造函数,完成后转向下一层class。
3.如此向下,直到自身的non-static初始化和构造函数调用完毕。
关于抽象class调用顺序:
abstract class Glyph{
abstract void draw();
Glyph(){
System.out.println("glyph before draw");
draw();
System.out.println("glyph after draw");
}
}
class RoundGlyph extends Glyph{
int radius = 1;
RoundGlyph(int r) {
radius = r;
System.out.println("roundglyph.roundglyph,radius="+radius);
}
void draw(){
System.out.println("roundglyph.draw,radius="+radius);
}
}
public class Main{
public static void main(String[] args){
new RoundGlyph(5);
}
}
你可能认为函数调用是这样的,在Glyph中,draw()是抽象的,所以其作用是让其他人进行重写,于是在RoundGlyph 中加以重写。但Glyph构造函数中调用了此函数,结果唤起了RoundGlyph .draw(),这可能是你的意图。但这是错误的。其真实输出为:
glyph before draw
roundglyph.draw,radius=0
glyph after draw
roundglyph.roundglyph,radius=5
实际的初始化过程是:
1.任何事情发生前,分配给此对象的存储空间会被初始化为二进制零值(为0或null或false)。
2.此例中,先调用base class的构造函数,重写后的draw()会被调用(在RoundGlyph 构造函数被调用之前),由于步骤1的缘故,draw()看到的radius为0。
3.以“成员声明顺序”来调用各成员的初始式。
4.调用derived class构造函数本体。
因此,在撰写构造函数时,一条原则就是:“尽可能简单地让对象进入正确的状态。如果可以的话,别调用任何函数”。构造函数中唯一可以安全调用的函数就是“base class中的final函数(对private函数来说一样成立,因为他们天生就是final)”。此类函数无法被重写,也就不会产生这一类意想不到的结果。