1. 为什么我们需要继承?
第一次接触继承这个概念时,我正试图为一个电商系统编写商品类。当时我创建了BookProduct、ClothingProduct、ElectronicProduct三个类,结果发现它们都有相同的name、price、stock属性,以及相同的getPrice()、reduceStock()方法。这种重复代码让我抓狂,直到我发现了继承这个神器。
继承是面向对象编程的三大特性之一(另外两个是封装和多态),它允许我们基于已有的类创建新类。想象一下乐高积木:基础积木块已经提供了稳定的结构和连接方式,我们只需要在上面添加特殊部件就能构建出城堡、飞船或任何你想要的东西。Java中的继承也是这样工作的。
注意:虽然继承能减少代码重复,但过度使用会导致类之间耦合度过高。就像现实中的亲子关系一样,过于紧密的依赖可能让双方都失去独立性。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 继承的基本语法
让我们从一个最简单的例子开始:
java复制class Animal {
void eat() {
System.out.println("动物在吃东西");
}
}
class Dog extends Animal {
void bark() {
System.out.println("汪汪叫");
}
}
这里,Dog类通过extends关键字继承了Animal类。这意味着:
- Dog自动获得了Animal的eat()方法
- Dog可以有自己的特有方法bark()
- Dog还可以重写(override)父类的方法
测试这个继承关系:
java复制public class Main {
public static void main(String[] args) {
Dog myDog = new Dog();
myDog.eat(); // 继承自Animal
myDog.bark(); // Dog自己的方法
}
}
输出:
code复制动物在吃东西
汪汪叫
2.1 继承中的术语澄清
刚开始学习时,我经常混淆这些术语:
- 父类/超类(Superclass):被继承的类(如Animal)
- 子类/派生类(Subclass):继承的类(如Dog)
- IS-A关系:Dog IS-A Animal(狗是一种动物)
提示:Java是单继承语言,一个类只能直接继承一个父类。这与C++的多继承不同,避免了"菱形继承"等复杂问题。
3. 方法重写(Override)实战
继承不只是复制粘贴父类的代码,更重要的是可以修改和扩展行为。这就是方法重写的用武之地。
java复制class Animal {
void makeSound() {
System.out.println("动物发出声音");
}
}
class Cat extends Animal {
@Override
void makeSound() {
System.out.println("喵喵叫");
}
void scratch() {
System.out.println("猫在抓沙发");
}
}
这里的关键点:
- 使用
@Override注解(虽然不是必须的,但强烈推荐) - 方法签名必须完全相同(名称、参数、返回类型)
- 子类方法不能比父类方法有更严格的访问权限(如父类是public,子类不能是private)
测试重写效果:
java复制Animal myCat = new Cat();
myCat.makeSound(); // 输出"喵喵叫"而非"动物发出声音"
3.1 重写 vs 重载
这是我初学时最常混淆的概念:
- 重写(Override):子类重新定义父类已有的方法,方法签名相同
- 重载(Overload):在同一个类中,方法名相同但参数不同
java复制class Calculator {
// 重载示例
int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; }
// 可以被重写的方法
void showResult(int result) {
System.out.println("结果是: " + result);
}
}
class ScientificCalculator extends Calculator {
// 重写示例
@Override
void showResult(int result) {
System.out.println("科学计算结果: " + result);
}
}
4. 继承中的构造方法
构造方法的继承规则曾让我踩过不少坑。与普通方法不同,构造方法不会被继承,但子类必须调用父类的构造方法。
java复制class Vehicle {
private String brand;
Vehicle(String brand) {
this.brand = brand;
System.out.println("Vehicle构造器被调用");
}
}
class Car extends Vehicle {
private int year;
Car(String brand, int year) {
super(brand); // 必须放在第一行
this.year = year;
System.out.println("Car构造器被调用");
}
}
关键规则:
- 子类构造方法必须调用父类构造方法(通过
super()) super()必须是子类构造方法的第一条语句- 如果没有显式调用,编译器会自动插入
super()(仅当父类有无参构造方法时)
警告:如果父类没有无参构造方法,而子类又没有显式调用父类的有参构造方法,编译会失败。这是我初学时最常见的错误之一。
5. 访问控制与继承
Java的访问修饰符在继承中扮演着重要角色:
| 修饰符 | 类内 | 同包 | 子类 | 任意位置 |
|---|---|---|---|---|
| private | ✔️ | ✖ | ✖ | ✖ |
| default | ✔️ | ✔️ | ✖ | ✖ |
| protected | ✔️ | ✔️ | ✔️ | ✖ |
| public | ✔️ | ✔️ | ✔️ | ✔️ |
继承时的可见性规则:
- 子类继承父类的所有非private成员(字段和方法)
- 但子类只能直接访问那些对它可见的成员(public/protected/同包default)
java复制class Parent {
public String publicField = "public";
protected String protectedField = "protected";
String defaultField = "default";
private String privateField = "private";
}
class Child extends Parent {
void printFields() {
System.out.println(publicField); // 可访问
System.out.println(protectedField); // 可访问
System.out.println(defaultField); // 同包时可访问
// System.out.println(privateField); // 编译错误
}
}
6. 实际项目中的继承设计
在真实项目中,继承关系需要精心设计。以电商系统为例:
java复制class Product {
private String id;
private String name;
private double price;
// 构造方法、getter/setter省略
public double calculateDiscount() {
return 0; // 默认无折扣
}
}
class Book extends Product {
private String author;
private String isbn;
@Override
public double calculateDiscount() {
return super.calculateDiscount() + 0.1; // 图书额外10%折扣
}
}
class Electronic extends Product {
private String brand;
private int warrantyMonths;
@Override
public double calculateDiscount() {
return 0.05; // 电子产品固定5%折扣
}
}
设计原则:
- 将共性提取到父类(如id,name,price)
- 特定属性放在子类(如author, brand)
- 可以重写父类方法实现特定行为(如不同商品折扣策略)
7. 继承的替代方案:组合
虽然继承很强大,但并非银弹。过度使用继承会导致:
- 类层次结构过深
- 脆弱的基类问题(父类修改会影响所有子类)
- 不灵活的类关系
这时可以考虑组合优于继承原则:
java复制// 使用继承
class Engine {
void start() { /*...*/ }
}
class Car extends Engine { // 不自然的关系:车不是引擎
// ...
}
// 使用组合
class Car {
private Engine engine; // 车包含引擎
void start() {
engine.start();
}
}
组合的优点:
- 更灵活的关系
- 运行时可以改变行为
- 避免继承层次过深
8. 常见面试题解析
根据提供的热词,这里解析几个关于继承的常见面试题:
8.1 Java为什么是单继承?
Java选择单继承主要是为了避免多继承带来的复杂性,尤其是著名的"菱形问题":
code复制 A
/ \
B C
\ /
D
如果B和C都重写了A的方法,D该继承哪个版本?Java通过单继承+多接口的方式既保持了简单性,又提供了足够的灵活性。
8.2 final关键字与继承
- final类:不能被继承(如String类)
- final方法:不能被子类重写
- final变量:常量,值不能改变
java复制final class ImmutableClass { // 不能被继承
final int MAX_VALUE = 100; // 常量
final void importantMethod() { // 不能被子类重写
// ...
}
}
8.3 抽象类与继承
抽象类是专门设计用来被继承的类:
java复制abstract class Shape {
abstract double area(); // 抽象方法,必须被子类实现
void printArea() { // 具体方法
System.out.println("面积: " + area());
}
}
class Circle extends Shape {
private double radius;
Circle(double r) { radius = r; }
@Override
double area() {
return Math.PI * radius * radius;
}
}
抽象类的特点:
- 不能实例化
- 可以包含抽象方法和具体方法
- 子类必须实现所有抽象方法(除非子类也是抽象类)
9. 继承中的内存机制
理解继承在JVM中的实现有助于深入掌握这个概念。当创建一个子类对象时:
- 加载类的字节码:先加载父类,再加载子类
- 分配内存:包括从父类继承的字段和子类自己的字段
- 初始化:先执行父类构造方法,再执行子类构造方法
java复制class A {
int x = 10;
A() { print(); }
void print() { System.out.println("A.x=" + x); }
}
class B extends A {
int x = 20;
B() { print(); }
void print() { System.out.println("B.x=" + x); }
}
new B();
输出:
code复制B.x=0
B.x=20
这个看似奇怪的结果揭示了:
- 对象创建时,先初始化父类部分
- 此时子类字段还未初始化(int默认0)
- 方法调用是动态绑定的,即使是在构造方法中
10. 继承的最佳实践
根据多年Java开发经验,我总结了这些继承使用原则:
-
LSP原则(里氏替换原则):子类应该能够替换父类而不破坏程序。这意味着:
- 子类不能缩小父类方法的访问权限
- 子类不能抛出比父类更多的异常
- 子类方法的返回值类型应该与父类兼容
-
控制继承层次深度:通常不超过3层。过深的继承树会:
- 增加理解难度
- 降低灵活性
- 增加维护成本
-
为继承而设计,否则禁止继承:
- 如果类不是专门为继承设计的,声明为final
- 文档化可重写的方法的预期行为
- 避免在构造方法中调用可重写的方法
-
优先使用组合:当出现以下情况时,考虑用组合替代继承:
- 只是想复用代码,没有真正的"is-a"关系
- 需要从多个来源复用行为
- 需要在运行时改变行为
-
模板方法模式:这是继承的一个优雅用法:
java复制abstract class Game {
// 模板方法
final void play() { // final防止子类修改算法结构
initialize();
startPlay();
endPlay();
}
abstract void initialize();
abstract void startPlay();
abstract void endPlay();
}
class Cricket extends Game {
void initialize() { System.out.println("板球游戏初始化"); }
void startPlay() { System.out.println("板球游戏开始"); }
void endPlay() { System.out.println("板球游戏结束"); }
}
这种模式在框架设计中非常常见,它定义了算法的骨架,而将一些步骤延迟到子类中实现。
