在 Java 编程中,“面向对象” 不是一句空洞的口号,而是通过具体特性解决实际问题的思路。比如我们想描述 “狗” 和 “猫” 这两种动物时,会发现它们都有名字、年龄,都会吃饭、睡觉 —— 这些重复的代码如果每次都手动编写,不仅效率低,后续修改也会变得繁琐。而继承和多态,正是 Java 为解决这类问题提供的两大核心特性。今天我们就从实际场景出发,一步步拆解这两个概念的用法与设计思想。
一、继承
1. 为什么需要继承?
如果分别定义Dog和Cat类,会发现大量重复逻辑:
//狗类
class Dog {
String name;
int age;
//吃饭
public void eat() {
System.out.println(name + "啃着骨头吃饭");
}
//睡觉
public void sleep() {
System.out.println(name + "蜷缩着睡觉");
}
//狗特有:叫
public void bark() {
System.out.println(name + "汪汪叫个不停");
}
}
//猫类
class Cat {
String name; //重复
int age; //重复
//吃饭(重复逻辑)
public void eat() {
System.out.println(name + "舔着猫粮吃饭");
}
//睡觉(重复逻辑)
public void sleep() {
System.out.println(name + "蜷在沙发上睡觉");
}
//猫特有:叫
public void mew() {
System.out.println(name + "喵喵叫");
}
}
name、age属性,eat()、sleep()方法在两个类中完全重复。如果后续要加 “体重” 属性,就得同时修改两个类 —— 这显然不符合 “一次修改,处处生效” 的原则。
这时候,继承就派上用场了:我们可以抽取共性,定义一个 “父类”(比如Animal),让Dog和Cat作为 “子类” 继承父类的共性,再专注于自己的特有逻辑。
2. 继承的核心语法与概念
Java 中用extends关键字表示继承关系,核心概念如下:
- 父类(超类 / 基类):抽取共性的类(如
Animal); - 子类(派生类):继承父类的类(如
Dog、Cat); - 继承效果:子类自动拥有父类的非私有成员(属性和方法),只需编写自己的特有成员。
用继承重构上面的代码,会简洁很多:
//父类:动物
class Animal {
String name;
int age;
//共性方法:吃饭
public void eat() {
System.out.println(name + "正在吃饭");
}
//共性方法:睡觉
public void sleep() {
System.out.println(name + "正在睡觉");
}
}
//子类:狗(继承Animal)
class Dog extends Animal {
//特有方法:叫
public void bark() {
System.out.println(name + "汪汪叫个不停");
}
}
//子类:猫(继承Animal)
class Cat extends Animal {
//特有方法:叫
public void mew() {
System.out.println(name + "喵喵叫");
}
}
测试一下效果:子类可以直接使用父类的成员,无需重复定义:
public class TestInherit {
public static void main(String[] args) {
Dog dog = new Dog();
dog.name = "小七"; //父类属性
dog.eat(); //父类方法:小七正在吃饭
dog.bark(); //子类特有方法:小七汪汪叫个不停
Cat cat = new Cat();
cat.name = "元宝"; //父类属性
cat.sleep(); //父类方法:元宝正在睡觉
cat.mew(); //子类特有方法:元宝喵喵叫
}
}
3. 继承中的关键细节
(1)父类成员的访问规则
子类访问父类成员时,遵循 “就近原则”:
- 如果子类没有与父类同名的成员,直接访问父类的;
- 如果子类有与父类同名的成员,优先访问子类自己的;
- 如果想强制访问父类的同名成员,用
super关键字。
比如子类和父类都有a属性,用super.a访问父类的a:
class Base {
int a = 10; //父类的a
}
class Derived extends Base {
int a = 20; //子类的a
public void show() {
System.out.println(a); //优先子类:20
System.out.println(super.a); //强制父类:10
}
}
(2)子类构造方法:先有父,再有子
创建子类对象时,会先执行父类的构造方法,再执行子类的构造方法—— 因为子类的成员依赖父类的共性成员,必须先初始化父类。
- 如果父类有默认无参构造(没写构造方法时,Java 会自动生成),子类构造会默认调用
super()(即使不写,编译器也会加); - 如果父类只有带参构造,子类必须显式用
super(参数)调用父类的带参构造,否则编译报错。
例子:父类Animal有带参构造,子类Dog必须显式调用:
class Animal {
String name;
int age;
//父类带参构造(无默认无参构造)
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
}
class Dog extends Animal {
//子类构造必须显式调用父类带参构造
public Dog(String name, int age) {
super(name, age); //必须放在子类构造第一行
}
}
(3)访问权限:protected 的特殊作用
继承中,父类成员的访问权限会影响子类的可见性。其中protected是为继承设计的权限:
- 同一包内:子类可直接访问父类的
protected成员; - 不同包内:子类仍可访问父类的
protected成员,但非子类(普通类)不能访问。
比如父类Animal在包 A,子类Dog在包 B,Dog可以访问Animal的protected属性,但包 B 的普通类Test不能访问。
(4)Java 不支持多继承,但支持多层继承
Java 的继承是 “单继承” 的 —— 一个子类只能有一个直接父类(比如Dog不能同时继承Animal和Pet),但支持 “多层继承”(比如Dog继承Animal,Husky继承Dog)。
如果想限制一个类被继承,可以用final关键字修饰类(比如 Java 的String类就是final的,不能被继承):
final class Animal { //被final修饰,不能被继承
//...
}
//编译报错:无法从最终类Animal继承
class Dog extends Animal { }
4. 继承 vs 组合:该怎么选?
除了继承,组合也是实现代码复用的方式 —— 它表示 “has-a”(有一个)的关系,而继承是 “is-a”(是一个)的关系。比如 “汽车” 和 “发动机”:汽车有发动机(组合),而不是汽车是发动机(继承);“奔驰”是汽车(继承),而不是奔驰有汽车(组合)。
组合的实现很简单:把一个类的实例作为另一个类的属性:
//发动机类
class Engine {
public void start() {
System.out.println("发动机启动");
}
}
//汽车类(组合发动机)
class Car {
private Engine engine = new Engine(); //组合Engine
public void startCar() {
engine.start(); //复用Engine的方法
System.out.println("汽车启动");
}
}
//奔驰类(继承Car)
class Benz extends Car { }
设计建议:能用组合尽量用组合。因为继承会让子类和父类耦合度太高(父类修改会影响所有子类),而组合的耦合度更低,更灵活。
二、多态:让代码 “灵活扩展” 的底层逻辑
1. 什么是多态?
通俗来说,多态就是 “同一行为,不同对象有不同表现”。比如 “吃饭” 这个行为:
- 狗吃饭是 “啃骨头”;
- 猫吃饭是 “舔猫粮”;
- 人吃饭是 “用筷子夹菜”。
在代码中,多态的核心是:父类引用指向子类对象,调用方法时会执行子类的实现。
2. 多态的实现条件
要实现多态,必须满足三个条件,缺一不可:
- 存在继承关系(子类继承父类);
- 子类重写父类的方法;
- 通过父类的引用调用重写的方法。
我们用一个完整例子演示:
//1. 父类:Animal
class Animal {
String name;
public Animal(String name) {
this.name = name;
}
//父类方法:吃饭(将被重写)
public void eat() {
System.out.println(name + "正在吃饭");
}
}
//2. 子类:Dog(继承Animal,重写eat)
class Dog extends Animal {
public Dog(String name) {
super(name);
}
//重写父类的eat方法
@Override
public void eat() {
System.out.println(name + "啃着骨头吃饭");
}
}
//2. 子类:Cat(继承Animal,重写eat)
class Cat extends Animal {
public Cat(String name) {
super(name);
}
//重写父类的eat方法
@Override
public void eat() {
System.out.println(name + "舔着猫粮吃饭");
}
}
//3. 测试:父类引用指向子类对象
public class TestPolymorphism {
//方法参数是父类引用(Animal)
public static void feed(Animal animal) {
animal.eat(); //调用重写的方法
}
public static void main(String[] args) {
Animal dog = new Dog("小七"); //父类引用指向Dog对象
Animal cat = new Cat("元宝"); //父类引用指向Cat对象
feed(dog); //输出:小七啃着骨头吃饭
feed(cat); //输出:元宝舔着猫粮吃饭
}
}
这里feed方法的参数是Animal(父类),但传入Dog和Cat(子类)对象时,会自动执行子类的eat方法 —— 这就是多态的魔力。
3. 关键:方法重写(Override)
多态的核心是 “重写”—— 子类对父类的方法进行重新实现,规则如下:
- 方法签名必须完全一致:方法名、参数列表、返回值类型(返回值可以是父类返回值的子类,比如父类返回
Animal,子类返回Dog); - 访问权限:子类重写的方法权限不能比父类更严格(比如父类是
public,子类不能是protected); - 不能重写的方法:
static方法、private方法、final方法(final修饰的方法不能被重写); - 建议加
@Override注解:编译器会校验是否符合重写规则,避免拼写错误(比如把eat写成eet)。
重写 vs 重载(Overload)
很多人会混淆重写和重载,这里用表格清晰区分:
| 对比维度 | 重写(Override) | 重载(Overload) |
|---|---|---|
| 作用范围 | 父子类之间 | 同一类内部 |
| 方法签名 | 必须完全一致 | 必须不同(参数列表不同) |
| 返回值 | 必须一致(或父子关系) | 可任意修改 |
| 访问权限 | 不能更严格 | 可任意修改 |
| 与多态的关系 | 多态的核心(父子类多态) | 类内多态(同一类的不同表现) |
4. 向上转型与向下转型
多态中,父类引用指向子类对象的过程叫 “向上转型”,反之叫 “向下转型”。
(1)向上转型(安全)
“向上转型” 是自动的,比如Animal dog = new Dog("小七")—— 相当于 “把小范围的Dog,放到大范围的Animal中”,是安全的。
常见场景:
- 直接赋值:
Animal dog = new Dog("小七"); - 方法传参:
feed(new Dog("小七"))(feed参数是Animal); - 方法返回值:
public static Animal getAnimal() { return new Dog("小七"); }。
优点:代码更灵活,无需为每个子类写单独的方法;
缺点:父类引用不能调用子类的特有方法(比如dog.bark()会编译报错,因为Animal类没有bark方法)。
(2)向下转型(不安全,需谨慎)
如果想调用子类的特有方法,就需要 “向下转型”—— 把父类引用强制转回子类类型。但这种转型是不安全的,比如把指向Dog的Animal引用转成Cat,会抛出ClassCastException(类型转换异常)。
解决办法:用instanceof关键字先判断类型,再转型:
public class TestCast {
public static void main(String[] args) {
Animal animal = new Dog("小七"); //向上转型
//错误:直接转型,编译通过但运行报错
//Cat cat = (Cat) animal;
//正确:先用instanceof判断
if (animal instanceof Dog) {
Dog dog = (Dog) animal; //安全转型
dog.bark(); //调用子类特有方法:小七汪汪叫个不停
}
if (animal instanceof Cat) {
Cat cat = (Cat) animal;
cat.mew();
}
}
}
instanceof会判断 “父类引用指向的对象是否是某个子类类型”,返回true才转型,避免异常。
5. 多态的优缺点
优点:
1.降低圈复杂度,减少 if-else:比如要打印多种图形(圆形、矩形、花朵),不用多态的话需要大量if-else判断类型;用多态只需遍历父类数组,调用统一方法。
//用多态的简洁代码
Shape[] shapes = {new Circle(), new Rect(), new Flower()};
for (Shape s : shapes) {
s.draw(); //自动调用子类的draw方法
}
2.可扩展性强:新增一个子类(比如Triangle),只需重写draw方法,无需修改原有代码(符合 “开闭原则”:对扩展开放,对修改关闭)。
缺点:
- 运行效率略低:多态依赖 “动态绑定”(运行时才确定调用哪个方法),比静态绑定(编译时确定)慢一点;
- 属性没有多态性:如果父类和子类有同名属性,父类引用只能访问父类的属性(比如
Animal有age=1,Dog有age=2,Animal dog = new Dog(),dog.age是 1); - 构造方法没有多态性:且不要在构造方法中调用重写的方法(会触发动态绑定,此时子类对象还没初始化,可能出现奇怪问题)。
6. 避坑:构造方法中不要调用重写的方法
class Parent {
public Parent() {
func(); //构造方法中调用func
}
public void func() {
System.out.println("Parent.func()");
}
}
class Child extends Parent {
private int num = 1; //子类属性
@Override
public void func() {
System.out.println("Child.func() " + num);
}
}
public class TestPit {
public static void main(String[] args) {
new Child(); //输出什么?
}
}
输出结果:Child.func() 0
原因:创建Child对象时,会先执行Parent的构造方法,Parent的func()会触发动态绑定,调用Child的func()。但此时Child的num还没初始化(Java 中成员变量默认值是 0),所以输出0。
结论:构造方法中尽量只做简单的初始化(比如给属性赋值),不要调用实例方法(尤其是可能被重写的方法)。
三、总结:继承与多态的设计思想
- 继承:核心是 “代码复用”,通过抽取共性减少重复逻辑,但要注意耦合度(尽量用组合替代继承);
- 多态:核心是 “灵活扩展”,通过父类引用指向子类对象,让代码适应变化(新增子类无需修改原有逻辑);
- 底层逻辑:多态依赖 “动态绑定”(运行时确定调用的方法),而继承是多态的基础。
掌握继承与多态,不仅能写出更简洁、灵活的代码,更能理解面向对象设计的核心思想 ——封装(隐藏细节)、继承(复用代码)、多态(扩展能力),这三大特性共同构成了 Java 面向对象的基石。希望这篇文章能帮你理清继承与多态的逻辑,下次写代码时,不妨多思考:“这里用继承还是组合?能不能用多态让代码更灵活?”—— 好的设计,都是在不断思考中打磨出来的。
Java继承与多态的实现与应用

被折叠的 条评论
为什么被折叠?



