引文
在面向对象的世界里,Java多态如同棱镜折射阳光:一段父类引用的代码,却能投射出子类各异的行为光谱。编译时多态以方法重载编织参数组合的变奏曲,运行时多态则借动态绑定让继承树上的每个节点绽放独特芬芳。
当父类变量轻触子类对象,JVM的虚方法表便化作隐形指挥家,让每个重写的方法在内存中跳起专属的芭蕾。这种“形而上”的智慧,既成全了策略模式中算法的自由置换,也铸就了工厂模式里对象的灵动新生。开发者只需执掌继承、重写与向上转型的密钥,便能解锁“万物皆对象”的终极隐喻——同一行animal.speak()
,可以是虎啸深林,亦能化燕语雕梁。
多态以抽象为舟,载着代码穿越具象的河流,在扩展性与维护性之间架起黄金桥梁。它不仅是技术规则,更是将生物多样性写入数字基因的诗意尝试:在类型系统的疆域里,演绎着“一即是全”的哲学寓言
目录
一、多态的概念
多态(Polymorphism)是面向对象编程的三大核心特性之一(另两个是封装和继承),它允许不同类的对象对同一方法或操作做出不同的响应。简单来说,多态意味着“同一接口,多种实现”。具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
举个例子吧:
打印机分为黑白打印机和彩色打印机,它们都可以实现打印,但是黑白打印机显然只能打印出黑白两种颜色,而彩色打印机却可以打印出多种颜色。
这里还是用经典的Animal和Dog、Cat来实现代码:
在这串代码中,Animal是父类(基类),Dog类和Cat类是Animal的两个子类。
可以看到,在Animal中,我们定义了三个成员变量,并通过构造方法对其初始化;接着,我们又定义了一个公共方法——eat。
在两个子类中,我们则通过super关键字,将传给它们构造方法的参数传递给它们父类(Animal)的构造方法,对Animal中定义的三个成员变量进行初始化。
在两个子类中,我对Animal中定义的方法进行了重写:
如此,我们在主类First的主方法中,通过向上转型,将Dog实例和Cat实例的对象引用,赋给父类对象引用(Animal)。
然后,我们通过父类对象引用,引用子类对象,并调用子类对象中的eat方法:
我在这三个类的构造方法中,都添加了一句打印语句,通过运行结果可以看到,三个构造方法的运行顺序。“Animal created”运行两次的原因是:创建了两次实例,每创建一次实例,参数都会通过子类的构造方法,传递给父类的构造方法。
通过运行结果,可以清晰地看出,两个子类中的eat方法都成功运行,并且运行结果并不是父类(Animal)中的eat方法的结果,而是两个子类中定义的eat方法的运行结果。
这就是经典的多态了。
二、多态的实现
实现运行时多态需要满足:
-
继承关系:存在父类与子类的层次结构。
-
方法重写:子类必须重写父类的方法。
-
向上转型:父类引用指向子类对象(如
Animal a = new Cat()
)。
想象“动物”都有“叫”的行为,但不同动物叫声不同。多态允许我们统一调用 animal.makeSound()
,而实际执行的是猫、狗或鸟的叫声,具体取决于对象类型。这种“同一行为,不同表现”的特性,就是多态的威力。
1、继承关系
所谓继承,就是将多个类的共同特点抽取出来,放在一个事类中。拥有这些共同特点的类,可以通过extends关键字,与这个类达成继承关系。这个被继承的类,就是父类(或基类),继承父类成员变量或成员方法的类,就是子类(或派生类)。
在之前的篇章——“java SE -- 继承”中,我花费五千多字,详细讲解过继承,在这里不做赘述,有兴趣的朋友可以移步——
2、 方法重写
Java中的方法重写是指子类重新定义父类中已有的方法,以提供更适合自身功能的实现。这是面向对象编程中实现多态性的关键机制之一。
(1)方法重写的规则
-
方法签名必须相同
方法名、参数列表(类型、顺序、数量)必须与父类方法完全一致。 -
返回类型兼容
-
基本类型:必须与父类方法返回类型相同。
-
引用类型:可以是父类方法返回类型的子类(协变返回类型,Java 5+支持)。
class Animal {} class Dog extends Animal {} // Dog 是 Animal 的子类 class Parent { public Animal create() { // 父类方法返回 Animal return new Animal(); } } class Child extends Parent { @Override public Dog create() { // 子类重写方法返回 Dog(Animal 的子类) return new Dog(); // 协变返回类型! } }
-
-
访问权限不能更严格
子类方法的访问修饰符必须 ≥ 父类方法的权限(例如,父类方法是protected
,子类可以是public
或protected
,但不能是private
)。 -
异常处理限制
子类方法抛出的检查异常(Checked Exception)不能比父类方法更宽泛,可以不抛出异常或抛出更具体的异常。 -
@Override
注解
建议使用@Override
注解标记重写的方法,编译器会检查是否符合重写规则。
(2)方法重写与方法重载
特性 | 重写 (Override) | 重载 (Overload) |
---|---|---|
作用范围 | 子类与父类之间 | 同一类中 |
方法签名 | 必须相同 | 必须不同(参数列表不同) |
返回类型 | 必须兼容 | 可以不同 |
访问权限 | 不能更严格 | 可以不同 |
多态性 | 基于对象类型(运行时多态) | 基于参数类型(编译时多态) |
注意:
静态方法不能被重写
静态方法属于类,若子类定义同名静态方法,属于隐藏,而非重写。final方法禁止重写
父类中声明为final
的方法不可被子类重写。私有方法无法重写
父类的私有方法对子类不可见,因此不能重写。
3、向上转型与向下转型
在 Java 中,向上转型是子类对象隐式转换为父类类型的过程,无需显式类型转换符,是实现多态的基础。
(1)向上转型
A、向上转型的方式
1.直接赋值
将子类对象直接赋值给父类引用变量:
class Animal {}
class Cat extends Animal {} // Cat 是 Animal 的子类
public class Main {
public static void main(String[] args) {
// 向上转型:子类对象赋值给父类引用
Animal animal = new Cat();
}
}
这种方式十分安全,无需进行强制类型转换,java会自动完成类型提升。因为子类对象一定是父类类型(如狗一定是动物)。
2.方法参数传递
将子类对象作为参数传递给接收父类类型的方法:
class Animal {}
class Dog extends Animal {}
public class Main {
// 方法参数类型为父类 Animal
public static void feed(Animal animal) {
System.out.println("喂食动物");
}
public static void main(String[] args) {
Dog dog = new Dog();
feed(dog); // 向上转型:Dog → Animal
}
}
3. 集合存储
将不同子类对象存入父类类型的集合中:
import java.util.ArrayList;
import java.util.List;
class Animal {}
class Bird extends Animal {}
class Fish extends Animal {}
public class Main {
public static void main(String[] args) {
List<Animal> zoo = new ArrayList<>();
zoo.add(new Bird()); // 向上转型:Bird → Animal
zoo.add(new Fish()); // 向上转型:Fish → Animal
}
}
B、向上转型后的特点
- 只能访问父类成员
父类引用无法直接调用子类特有的方法或属性:class Animal { public void eat() { System.out.println("动物吃东西"); } } class Cat extends Animal { public void catchMouse() { System.out.println("猫抓老鼠"); } // 子类特有方法 } public class Main { public static void main(String[] args) { Animal animal = new Cat(); animal.eat(); // 可以调用父类方法 // animal.catchMouse(); // 编译错误!父类引用无法访问子类特有方法 } }
- 多态性
如果子类重写了父类方法,调用时会执行子类的方法:class Animal { public void makeSound() { System.out.println("动物叫"); } } class Dog extends Animal { @Override public void makeSound() { System.out.println("汪汪汪"); } // 重写父类方法 } public class Main { public static void main(String[] args) { Animal animal = new Dog(); // 向上转型 animal.makeSound(); // 输出“汪汪汪”(实际调用子类方法) } }
(2)向下转型
在 Java 中,向下转型(Downcasting)是将父类类型的引用强制转换为其子类类型的过程。与向上转型不同,向下转型需要显式类型转换符((子类名)
),并且存在类型不安全的风险。
A、向下转型的必要性
当父类引用指向一个子类对象时,虽然可以通过多态调用重写的方法,但无法直接访问子类特有的成员(方法或属性)。此时需要通过向下转型恢复对象的子类类型,以调用其特有功能。
class Animal {
public void eat() { System.out.println("动物吃东西"); }
}
class Cat extends Animal {
@Override
public void eat() { System.out.println("猫吃鱼"); }
// 子类特有方法
public void catchMouse() {
System.out.println("猫抓老鼠");
}
}
public class Main {
public static void main(String[] args) {
Animal animal = new Cat(); // 向上转型
animal.eat(); // 输出“猫吃鱼”(多态)
// animal.catchMouse(); // 编译错误!Animal引用无法直接调用子类方法
Cat cat = (Cat) animal; // 向下转型:强制转换为Cat类型
cat.catchMouse(); // 输出“猫抓老鼠”
}
}
B、向下转型的实现步骤
-
强制类型转换
使用
(子类名)
显式转换:父类引用 变量名 = new 子类(); // 向上转型 子类名 子类变量 = (子类名) 父类引用; // 向下转型
-
类型检查(
instanceof
)为避免
ClassCastException
,转换前必须用instanceof
检查实际类型:if (父类引用 instanceof 子类名) { 子类名 子类变量 = (子类名) 父类引用; // 调用子类特有方法 }
-
示例:
Animal animal = new Cat(); // 假设 animal 实际指向 Cat 对象 if (animal instanceof Cat) { Cat cat = (Cat) animal; // 安全向下转型 cat.catchMouse(); // 成功调用 } else { System.out.println("无法转换"); }
C、向下转型的风险
-
ClassCastException
如果父类引用实际指向的对象与目标子类不匹配,会抛出运行时异常:
Animal animal = new Dog(); // 实际是 Dog 对象 Cat cat = (Cat) animal; // 抛出 ClassCastException
必须显式检查
未使用 instanceof
检查直接转换是危险的:
// 危险代码(可能崩溃)
Animal animal = new Animal(); // 父类对象(不是子类)
Cat cat = (Cat) animal; // 运行时异常
(3)对比
特性 | 向下转型 | 向上转型 |
---|---|---|
方向 | 父类 → 子类 | 子类 → 父类 |
安全性 | 不安全(需显式检查和转换) | 安全(自动完成) |
显式/隐式 | 显式(需强制类型转换符) | 隐式(无需代码干预) |
用途 | 恢复子类特有功能 | 统一接口处理对象 |
注意:
优先使用多态
如果频繁需要向下转型,可能说明代码设计有问题,应尝试通过多态或接口优化。避免滥用
instanceof
过多类型检查会导致代码冗余,考虑使用设计模式(如工厂模式、策略模式)替代。继承体系的合理性
确保向下转型的目标类型是实际对象的子类,否则转换必然失败。