19. 【Java】继承:代码复用的艺术

摘要: 本文深入讲解Java面向对象编程中的继承机制,从重复代码问题引入,详细解析extends关键字、super用法、方法重写规则、构造方法调用链等核心概念。通过学校人员管理系统等实际案例,展示如何通过继承实现代码复用和层次化设计,帮助初学者掌握继承的适用场景与设计考量,为理解多态打下坚实基础。

关键词: Java继承, extends关键字, super关键字, 方法重写, 面向对象编程


在上一篇文章里,我们给代码装上了一道“防护门”——封装。通过把属性设为 private,只暴露受控的方法,我们的类变得安全了、有自我意识了。

今天,我们要给面向对象的大厦添上第二根顶梁柱——继承(Inheritance)

如果说封装是“横向”地把数据和操作放在一起,那继承就是“纵向”地组织类与类之间的关系,让代码能够一层一层地复用


1. 一个熟悉的场景:重复的代码

假设你是学校信息系统的开发者。你需要定义几种角色:Student(学生)、Teacher(老师)、Staff(行政人员)。

你会发现,这三者有大量共同的信息:姓名、年龄、联系方式、ID。如果你独立定义三个类,这些字段和方法每个类都要写一遍:

public class Student {
    private String name;
    private int age;
    private String phone;
    private String studentId;
    // getter/setter 大量重复...
}

public class Teacher {
    private String name;
    private int age;
    private String phone;
    private String employeeId;
    // getter/setter 还是大量重复...
}

public class Staff {
    private String name;
    private int age;
    private String phone;
    private String department;
    // 又是大量重复...
}

你发现了问题:代码在重复。修改一个字段的名字,要改三个类;增加一个公共属性,也要改三个类。这不优雅,也不高效。

这时你就会想:能不能把“人”的共同特征提取出来,让 StudentTeacherStaff 直接“继承”这些共同的东西,然后再加上自己特有的内容?

这就是继承要解决的问题。


2. 什么是继承?—— “父子”关系的建立

继承(Inheritance) 是一种机制,它允许一个类(子类)基于另一个类(父类)来定义,从而获得父类的属性和方法,并可以在此基础上扩展或修改。

  • 父类(Superclass / Parent Class):被继承的类,也叫基类。它包含了通用的属性和行为。
  • 子类(Subclass / Child Class):继承父类的类,也叫派生类。它拥有父类的全部属性和方法(除了 private 的),并可以增加自己特有的内容。

打个比方:你觉得“动物”是一个比较宽泛的概念。动物有名字、会吃东西、会移动。在这个基础上,你定义“狗”是动物的一个特例——狗继承了动物的全部特征(名字、吃、移动),但额外增加了“吠叫”的能力。“猫”也继承了动物,但增加了“捉老鼠”的能力。这就是继承:从“一般”到“特殊”的延伸。

用专业术语说:子类是一种(is-a)父类。狗是一种动物,学生是一个人。继承关系表达的是一种 “is-a” 关系。


3. 继承的语法 —— extends

在 Java 中,继承使用 extends 关键字。

// 父类(基类)
public class Person {
    private String name;
    private int age;
    private String phone;

    // 构造方法
    public Person(String name, int age, String phone) {
        this.name = name;
        this.age = age;
        this.phone = phone;
    }

    // getter/setter
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    // ...
}

然后定义子类,用 extends 继承 Person

// 子类:Student 继承 Person
public class Student extends Person {
    // 新增属性:学号
    private String studentId;

    // 构造方法
    public Student(String name, int age, String phone, String studentId) {
        super(name, age, phone);  // 调用父类的构造方法
        this.studentId = studentId;
    }

    // 新增方法:学习
    public void study() {
        System.out.println(getName() + " 正在学习...");
    }
}
// 子类:Teacher 继承 Person
public class Teacher extends Person {
    private String employeeId;

    public Teacher(String name, int age, String phone, String employeeId) {
        super(name, age, phone);
        this.employeeId = employeeId;
    }

    public void teach() {
        System.out.println(getName() + " 正在讲课...");
    }
}

现在,StudentTeacher 都自动拥有了 Personnameagephone 属性以及它们的 getter/setter,不需要再重复定义了。它们只需要关注自己特有的部分。


4. super 关键字 —— 指向“父辈”的引用

在上面的代码中,你可能已经注意到了 super(name, age, phone); 这个看起来有些神秘的调用。我们来拆解一下。

super() —— 调用父类的构造方法

子类构造方法的第一条语句,必须调用父类的构造方法(使用 super(...)),以确保父类的属性先被正确初始化。

如果父类有无参构造方法,并且子类没有显式调用 super(...),Java 会自动帮你调用父类的无参构造。但如果父类只有有参构造(没有无参构造),子类必须显式地用 super(...) 调用。

public class Person {
    private String name;

    // 没有无参构造,只有有参构造
    public Person(String name) {
        this.name = name;
    }
}

public class Student extends Person {
    private int score;

    public Student(String name, int score) {
        super(name);   // 必须显式调用父类的有参构造
        this.score = score;
    }
}

⚠️ 如果父类既有无参构造又有有参构造,子类可以不用显式调用 super(),但如果你需要传入参数,必须显式调用。

super.xxx —— 访问父类的成员

当子类中定义了和父类同名的属性或方法时,super.xxx 用来明确访问父类的版本。

public class Person {
    protected String name = "Person";
    public void sayHello() {
        System.out.println("Hello from Person");
    }
}

public class Student extends Person {
    private String name = "Student";

    public void test() {
        System.out.println(super.name);    // 输出 "Person"
        System.out.println(this.name);     // 输出 "Student"
        super.sayHello();                 // 调用父类的方法
    }
}

注意:superthis 一样,不能用于静态上下文中(因为它们是针对实例的)。


5. 方法重写(Override) —— 子类的“个性化定制”

子类继承了父类的方法,但如果父类的方法在子类的语境下“不适用”,子类可以重新定义该方法——这就是方法重写(Override)

public class Animal {
    public void sound() {
        System.out.println("动物发出声音");
    }
}

public class Dog extends Animal {
    @Override   // 注解,告诉编译器这是重写,帮你检查
    public void sound() {
        System.out.println("汪汪汪!");
    }
}

public class Cat extends Animal {
    @Override
    public void sound() {
        System.out.println("喵喵喵~");
    }
}

使用:

Animal a1 = new Dog();
a1.sound();   // 输出:汪汪汪!

Animal a2 = new Cat();
a2.sound();   // 输出:喵喵喵~
重写的规则
  • 方法名、参数列表必须完全一致(返回类型可以是父类返回类型的子类型,称为“协变返回类型”,初学暂不深入)。
  • 访问修饰符不能比父类更严格(比如父类是 public,子类不能是 private)。
  • final 方法不能重写,static 方法不能被重写(静态方法属于类,没有“重写”的概念,只能说“隐藏”)。
@Override 注解

@Override 是一个注解,它不改变代码的运行,但给编译器传达了信息:“我要重写父类的方法,请帮我检查一下方法签名是否正确。”如果你写错了(比如拼写错误或参数类型不对),编译器会报错。这是一个很好的习惯,一定要加


6. 继承中的构造方法调用链 —— 从顶到底

当创建一个子类对象时,JVM 会调用父类的构造方法,然后调用子类的构造方法。从继承链的顶端(Object)到最底端(当前类),一层一层地初始化。

public class Grandparent {
    public Grandparent() {
        System.out.println("Grandparent 构造方法");
    }
}

public class Parent extends Grandparent {
    public Parent() {
        System.out.println("Parent 构造方法");
    }
}

public class Child extends Parent {
    public Child() {
        System.out.println("Child 构造方法");
    }
}

// 输出:
// Grandparent 构造方法
// Parent 构造方法
// Child 构造方法

记住:父类的构造方法总是先执行。这保证了父类的属性在子类使用之前已经被正确初始化。


7. 单继承 —— Java 的“一个孩子只有一个亲生父亲”

在 Java 中,一个类只能直接继承一个父类(单继承)。这与其他一些语言(如 C++ 支持多继承)不同。

// 这是合法的
public class Student extends Person { ... }

// 这是非法的(编译错误)
public class Student extends Person, Employee { ... }   // ❌

为什么 Java 要这样做?

  1. 避免“菱形继承”问题:如果 A 有方法 m(),B 和 C 都继承自 A 并各自重写了 m(),D 同时继承 B 和 C,那么 D 应该继承谁的 m()?这种歧义在多继承中很容易出现,Java 通过单继承彻底避免了它。
  2. 简化设计:单继承让类的关系更清晰,也更容易理解和维护。

但 Java 提供了接口(Interface) 来实现“多重能力”,我们会在第 22 篇文章中详细讲解。接口允许多实现,可以弥补单继承的不足。


8. 什么时候用继承?—— 设计上的考量

继承是一种强耦合的关系,子类对父类有很强的依赖。所以,使用继承时需要谨慎思考:

✅ 适合用继承的场景

  • 子类和父类之间存在明显的“is-a”关系(狗是一种动物、学生是一个人)。
  • 子类需要父类的全部或大部分功能,且基本不需要修改父类的行为。
  • 你在构建一个框架或公共库,希望用户通过继承来扩展功能。

❌ 不适合用继承的场景

  • 只是为了复用代码(比如恰好都有 nameage 字段),但逻辑上不属于“is-a”关系。
  • 父类的行为在子类中需要大幅修改(那说明它们可能不应该有继承关系)。
  • 深层继承(超过 3 层):继承链过长会让代码难以理解和调试。

著名的设计原则:“优先使用组合而非继承”(Favor composition over inheritance)。但那是进阶话题,初学阶段,我们先掌握好继承这个工具。


9. 综合示例 —— 学校人员管理系统

让我们把今天的内容串起来,写一个完整的例子:

// ========== 父类:Person ==========
public class Person {
    private String name;
    private int age;
    private String phone;

    public Person(String name, int age, String phone) {
        this.name = name;
        this.age = age;
        this.phone = phone;
    }

    // Getter/Setter(省略部分,避免代码过长)
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }

    public String getPhone() { return phone; }
    public void setPhone(String phone) { this.phone = phone; }

    // 公共方法
    public void introduce() {
        System.out.println("我是 " + name + ",今年 " + age + " 岁。");
    }
}

// ========== 子类:Student ==========
public class Student extends Person {
    private String studentId;
    private String major;

    public Student(String name, int age, String phone, String studentId, String major) {
        super(name, age, phone);
        this.studentId = studentId;
        this.major = major;
    }

    public String getStudentId() { return studentId; }
    public void setStudentId(String studentId) { this.studentId = studentId; }

    public String getMajor() { return major; }
    public void setMajor(String major) { this.major = major; }

    // 重写 introduce 方法,增加学生特有的信息
    @Override
    public void introduce() {
        super.introduce();   // 调用父类的方法
        System.out.println("我是学生,学号 " + studentId + ",专业 " + major);
    }

    public void study() {
        System.out.println(getName() + " 正在学习 " + major);
    }
}

// ========== 子类:Teacher ==========
public class Teacher extends Person {
    private String employeeId;
    private String title;   // 职称

    public Teacher(String name, int age, String phone, String employeeId, String title) {
        super(name, age, phone);
        this.employeeId = employeeId;
        this.title = title;
    }

    // Getter/Setter 省略...

    @Override
    public void introduce() {
        super.introduce();
        System.out.println("我是老师,工号 " + employeeId + ",职称 " + title);
    }

    public void teach() {
        System.out.println(getName() + " 正在授课");
    }
}

// ========== 主程序 ==========
public class Main {
    public static void main(String[] args) {
        Student s = new Student("张三", 20, "13800001111", "S001", "计算机科学");
        Teacher t = new Teacher("李老师", 35, "13800002222", "T001", "教授");

        System.out.println("=== 学生信息 ===");
        s.introduce();
        s.study();

        System.out.println("\n=== 教师信息 ===");
        t.introduce();
        t.teach();

        // 多态的体现:把子类对象当作父类来使用
        System.out.println("\n=== 多态演示 ===");
        Person p1 = new Student("王五", 22, "13800003333", "S002", "数学");
        p1.introduce();   // 动态绑定:调用的是 Student 的 introduce()
        // p1.study();     // 编译错误!Person 类型没有 study() 方法
    }
}

输出:

=== 学生信息 ===
我是 张三,今年 20 岁。
我是学生,学号 S001,专业 计算机科学
张三 正在学习 计算机科学

=== 教师信息 ===
我是 李老师,今年 35 岁。
我是老师,工号 T001,职称 教授
李老师 正在授课

=== 多态演示 ===
我是 王五,今年 22 岁。
我是学生,学号 S002,专业 数学

10. 今天的总结

今天我们学习了面向对象的第二大特性 —— 继承:

  • 继承:子类通过 extends 获得父类的属性和方法,实现代码复用。
  • super:访问父类的构造方法(super(...))或父类的成员(super.xxx)。
  • 方法重写(Override):子类重新定义父类的方法,实现个性化行为。记得加 @Override
  • 构造调用链:从父类到子类依次构造,层层递进。
  • 单继承:一个子类只能有一个直接父类,避免菱形继承问题。

继承让代码能够复用,让类与类之间形成层次结构,是面向对象中组织和扩展代码的关键工具。


动手试试

  1. 定义一个 Vehicle(交通工具)类,包含 brandspeed 属性和 move() 方法。再定义两个子类 CarBicycleCar 增加 engineType 属性和 honk() 方法,Bicycle 增加 hasBell 属性和 ringBell() 方法。各自重写 move() 方法,输出不同的移动方式。

  2. 在上题的基础上,写一个方法 testDrive(Vehicle v),接收 Vehicle 参数,调用 move() 方法。传入 CarBicycle 对象,观察多态的效果。

  3. 定义一个 Shape(图形)父类,包含 getArea() 方法(返回 0)。定义 CircleRectangleTriangle 子类,各自实现 getArea() 的计算方法。在 main 中创建一个 Shape 数组,放入不同图形,循环调用 getArea(),打印面积。

  4. (思考)如果你在 Person 类中把 name 设为 private,在 Student 中还能直接访问 name 吗?如果不能,Student 如何获取或修改自己的名字?结合 super 和 getter/setter 来回答。

继承的知识很多,但核心就一条:“子类是一种父类”。牢牢抓住这个“is-a”关系,遇到任何继承场景,先问自己:“这两个类之间是 is-a 关系吗?”如果不是,就不要用继承。

我们下一篇会讲多态——在继承的基础上,让同一个方法表现出不同的行为。它是面向对象的第三大特性,也是让代码真正“活”起来的关键。

我们下一篇见。😃

📌 获取本系列示例代码请访问 GitCode

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值