Menu

Java 多态:一个接口,多种形态

Java 多态如何让一个变量引用多种类型,为什么重写的方法在运行时才分派,以及如何安全地使用向上转型、向下转型和 instanceof。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

一个引用,多种类型

多态是继承和接口带来的回报。它的意思是:一个声明为父类(或接口)类型的变量,可以存放任何子类型的对象;当你调用它的方法时,Java 运行的是对象实际所属类中的版本,而不是变量声明类型所对应的版本。

你在继承那一页已经见过前提条件:子类重写父类的方法。多态让这种重写变得有意义:你可以针对通用类型编写代码,让每个具体的对象按自己的方式表现。

a 和 b 都声明为 Animal,却各自打印出自己的叫声。这个选择是在运行时根据真实对象做出的。

动态方法分派

背后的机制叫作动态方法分派:对于被重写的实例方法,JVM 会查看对象在运行时的类,来决定调用哪个实现。编译器只检查这个方法在声明类型上是否存在,真正的选择要等到程序运行时才进行。

正因如此,一个循环就能处理各种类型的混合,完全不必询问每个对象是什么类型:

这个循环只知道 Shape。以后再加一个 Triangle extends Shape,这段代码无需任何修改依然能正常工作,这正是多态的意义所在。代码依赖的是抽象,而不是一份具体的类型清单。

向上转型与向下转型

把 Dog 存进 Animal 变量叫作向上转型,也就是沿着继承层次向上,变成一个更通用的类型。它总是安全的,Java 会隐式完成,因为每个 Dog 都是一个 Animal。

反方向就是向下转型:拿一个父类引用,把它当作某个具体的子类型来对待。这是引用类型之间的一种类型转换,只有当对象确实是那个子类型时才有效,所以你必须显式地写出转换,而且一旦判断错了,就有抛出 ClassCastException 的风险:

最后那次转换能顺利通过编译,因为编译器无法证明它是错的,但运行时就会出错,因为 Cat 不是 Dog。永远不要凭感觉向下转型。

用 instanceof 保护向下转型

向下转型之前,先用 instanceof 检查真实类型。现代 Java 允许你在同一个表达式中绑定结果(instanceof 的模式匹配),这样就省去了单独的转换:

对于 null,instanceof 返回 false,所以这个检查还能防止 NullPointerException。不过,如果你发现自己在写很长的 instanceof 链,这往往说明这些行为应该放进类的内部,作为被重写的方法,让多态替你完成分支判断。

重写与重载

这两个词听起来很像,但毫无关系,把它们弄混是一个常见的困惑来源。

重写是子类用完全相同的签名替换父类的方法。它在运行时根据对象的类型解析,这就是我们一直在用的多态。

重载是一个类中有多个同名但参数列表不同的方法。它在编译时根据实参的类型解析,不涉及任何运行时分派:

编译器完全根据实参的静态类型选出匹配的 describe。这里不涉及父类和子类对象,所以这不是运行时多态,只是复用了一个方法名。

一个常见陷阱:字段没有多态

只有实例方法会在运行时分派。字段和静态方法按声明类型解析,很多人都在这里栽过跟头:

p.name() 运行的是 Child 的版本(多态),但 p.label 读取的是 Parent 的字段,因为字段是被隐藏,而不是被重写。解决方法很简单:把字段设为 private,只通过方法访问它们,这样多态调用总能胜出。

下一页:访问修饰符

只有当子类能看到并重写正确的成员,而其余代码又无法伸手进来破坏不变量时,多态才能干净地发挥作用。这种平衡由 public、protected、private 和包级私有访问来控制,也就是下一页要讲的访问修饰符。

常见问题

Java 中的多态是什么?

多态是指一个引用类型可以指向许多不同类的对象,而实际运行哪个方法,由对象在运行时的真实类型决定,而不是由变量的声明类型决定。所以一个 Shape shape 变量可以存放 Circle 或 Square,调用 shape.area() 时会自动运行正确的版本。

Java 中重写和重载有什么区别?

重写是子类用相同的签名替换父类的方法,运行时多态正是靠它实现的。重载是一个类中有多个同名但参数列表不同的方法,编译器在编译时根据实参选出其中一个。重写在运行时根据对象的类型解析,重载在编译时根据实参的类型解析。

Java 中向上转型和向下转型有什么区别?

向上转型是把子类对象当作父类类型来对待(Animal a = new Dog();),它总是安全的,而且通常是隐式的。向下转型则反过来(Dog d = (Dog) a;),只有当对象确实是那个子类型时才安全,否则会抛出 ClassCastException。每次向下转型之前都先用 instanceof 检查。

Coddy 编程语言插图

和 Coddy 一起学编程

开始