类型转换与 instanceof | JavaSE
类型转换与 instanceof
一、学习目标
学完本章,你应该能够:
- 理解对象引用类型转换解决什么问题。
- 区分向上转型(Upcasting)与向下转型(Downcasting)。
- 理解为什么子类对象可以自动赋值给父类类型变量。
- 理解为什么父类类型变量不能直接调用子类独有方法。
- 能够使用强制类型转换重新获得子类类型视角。
- 理解强制类型转换不会改变对象本身的真实类型。
- 能够解释
ClassCastException为什么出现。 - 能够使用
instanceof在强制类型转换前检查对象真实类型。 - 掌握 JDK 21 中
instanceof模式匹配语法。 - 能够安全处理
Dog、Cat等不同实际子类型。 - 能够将“多态 → 类型判断 → 子类独有行为”完整串联起来。
二、核心知识
2.1 从多态遗留的问题开始
上一章学习了:
Animal animal = new Dog();
这里:
animal 的编译时类型:Animal
实际对象类型:Dog
假设:
public class Animal {
public void eat() {
System.out.println("动物吃东西");
}
}
public class Dog extends Animal {
@Override
public void eat() {
System.out.println("狗吃骨头");
}
public void lookHome() {
System.out.println("狗正在看家");
}
}
多态:
Animal animal = new Dog();
可以:
animal.eat();
运行时执行:
Dog.eat()
但是不能直接:
animal.lookHome();
原因已经学习过:
编译器首先根据变量的编译时类型判断可以直接访问哪些成员。
变量:
animal
的类型是:
Animal
而 Animal 中没有:
lookHome()
因此代码在编译阶段就无法通过。
可是实际对象明明是:
Dog
有没有办法重新获得:
Dog 类型视角
然后调用:
lookHome()
这就是对象类型转换要解决的问题。
2.2 对象引用类型转换有两种典型方向
在继承关系中:
Animal
↑
Dog
可以进行两个方向的引用类型转换:
Dog → Animal
称为:
向上转型(Upcasting)
以及:
Animal → Dog
称为:
向下转型(Downcasting)
可以先建立一个图:
Animal
↑
│ 向上转型
│
Dog
│
│ 向下转型
↓
Dog
注意:
这里转换的主要是“引用的类型视角”,不是把堆中的对象重新制造成另一个对象。
2.3 什么是向上转型
例如:
Dog dog = new Dog();
Animal animal = dog;
这里:
Dog → Animal
属于向上转型。
也可以直接:
Animal animal = new Dog();
这实际上就是最常见的多态写法。
因为:
Dog is an Animal
Dog 本来就是 Animal 的一种。
所以这种转换通常可以自动完成,不需要显式写:
(Animal)
2.4 向上转型为什么安全
假设:
Animal
├── eat()
└── run()
Dog extends Animal
├── eat()
├── run()
└── lookHome()
一个 Dog 对象一定具备:
Animal 所承诺的基本类型能力
所以:
Animal animal = new Dog();
不存在:
Dog 根本不是 Animal
这样的问题。
可以简单理解成:
Dog 的类型信息更具体
Animal 的类型信息更一般
从具体类型转换到它的父类型,是一种正常的类型兼容关系。
2.5 向上转型以后发生了什么
代码:
Dog dog = new Dog();
Animal animal = dog;
不要误以为出现了两个对象。
真实情况更接近:
Dog dog ──────────────┐
│
▼
┌─────────┐
│ Dog对象 │
└─────────┘
▲
│
Animal animal ───────┘
两个引用变量:
dog
animal
可以指向同一个 Dog 对象。
区别只是:
dog
按照 Dog 类型使用对象
animal
按照 Animal 类型使用对象
2.6 为什么需要向下转型
仍然是:
Animal animal = new Dog();
虽然实际对象:
Dog
但变量当前是:
Animal
所以:
animal.lookHome();
不能编译。
如果我们已经确认:
animal 当前引用的确实是 Dog 对象。
就可以:
Dog dog = (Dog) animal;
然后:
dog.lookHome();
完整:
Animal animal = new Dog();
Dog dog = (Dog) animal;
dog.lookHome();
这就是:
向下转型。
2.7 强制类型转换的语法
基本格式:
目标类型 变量名 = (目标类型) 原引用;
例如:
Dog dog = (Dog) animal;
其中:
目标类型:Dog
原引用类型:Animal
转换后:
dog.lookHome();
就可以使用 Dog 独有方法。
2.8 向下转型不会改变真实对象
这一点必须理解。
原本:
Animal animal = new Dog();
强转:
Dog dog = (Dog) animal;
不是:
Animal 对象
↓
转换成 Dog 对象
因为真正创建的对象从一开始就是:
Dog
我们只是:
Animal 类型视角
↓
重新恢复为 Dog 类型视角
所以强制类型转换主要影响:
编译器如何理解这个引用。
而不是把堆中已有对象重新加工成另一种对象。
2.9 强转为什么存在风险
考虑:
Animal animal = new Cat();
然后:
Dog dog = (Dog) animal;
从源码表面看:
Animal
↓
Dog
似乎存在父子类型关系。
但运行时真正对象是:
Cat
你实际上在告诉 JVM:
“相信我,这个 Animal 引用的对象其实是一只 Dog。”
然而现实却是:
它是一只 Cat
于是运行时会抛出:
ClassCastException
即:
类型转换异常。
2.10 ClassCastException 示例
class Animal {
}
class Dog extends Animal {
}
class Cat extends Animal {
}
测试:
public class Test {
public static void main(String[] args) {
Animal animal = new Cat();
Dog dog = (Dog) animal;
}
}
编译器可能允许这种具有转换可能性的引用转换表达式存在。
但是运行时发现:
实际对象:Cat
目标类型:Dog
Cat 并不是 Dog。
于是转换失败。
这说明:
强制类型转换的语法写对了,不代表运行时对象一定真的符合目标类型。
2.11 不能理解成“有继承关系就一定随便强转成功”
一定要区分:
编译时能否存在某种合法转换
和:
运行时实际对象能否通过转换检查
例如:
Animal
├── Dog
└── Cat
变量:
Animal animal = new Cat();
表达式:
(Dog) animal
从静态类型角度存在向下转换的可能,因此可以进入运行时检查。
但真实对象:
Cat
不能被当成:
Dog
所以运行失败。
更准确的理解是:
Java 编译器会先判断两个引用类型之间是否存在合法的转换可能;如果转换需要运行时验证,JVM 会进一步检查实际对象是否真的兼容目标类型。
2.12 如何避免 ClassCastException
既然问题来自:
强转之前不知道对象到底是什么类型。
那么最自然的思路就是:
先判断,再转换。
Java 提供:
instanceof
例如:
if (animal instanceof Dog) {
Dog dog = (Dog) animal;
dog.lookHome();
}
这样只有当:
animal 当前引用的对象确实兼容 Dog
时,才进行:
(Dog) animal
2.13 instanceof 的基本语法
传统形式:
对象引用 instanceof 类型
例如:
animal instanceof Dog
返回:
boolean
也就是:
true
或
false
例如:
Animal animal = new Dog();
System.out.println(
animal instanceof Dog
);
结果:
true
如果:
Animal animal = new Cat();
那么:
animal instanceof Dog
结果:
false
2.14 instanceof 判断的真正含义
不要把:
animal instanceof Dog
机械理解成:
“animal 变量是不是 Dog 类型变量?”
因为:
animal
变量声明明明可能是:
Animal
instanceof 检查的是:
当前表达式引用的对象,在运行时是否与指定类型兼容。
例如:
Animal animal = new Dog();
虽然左侧变量是:
Animal
但:
animal instanceof Dog
仍然为:
true
因为实际对象确实是 Dog。
2.15 null instanceof 任何类型都是 false
例如:
Animal animal = null;
判断:
animal instanceof Dog
结果是:
false
不会因为:
animal == null
就抛出 NullPointerException。
因此:
instanceof
本身也可以自然排除 null。
2.16 传统的 instanceof + 强转写法
完整案例:
public static void showAnimal(
Animal animal
) {
animal.eat();
if (animal instanceof Dog) {
Dog dog = (Dog) animal;
dog.lookHome();
} else if (animal instanceof Cat) {
Cat cat = (Cat) animal;
cat.catchMouse();
}
}
这里:
公共行为
↓
animal.eat()
子类特有行为
↓
先 instanceof
再向下转型
再调用
2.17 JDK 21:instanceof 模式匹配
JDK 21 已经支持更简洁的写法:
if (animal instanceof Dog dog) {
dog.lookHome();
}
这段代码同时完成了:
1. 判断 animal 是否是 Dog
2. 如果判断成功
3. 创建 Dog 类型模式变量 dog
因此不用再写:
if (animal instanceof Dog) {
Dog dog = (Dog) animal;
dog.lookHome();
}
可以直接:
if (animal instanceof Dog dog) {
dog.lookHome();
}
2.18 模式变量只在类型成立的范围内可用
例如:
if (animal instanceof Dog dog) {
dog.lookHome();
}
这里:
dog
只会在编译器能够确定:
animal 已经成功匹配 Dog
的代码区域内有效。
不能:
if (animal instanceof Dog dog) {
dog.lookHome();
}
// dog.lookHome();
因为出了对应作用域以后:
dog
可能根本没有被成功创建。
2.19 模式匹配为什么更安全
传统代码:
if (animal instanceof Dog) {
Dog dog = (Dog) animal;
dog.lookHome();
}
这里:
判断类型
和
强转目标类型
由开发者写了两次。
理论上可能误写:
if (animal instanceof Dog) {
Cat cat = (Cat) animal;
}
而模式匹配:
if (animal instanceof Dog dog) {
dog.lookHome();
}
把:
检查类型
+
得到目标类型变量
绑定在一起。
代码:
- 更简洁;
- 更不容易写错;
- 意图更加清楚。
2.20 instanceof 不应该成为“到处判断类型”的理由
虽然:
instanceof
很好用,但不能看到多态就写:
if (animal instanceof Dog) {
...
} else if (animal instanceof Cat) {
...
} else if (animal instanceof Rabbit) {
...
} else if (...) {
}
如果处理的是所有动物共同拥有的行为:
eat()
run()
sleep()
更应该利用:
方法重写 + 多态
例如:
animal.eat();
让实际对象自己决定行为。
只有确实需要:
某个具体子类的独有功能
时,才有必要进行类型判断。
三、使用方法
3.1 Dog 与 Cat 的完整案例
父类:
public class Animal {
private String name;
public Animal(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void eat() {
System.out.println(
name + "正在吃东西"
);
}
}
Dog:
public class Dog extends Animal {
public Dog(String name) {
super(name);
}
@Override
public void eat() {
System.out.println(
getName() + "正在吃骨头"
);
}
public void lookHome() {
System.out.println(
getName() + "正在看家"
);
}
}
Cat:
public class Cat extends Animal {
public Cat(String name) {
super(name);
}
@Override
public void eat() {
System.out.println(
getName() + "正在吃鱼"
);
}
public void catchMouse() {
System.out.println(
getName() + "正在抓老鼠"
);
}
}
处理方法:
public static void handle(
Animal animal
) {
animal.eat();
if (animal instanceof Dog dog) {
dog.lookHome();
} else if (animal instanceof Cat cat) {
cat.catchMouse();
}
}
调用:
handle(
new Dog("旺财")
);
handle(
new Cat("咪咪")
);
3.2 多态和向下转型各自负责什么
这个案例实际上把前两章连起来了:
Animal animal = new Dog();
│
└── 多态
用父类型统一接收对象
然后:
animal.eat()
│
└── 多态方法调用
运行 Dog.eat()
如果需要 Dog 独有功能:
animal instanceof Dog dog
│
└── 判断真实类型并获得 Dog 类型视角
最后:
dog.lookHome();
所以二者不是竞争关系,而是:
多态负责统一
类型转换负责在必要时恢复具体能力
3.3 方法参数中的安全处理
例如:
public static void work(
Animal animal
) {
animal.eat();
if (animal instanceof Dog dog) {
dog.lookHome();
}
if (animal instanceof Cat cat) {
cat.catchMouse();
}
}
调用方:
work(new Dog("旺财"));
work(new Cat("汤姆"));
方法形参继续保持:
Animal
因此保留多态带来的扩展能力。
只有真正需要子类独有行为时再缩小类型。
四、原理与进阶
4.1 向上转型本质上是扩大抽象视角
例如:
Dog dog = new Dog();
Animal animal = dog;
并不是对象能力消失。
而是:
Dog 视角
拥有更加具体的 API
↓
Animal 视角
只直接暴露 Animal 约定的 API
因此多态能够让调用者:
少依赖具体类型。
4.2 向下转型意味着“我知道得更多”
当你写:
Dog dog = (Dog) animal;
实际上是在告诉 Java:
虽然目前静态类型只是 Animal,但我认为运行时对象实际上兼容 Dog。
这是一种比当前编译时类型更强的假设。
所以 Java 必须检查。
如果假设正确:
继续执行
如果假设错误:
ClassCastException
因此:
向下转型比向上转型风险更高。
4.3 ClassCastException 是运行时异常
现在只需要知道:
ClassCastException
发生在:
运行时类型检查失败。
关于:
RuntimeException
Checked Exception
try-catch
throw
throws
等完整异常体系,冻结课程会在 GROUP 04 系统学习。
本章不提前展开异常处理机制。
4.4 编译器并不会允许任何毫无关系的强转
原始入门课程中常说:
“存在继承/实现关系就可以强制类型转换,编译时一般不报错。”
这适合帮助理解父子类型转换,但不能进一步误解成:
任意两个引用类型都可以随便写强转,反正交给运行时。
Java 在编译阶段本身也会检查:
转换是否具有类型兼容的可能性
某些能够静态证明绝不可能成立的转换,会直接成为编译错误。
因此真正应该记住的是:
编译阶段
先判断这种 cast 是否允许存在
↓
如果需要
运行阶段再检查实际对象
↓
不兼容则 ClassCastException
4.5 instanceof 和多态设计的边界
假设:
public static void makeSound(
Animal animal
) {
if (animal instanceof Dog) {
System.out.println("汪汪");
} else if (animal instanceof Cat) {
System.out.println("喵喵");
}
}
这段代码虽然可以工作,但如果:
发出声音
本来就是所有 Animal 的共同能力,更好的面向对象设计往往是:
animal.sound();
由:
Dog
Cat
自己重写。
否则每新增一个动物:
Rabbit
Bird
Tiger
都需要修改 makeSound()。
所以:
能通过父类型共同方法表达的行为,优先使用方法重写和多态。
instanceof 更适合:
真正需要使用具体子类型独有能力的场景。
五、实践应用
5.1 动物管理系统
统一:
public static void service(
Animal animal
)
公共操作:
animal.eat();
然后针对:
Dog
调用:
lookHome()
针对:
Cat
调用:
catchMouse()
这正是:
多态入口
+
必要时类型判断
+
具体能力调用
的完整应用。
5.2 Object 参数以后也会大量遇到类型判断
Java 中:
Object
是根类型。
因此以后可能遇到:
public void process(Object obj)
此时:
obj
可能引用各种不同类型对象。
如果确实需要根据运行时类型执行特定逻辑,就可能使用:
instanceof
所以它不仅服务于 Animal 示例,而是 Java 类型系统中的通用操作符。
六、常见问题
6.1 什么是向上转型?
子类引用转换成父类型引用,例如:
Animal animal = new Dog();
通常自动完成。
6.2 什么是向下转型?
把父类型引用转换为更加具体的子类型视角:
Dog dog = (Dog) animal;
6.3 向下转型会把 Animal 对象变成 Dog 吗?
不会。
实际对象本身不会因为 cast 被重新创建或改变真实类型。
6.4 为什么需要向下转型?
因为多态状态下:
Animal animal = new Dog();
变量不能直接访问:
Dog 独有 API
向下转型可以重新获得 Dog 类型视角。
6.5 为什么会发生 ClassCastException?
强制转换所假设的目标类型与对象真实运行时类型不兼容。
6.6 强转只要编译通过就一定安全吗?
不一定。
某些引用类型转换还需要运行时类型检查。
6.7 instanceof 返回什么?
boolean
也就是:
true / false
6.8 null instanceof Dog 会报错吗?
不会。
结果是:
false
6.9 JDK 21 推荐怎样写 instanceof?
可以使用模式匹配:
if (animal instanceof Dog dog) {
dog.lookHome();
}
减少重复强转。
6.10 模式变量 dog 在 if 外面一定能用吗?
不能。
它只在编译器能够确认类型匹配成功的有效作用域中使用。
6.11 是否应该大量使用 instanceof?
不是。
如果一个行为属于所有父类型共同能力,应优先考虑:
重写 + 多态
不要把多态重新写成大串类型判断。
七、练习与验收
7.1 知识问答
- 什么是对象引用类型转换?
- 什么是向上转型?
- 什么是向下转型?
Animal a = new Dog()属于什么转换?- 为什么向上转型通常不需要显式强转?
- 为什么多态变量不能直接调用子类独有方法?
- 向下转型解决什么问题?
- 强制类型转换会不会改变对象真实类型?
- 什么情况下会出现
ClassCastException? - 强转编译通过是否代表运行时一定成功?
instanceof有什么作用?instanceof检查的是变量声明类型还是对象运行时兼容性?null instanceof Dog的结果是什么?- 为什么强转前通常应该进行类型判断?
- JDK 21 中
instanceof Dog dog有什么作用? - 模式匹配和传统
instanceof + cast相比有什么优势? - 为什么不能为了调用不同子类行为而到处写
instanceof? - 什么情况下应该优先使用方法重写和多态?
7.2 代码阅读
不运行:
class Animal {
}
class Dog extends Animal {
public void lookHome() {
System.out.println("lookHome");
}
}
class Cat extends Animal {
public void catchMouse() {
System.out.println("catchMouse");
}
}
public class Test {
public static void main(String[] args) {
Animal animal = new Dog();
if (animal instanceof Dog dog) {
dog.lookHome();
}
if (animal instanceof Cat cat) {
cat.catchMouse();
}
}
}
回答:
animal的编译时类型是什么?- 实际对象是什么?
- 第一个
instanceof是 true 还是 false? - 第二个呢?
- 哪个独有方法最终被调用?
- 是否发生了对象重新创建?
继续阅读:
Animal animal = new Cat();
Dog dog = (Dog) animal;
dog.lookHome();
回答:
- 编译阶段为什么可能允许
(Dog) animal? - 运行时真正对象是什么?
- 为什么运行失败?
- 异常名称是什么?
7.3 手写代码
任务一:安全调用独有功能
设计:
Animal
├── Dog → lookHome()
└── Cat → catchMouse()
要求:
public static void handle(
Animal animal
)
内部使用 JDK 21:
instanceof 类型 变量
安全调用不同子类独有功能。
任务二:传统写法与 JDK 21 写法
分别实现:
instanceof
+
强制类型转换
以及:
instanceof 模式匹配
完成同样的 Dog 判断。
比较代码量和出错可能性。
任务三:ClassCastException 实验
设计:
Animal
Dog
Cat
构造一个可以编译、但运行时发生错误类型转换的例子。
要求:
- 先预测异常。
- 再运行。
- 最后使用
instanceof修复。
7.4 Debug
下面代码的问题是什么?
Animal animal = new Dog();
animal.lookHome();
回答:
- 对象真实类型是什么?
- 为什么仍不能直接调用?
- 如何使用 JDK 21 的模式匹配安全解决?
下面代码:
Animal animal = new Cat();
if (animal instanceof Dog) {
Cat cat = (Cat) animal;
}
虽然这个例子某些路径未必马上导致异常,但设计明显混乱。
要求:
- 判断条件与转换目标是否一致。
- 改写成 pattern matching。
- 说明为什么新版写法更不容易出现这种错误。
7.5 综合训练
设计一个宠物服务系统:
Pet
├── Dog
├── Cat
└── Bird
共同方法:
eat()
独有方法:
Dog → guard()
Cat → catchMouse()
Bird → fly()
设计:
public static void service(Pet pet)
要求:
eat()必须通过多态直接调用。- 独有行为使用 JDK 21
instanceof模式匹配。 - 禁止对公共
eat()使用三层if instanceof。 - 解释为什么公共行为和独有行为采取了不同处理方式。
7.6 本章验收
关闭资料,确认自己能够:
- [ ] 解释向上转型。
- [ ] 解释向下转型。
- [ ] 手写
Animal a = new Dog()。 - [ ] 手写
Dog d = (Dog) a。 - [ ] 解释 cast 不会改变对象本身。
- [ ] 判断什么时候可能出现
ClassCastException。 - [ ] 使用传统
instanceof安全强转。 - [ ] 使用 JDK 21 pattern matching for instanceof。
- [ ] 判断
null instanceof Xxx的结果。 - [ ] 解释为什么不能滥用 instanceof。
- [ ] 将继承、重写、多态、类型转换四个知识点完整串起来。
如果你只是会写:
(Dog) animal
却不知道 JVM 为什么有时允许、有时抛出 ClassCastException,那么对象类型转换还没有真正掌握。