类型转换与 instanceof | JavaSE

类型转换与 instanceof

一、学习目标

学完本章,你应该能够:

  • 理解对象引用类型转换解决什么问题。
  • 区分向上转型(Upcasting)与向下转型(Downcasting)。
  • 理解为什么子类对象可以自动赋值给父类类型变量。
  • 理解为什么父类类型变量不能直接调用子类独有方法。
  • 能够使用强制类型转换重新获得子类类型视角。
  • 理解强制类型转换不会改变对象本身的真实类型。
  • 能够解释 ClassCastException 为什么出现。
  • 能够使用 instanceof 在强制类型转换前检查对象真实类型。
  • 掌握 JDK 21 中 instanceof 模式匹配语法。
  • 能够安全处理 DogCat 等不同实际子类型。
  • 能够将“多态 → 类型判断 → 子类独有行为”完整串联起来。

二、核心知识

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 知识问答

  1. 什么是对象引用类型转换?
  2. 什么是向上转型?
  3. 什么是向下转型?
  4. Animal a = new Dog() 属于什么转换?
  5. 为什么向上转型通常不需要显式强转?
  6. 为什么多态变量不能直接调用子类独有方法?
  7. 向下转型解决什么问题?
  8. 强制类型转换会不会改变对象真实类型?
  9. 什么情况下会出现 ClassCastException
  10. 强转编译通过是否代表运行时一定成功?
  11. instanceof 有什么作用?
  12. instanceof 检查的是变量声明类型还是对象运行时兼容性?
  13. null instanceof Dog 的结果是什么?
  14. 为什么强转前通常应该进行类型判断?
  15. JDK 21 中 instanceof Dog dog 有什么作用?
  16. 模式匹配和传统 instanceof + cast 相比有什么优势?
  17. 为什么不能为了调用不同子类行为而到处写 instanceof
  18. 什么情况下应该优先使用方法重写和多态?

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();
        }
    }
}

回答:

  1. animal 的编译时类型是什么?
  2. 实际对象是什么?
  3. 第一个 instanceof 是 true 还是 false?
  4. 第二个呢?
  5. 哪个独有方法最终被调用?
  6. 是否发生了对象重新创建?

继续阅读:

Animal animal = new Cat();

Dog dog = (Dog) animal;

dog.lookHome();

回答:

  1. 编译阶段为什么可能允许 (Dog) animal
  2. 运行时真正对象是什么?
  3. 为什么运行失败?
  4. 异常名称是什么?

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

构造一个可以编译、但运行时发生错误类型转换的例子。

要求:

  1. 先预测异常。
  2. 再运行。
  3. 最后使用 instanceof 修复。

7.4 Debug

下面代码的问题是什么?

Animal animal = new Dog();

animal.lookHome();

回答:

  1. 对象真实类型是什么?
  2. 为什么仍不能直接调用?
  3. 如何使用 JDK 21 的模式匹配安全解决?

下面代码:

Animal animal = new Cat();

if (animal instanceof Dog) {
    Cat cat = (Cat) animal;
}

虽然这个例子某些路径未必马上导致异常,但设计明显混乱。

要求:

  1. 判断条件与转换目标是否一致。
  2. 改写成 pattern matching。
  3. 说明为什么新版写法更不容易出现这种错误。

7.5 综合训练

设计一个宠物服务系统:

Pet
├── Dog
├── Cat
└── Bird

共同方法:

eat()

独有方法:

Dog → guard()
Cat → catchMouse()
Bird → fly()

设计:

public static void service(Pet pet)

要求:

  1. eat() 必须通过多态直接调用。
  2. 独有行为使用 JDK 21 instanceof 模式匹配。
  3. 禁止对公共 eat() 使用三层 if instanceof
  4. 解释为什么公共行为和独有行为采取了不同处理方式。

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,那么对象类型转换还没有真正掌握。