父类引用只能调用编译时类型声明的方法,如Animal animal = new Dog()中仅能调用Animal中定义的方法;运行时则根据实际对象类型动态绑定非private、非static、非final的实例方法,实现多态。

父类引用能调用哪些方法,由编译时类型决定
你写 Animal animal = new Dog();,编译器只认 animal 是 Animal 类型,所以它只允许你调用 Animal 中声明过的方法。哪怕 Dog 自己多写了 bark(),你直接写 animal.bark() 就会报错:「cannot resolve method bark()」。
这不是运行时限制,是编译器在检查语法时就卡死的。它不关心右边是不是 Dog,只看左边声明的类型。
- 想调用子类特有方法?必须向下转型:
((Dog) animal).bark() - 转型前建议先用
instanceof判断,否则可能抛ClassCastException - 如果父类方法是
private或static,子类重写也无效——多态不作用于这两类
实际运行时执行哪个方法,由对象真实类型决定
虽然编译器只让调用父类声明的方法,但 JVM 在运行时会查这个对象“到底是谁”。比如 animal.eat(),animal 声明为 Animal,但背后是 Dog 实例,JVM 就会去调 Dog.eat(),而不是 Animal.eat()。
这个过程叫「动态绑定」或「运行时多态」,它依赖两个前提:
-
eat()必须是非private、非static、非final的实例方法 -
Dog确实重写了该方法(用@Override显式标注更安全) - 如果子类没重写,就退回到父类实现,不会报错也不会跳过
为什么说它提升灵活性?看集合和参数传递场景
真正体现价值的地方不是单个变量,而是批量处理。比如你要管理一群动物,不管它们是 Dog、Cat 还是将来加的 Bird:
List<animal> animals = Arrays.asList(new Dog(), new Cat(), new Bird());
for (Animal a : animals) {
a.eat(); // 自动分发到各自重写的 eat()
}</animal>
新增子类完全不用改循环逻辑;方法参数也同理:void feed(Animal a) 可以接收任意 Animal 子类实例,调用方无需知道具体类型。
- 这种设计把“变的部分”(具体行为)和“不变的部分”(遍历、传参、调用)隔开
- 如果硬写成
if (a instanceof Dog)分支判断,每加一个子类就要改一次,违反开闭原则 - 接口 + 实现类的组合也能达到类似效果,但父类引用是继承体系下最轻量的多态入口
容易被忽略的关键点:属性访问不具多态性
很多人误以为字段(成员变量)也有多态,其实不然。写 Animal animal = new Dog(); 后,animal.name 拿到的是 Animal 类里定义的 name,不是 Dog 里可能同名但独立声明的字段。
字段访问是静态绑定的,只看引用类型,不看实际对象。这也是为什么多态只说“方法的多态”,从不提“属性的多态”。
- 若需统一访问字段,应在父类中定义并由子类继承,避免子类重复声明同名字段
- 构造器、静态方法、
final方法同样不参与动态绑定 - 调试时注意 IDE 提示的“method call resolved at runtime”——这才是多态生效的信号










