多态
在 Java 里,**多态(Polymorphism)**是面向对象三个核心特性之一:封装、继承、多态。 你现在可以先把多态记成一句话:
同一个父类类型,可以指向不同的子类对象;调用同一个方法时,会根据实际对象执行不同的行为。
1. 先看一个最简单的例子
假设有一个父类 Animal:
class Animal { public void speak() { System.out.println("动物发出声音"); }}两个子类分别重写 speak():
class Dog extends Animal { @Override public void speak() { System.out.println("狗:汪汪汪"); }}
class Cat extends Animal { @Override public void speak() { System.out.println("猫:喵喵喵"); }}然后:
Animal a1 = new Dog();Animal a2 = new Cat();
a1.speak();a2.speak();输出:
狗:汪汪汪猫:喵喵喵关键就在这里:
Animal a1 = new Dog();Animal a2 = new Cat();变量的类型都是:
Animal但实际创建的对象分别是:
DogCat所以执行:
a1.speak();Java 会发现 a1 实际指向的是 Dog 对象,于是调用:
Dog.speak()这就是多态。
2. 多态最重要的理解:左边看类型,右边看对象
例如:
Animal animal = new Dog();可以把它拆成:
Animal animal ↑引用类型 / 编译时类型
new Dog() ↑实际对象 / 运行时类型所以:
animal.speak();如果 Dog 重写了 speak(),最终执行的是:
Dog 的 speak()这叫做动态绑定 / 动态方法分派。
也就是说:
编译的时候看左边,运行的时候调用哪个被重写的方法主要看右边实际是什么对象。
这句话在学习 Java 多态时非常重要。
3. 为什么需要多态?
如果没有多态,假设你要让很多动物发出声音:
Dog dog = new Dog();Cat cat = new Cat();
dog.speak();cat.speak();动物越来越多:
Dog dog = new Dog();Cat cat = new Cat();Tiger tiger = new Tiger();Bird bird = new Bird();代码会越来越依赖具体类型。
有多态之后,可以统一写:
Animal dog = new Dog();Animal cat = new Cat();Animal tiger = new Tiger();
dog.speak();cat.speak();tiger.speak();甚至可以:
public static void makeSound(Animal animal) { animal.speak();}然后:
makeSound(new Dog());makeSound(new Cat());makeSound(new Tiger());makeSound() 根本不用关心传进来的是狗、猫还是老虎。
只需要知道:
它是一个 Animal然后:
animal.speak();不同对象自己决定应该怎么叫。
这才是多态真正有价值的地方:让程序依赖抽象,而不是依赖具体实现。
4. 一个更接近实际开发的例子
以后做 Java 项目,你会经常看到这种代码:
interface Payment { void pay(double money);}支付宝:
class Alipay implements Payment { @Override public void pay(double money) { System.out.println("支付宝支付:" + money); }}微信支付:
class WeChatPay implements Payment { @Override public void pay(double money) { System.out.println("微信支付:" + money); }}银行卡:
class BankCardPay implements Payment { @Override public void pay(double money) { System.out.println("银行卡支付:" + money); }}业务代码可以只写:
public void checkout(Payment payment) { payment.pay(100);}于是:
checkout(new Alipay());checkout(new WeChatPay());checkout(new BankCardPay());checkout() 不需要针对支付宝写一个版本、微信写一个版本、银行卡再写一个版本。
这就是实际开发中多态非常重要的原因。
5. 多态通常需要满足什么条件?
你目前学习阶段可以记住三个条件:
-
存在继承或接口实现关系
-
子类重写父类方法
-
父类/接口引用指向子类对象
经典形式:
父类类型 变量 = new 子类();例如:
Animal animal = new Dog();或者接口:
Payment payment = new Alipay();然后:
payment.pay(100);执行的是 Alipay 重写后的 pay()。
6. 一个特别容易考的坑
看:
class Animal { public void eat() { System.out.println("eat"); }}
class Dog extends Animal { public void bark() { System.out.println("汪汪"); }}然后:
Animal animal = new Dog();下面这个可以:
animal.eat();但这个不可以:
animal.bark(); // 编译错误你可能会觉得:
animal 明明指向的是 Dog,为什么不能调用 bark()?
因为变量能够调用哪些方法,首先看引用类型。
这里:
Animal animal编译器看到的是 Animal 类型。
Animal 里面没有:
bark()所以编译直接不通过。
但是对于父类本来就存在、又被子类重写的方法:
animal.speak();编译器看到:
Animal 有 speak()所以允许调用。
运行的时候再发现:
实际对象是 Dog于是执行 Dog.speak()。
所以可以总结成一个非常实用的口诀:
能不能调用,看左边;实际执行谁,看右边。
严格来说这个口诀主要针对实例方法的重写和多态调用,字段、静态方法等情况不能简单套用。
7. 再看一道面试/考试常见题
class Animal { public void speak() { System.out.println("Animal"); }}
class Dog extends Animal { @Override public void speak() { System.out.println("Dog"); }
public void run() { System.out.println("Dog running"); }}执行:
Animal animal = new Dog();
animal.speak();答案:
Dog因为 Dog 重写了 speak()。
但是:
animal.run();会:
编译错误因为引用类型 Animal 中不存在 run()。
如果真的需要调用:
Dog dog = (Dog) animal;dog.run();这就是后面会学习的向下转型。
8. 把继承、重写、多态串起来
这三个知识点其实是一条链:
继承 ↓Animal ← Dog ↓ 重写 speak() ↓Animal a = new Dog(); ↓ 多态 ↓ a.speak() ↓实际执行 Dog.speak()所以学习顺序最好是:
继承 → 方法重写 → 向上转型 → 多态 → 向下转型 → instanceof → 抽象类 → 接口
你只要真正理解下面这段代码,多态的核心基本就掌握了:
Animal animal = new Dog();animal.speak();脑子里同时想到:
左边 Animal→ 决定我“能调用什么”
右边 Dog→ 决定被重写的方法“实际执行谁”父类引用可以调用父类声明的方法;如果实际子类对象重写了该方法,就执行子类版本,否则执行继承的父类版本。
这就是 Java 多态最核心的思想。