向上转型,向下转型,还在头疼?

在学习Java编程中,最头疼事情之一就是数据类型转换。有时候它在不经意之间就完成了(自动类型转换),有时候却又要求程序员必须手动指定(强制类型转换)。基本数据类型,转换规则还可以通过类型本身空间大小和精度分析明白,而且最多就是丢失精度但运行起来至少是不会报错。可是面对引用数据类型,这个“坑”就大了:有自动转的,有强制转的,居然还有强制都转不了的;自动转了的却把对象身上的方法丢了看不见;强制转的编译过了运行却可能报异常。

一口老血,有木有?更要命的是,这个转型还相当的重要,Java中的动态多态还非它不可了。你说要命不要命?没关系,今天就让我们花点时间聊聊它,搞定它!

对象不是你想转,想转就能转

先人说:知己知彼,百战不殆。我们先来认知一下引用数据类型的一些基本概念,以下所有分析都基于两个方面进行:Java语法 和 面向对象场景。
在普通情况下,我们会书写这样的代码:

1
2
3
4
Dog snoopy = new Dog();
snoopy.play();//调用Dog的玩耍行为
snoopy.sitDown();//调用Dog的坐下行为
snoopy.shakeHands();//调用Dog的握手行为

我们看到在这个语法当中,赋值符号左右两边的数据类型是相同。赋值号左边是Dog类的引用snoopy变量,指向了右边new出来的Dog类对象。
这个语法在对应的日常场景中是非常形象的。我们用snoopy称呼一个狗对象,并且确定一定以及肯定snoopy就是一个狗东西。然后我们就可以大喊:snoopy来玩游戏啦,snoopy坐下,snoopy握手。你看,多自然。
但是,如果我们写出下面的代码:
1
2
Dog snoopy = new Cat();
snoopy.sitDown();

赋值符号左右两边的数据类型是不同。赋值号左边是Dog类的引用snoopy变量,指向了右边new出来的Cat类对象。编译后,你就发现这句代码根本通不过。为什么?
因为这个语法是荒谬的。snoopy明明是狗的代名词,结果你却让它指向了一只猫?还想让它执行狗才有的指令?铲屎的,你是不是不想活了?
这个时候有同学想到了,那就强转。
1
2
Dog snoopy = (Dog)new Cat();
snoopy.sitDown();

这就更令人发指了吧,你要怎么做才能强行让一只猫咪对象变成一只汪对象?上帝也疯狂了……你觉得Java会让允许你干这种完全匪夷所思不合常理的事情吗?所以,这句代码的下场也只有一个,那就是编译不通过。
因此,我们得到了第一个结论:在Java当中不是任意引用数据类型之间都能进行转换!那么,哪些引用类型之间可以呢?

向上转型—自动转换,没问题

我们分别定义两个类:一个叫做Pet(宠物);一个叫做Dog,并且让Dog继承于Pet。

1
2
3
4
5
public class Pet{
public void play(){
System.out.println("玩游戏");
}
}

1
2
3
4
5
public class Dog extends Pet{
public void sitDown(){
System.out.println("坐下");
}
}

那么,我们在需要调用处写下这个代码会发生什么呢?
1
Pet myBaby = new Dog();

你会发现虽然在赋值符号两边的数据类型不一致,但是这句代码无论是编译还是运行都完全没有问题,也就是说Java中父类的引用指向子类对象是自动成功的。 这是为啥呢?其实无论是从语法上还是从场景分析上,我们会发现这是非常自然的,本身就应该自动成功。继承关系本就是一种is a关系,即所谓的“是一个“,所以Dog对象是一个Pet类型的呀(狗就是一种宠物嘛~~),这完全没有问题。
在继承关系上,我们设计时通常在继承树上把父类画在上,子类在下,由于这种转型是沿着继承树往上走,所以我们把它称为–向上转型
但是,
1
2
myBaby.play();//编译通过
myBaby.sitDown();//编译失败

这又是为啥呢?
因为myBaby是一个Pet类型的引用,所以是站在宠物的角度去看待汪对象。虽然对象还是狗狗,但是只能看到来自于父类宠物定义的play方法了。所以父类引用指向子类对象,只能调用到来自父类的属性/行为
那如何调用到sitDown方法呢?答案很简单:换成狗的角度去看待这个汪星人。

向下转型—强制转换,有风险

1
2
3
4
Pet myBaby = new Dog();
myBaby.play();
Dog snoopy = (Dog)myBaby;
snoopy.sitDown();

这段代码无论编译还是运行都不会有任何问题。
我们先使用Pet类型的myBaby指向了狗对象,然后再换成Dog类型的snoopy去指向同一个狗对象。前者由于是父类型所以只能看到定义在父类的方法,后者是子类型,当然就可以看到狗对象身上的特有行为了。这种转型是从父类引用转为子类引用,从继承树的角度说就是向下转型
那为什么在把myBaby赋给snoopy的时候要使用强转语法呢?我们假设下面这种情况:
如果Pet类还有一个子类叫做Cat类。

1
2
3
4
5
public class Cat extends Pet{
public void climbTree(){
System.out.println("爬树");
}
}

然后书写代码:
1
2
3
4
5
public class Master{
public void playWithPet(Pet myBaby){
//操作代码
}
}

那么,你告诉我传进来的myBaby到底是Dog对象呢?还是Cat对象?或者是Pet对象?由于父类引用可以指向子类对象,所以上面几种情况皆有可能了。所以我们如果想转型Dog类,就必须强制告诉Java,myBaby确实是一个Dog类型的对象。因此我们需要在注释部分书写:
1
Dog snoopy = (Dog)myBaby;

不过,就算你这么书写也只能保证编译通过,这个代码运行起来还是有可能失败,这就是所谓的强转风险性吧。如果你赋给myBaby的是🐶对象,当然没有问题;但假如赋的是🐱对象呢?这是不是相当于我们又从狗的角度去看待一个猫对象了?如何让一只狗变成一只猫,这又是一个荒谬的事情了。所以,如果向下转型想要编译和运行都成功,必须使用强制转型语法,还必须要求运行起来父类引用确实指向该子类的对象。
所以,为了降低这种风险性,我们可以使用Java中的instance运算符,在强转前进行一次判断。所以最终代码是:
1
2
3
4
5
6
7
8
9
public class Master{
public void playWithPet(Pet myBaby){
myBaby.play();
if(myBaby instanceof Dog){
Dog snoopy = (Dog)myBaby;
snoopy.sitDown();
}
}
}

结论

  1. 在引用数据类型中,只有有继承关系的类型才能进行类型转换;
  2. 类型转换只是转换看待对象的引用的类型,对象本身没有也不可能参与转换;
  3. 父类引用可以自动指向子类对象,但只能访问和调用到来自于父类的属性和行为;
  4. . 子类的引用不能指向父类或其它子类对象,就算强转也会导致运行失败并抛出ClassCastException;
  5. . 把父类引用赋给子类引用,语法上必须使用强制类型转换,要想运行也成功还必须保证父类引用指向的对象一定是该子类对象(最好使用instance判断后,再强转)。