关于 Java 继承的详细教学,涵盖概念、语法、重点、易错点、考点及实操示例。

Java 继承详解

继承 (Inheritance) 是面向对象编程 (OOP) 的四大基本特性之一(封装、继承、多态、抽象)。它允许我们基于已有的类创建新的类。新的类(称为子类派生类)继承了已有类(称为父类基类超类)的属性和方法,并且可以添加自己新的属性和方法,或者对父类的方法进行重新定义(覆盖)。

一、 为什么使用继承?

  1. 代码重用 (Code Reusability): 避免重复编写父类中已有的代码,提高开发效率。
  2. 可维护性 (Maintainability): 当需要修改父类的功能时,所有继承它的子类都会自动获得更新(除非子类覆盖了该方法)。
  3. 建立类层次结构 (Class Hierarchy): 模拟现实世界中“是一个(Is-A)”的关系(如:狗是一个动物,轿车是一辆车)。
  4. 实现多态 (Polymorphism): 继承是实现运行时多态的基础。

二、 继承的语法

class SubclassName extends SuperclassName {
    // 子类特有的字段和方法
    // 可以覆盖父类的方法
}

  • extends 是 Java 中用于实现继承的关键字。
  • SubclassName 是你定义的子类名称。
  • SuperclassName 是父类的名称。
  • Java 是单继承,一个类只能直接继承自一个父类。但可以通过多层继承形成继承链(树形结构)。
  • Java 中所有类都直接或间接继承自 java.lang.Object 类(根类)。

三、 核心概念与重点

  1. 访问修饰符 (Access Modifiers) 与继承:

    • public: 任何地方都可以访问。
    • protected: 同一包内或不同包的子类中可以访问。
    • 默认 (default): 同一包内可以访问。子类在不同包则不能访问。
    • private: 仅在定义它的类内部访问。子类不能直接访问父类的 private 成员!
    • 重点: 子类继承父类时,只能访问父类的 publicprotected 成员(以及同一包下的 默认成员)。private 成员对子类不可见。
  2. 方法覆盖/重写 (Method Overriding):

    • 概念: 子类可以提供一个与父类方法签名(方法名 + 参数列表)完全相同的方法实现。当通过子类对象调用该方法时,执行的是子类覆盖后的方法,而不是父类的方法。
    • 规则:
      • 方法名和参数列表必须与父类方法完全相同。
      • 返回类型可以是父类方法返回类型的子类(协变返回类型,Java 5+ 支持)。
      • 访问权限不能比父类方法更严格(可以相同或更宽松)。
      • 不能覆盖 private 方法(因为不可见)。
      • 不能覆盖 final 方法。
      • 不能覆盖 static 方法(静态方法属于类,与对象无关,不存在覆盖,是隐藏)。
      • 可以覆盖 abstract 方法(必须提供实现)。
      • 使用 @Override 注解(可选但强烈推荐): 编译器会检查该方法是否确实覆盖了父类方法,避免因拼写错误等导致意外的重载而非重写。
    • 目的: 实现子类特有的行为,是实现多态的关键。
  3. super 关键字:

    • 作用: 用于在子类中引用父类的成员(字段、方法、构造方法)。
    • 调用父类方法: super.methodName();。常用于在子类覆盖的方法中,先调用父类的方法完成基础功能,再添加子类特有的功能。
    • 调用父类构造方法: super(...);必须放在子类构造方法的第一行。用于初始化从父类继承来的部分。如果父类有无参构造方法,编译器会自动在子类构造方法的第一行插入 super();
  4. 构造方法与继承:

    • 创建子类对象时,必须先调用父类的构造方法来初始化父类继承下来的部分。
    • 子类构造方法的第一行语句,如果没有显式地调用父类其他构造方法 (super(...);),编译器会自动插入对父类无参构造方法 (super();) 的调用。
    • 如果父类没有无参构造方法(只有带参构造方法),则子类构造方法中必须显式地用 super(...); 调用父类的某个带参构造方法,否则编译错误。

四、 易错点与考点

  1. private 成员的“不可见性”: 子类对象拥有父类的 private 字段(内存中有),但不能在子类代码中直接通过名字访问它们。只能通过父类提供的 publicprotectedgetter/setter 方法来间接访问。
  2. super 调用构造方法的位置: super(...); 必须位于子类构造方法的第一行,否则编译错误。
  3. 父类无无参构造方法: 如果父类定义了带参构造方法但没有定义无参构造方法,子类构造方法必须显式调用父类的带参构造方法 (super(...);),否则编译失败。
  4. 方法覆盖 vs 方法重载 (Overloading):
    • 覆盖 (Override): 发生在父子类之间,方法签名相同。
    • 重载 (Overload): 发生在同一个类中,方法名相同但参数列表不同(个数、类型、顺序)。
    • 考点:区分两者,避免混淆。
  5. final 与继承:
    • final 类:不能被继承。
    • final 方法:不能被覆盖。
    • final 变量:常量,值不能被修改。
  6. static 方法与继承: 静态方法属于类,不能被覆盖。如果子类定义了与父类签名相同的静态方法,这叫方法隐藏 (Method Hiding),不是覆盖。调用哪个方法取决于引用变量的类型,而不是实际对象的类型。
  7. 继承链与 Object 类: 理解所有类都继承自 Object,熟悉 Object 类的重要方法(如 toString(), equals(), hashCode()),并理解在子类中覆盖这些方法的必要性(尤其是集合类使用时)。

五、 实操示例

// 父类:交通工具
class Vehicle {
    // protected 允许子类直接访问
    protected String brand;

    // 父类带参构造方法 (没有无参构造方法)
    public Vehicle(String brand) {
        this.brand = brand;
        System.out.println("Vehicle constructor called. Brand: " + brand);
    }

    public void drive() {
        System.out.println("Vehicle is driving.");
    }

    // 可以被覆盖的方法
    public void honk() {
        System.out.println("Vehicle honks!");
    }
}

// 子类:汽车,继承自 Vehicle
class Car extends Vehicle {
    private int numDoors;

    // 子类构造方法,必须调用父类构造方法
    public Car(String brand, int numDoors) {
        super(brand); // 调用父类带参构造方法,必须放在第一行
        this.numDoors = numDoors;
        System.out.println("Car constructor called. Doors: " + numDoors);
    }

    // 覆盖父类的 honk 方法
    @Override
    public void honk() {
        super.honk(); // 调用父类的 honk 方法
        System.out.println("Car honks loudly: Beep Beep!");
    }

    // 子类特有的方法
    public void openSunroof() {
        System.out.println("Car opens sunroof.");
    }
}

// 测试类
public class InheritanceDemo {
    public static void main(String[] args) {
        // 创建 Car 对象
        Car myCar = new Car("Toyota", 4);
        myCar.drive(); // 调用继承自父类的方法
        myCar.honk();  // 调用子类覆盖后的方法 (输出父类+子类的信息)
        myCar.openSunroof(); // 调用子类特有的方法

        // 多态:父类引用指向子类对象
        Vehicle vehicle = new Car("Honda", 2);
        vehicle.drive(); // 调用的是 Vehicle 的 drive(),因为 Car 没有覆盖它
        vehicle.honk();  // 调用的是 Car 的 honk()!因为运行时实际对象是 Car,且 honk 被覆盖了
        // vehicle.openSunroof(); // 编译错误!Vehicle 类型没有 openSunroof 方法
    }
}

输出示例:

Vehicle constructor called. Brand: Toyota
Car constructor called. Doors: 4
Vehicle is driving.
Vehicle honks!
Car honks loudly: Beep Beep!
Car opens sunroof.
Vehicle constructor called. Brand: Honda
Car constructor called. Doors: 2
Vehicle is driving.
Vehicle honks!
Car honks loudly: Beep Beep!

示例解析:

  1. 构造方法链: 创建 Car 对象时,首先调用父类 Vehicle 的构造方法 (super("Toyota")),然后调用子类 Car 的构造方法。
  2. 方法继承: Car 对象可以直接调用父类 Vehicledrive() 方法。
  3. 方法覆盖: Car 覆盖了 Vehiclehonk() 方法。在覆盖的方法中,使用 super.honk() 先调用了父类的方法,然后添加了子类特有的行为。
  4. 特有方法: Car 定义了父类没有的 openSunroof() 方法。
  5. 多态: Vehicle vehicle = new Car("Honda", 2); 体现了多态。vehicle 引用是 Vehicle 类型,但指向的实际对象是 Car 类型。
    • vehicle.drive(): 因为 Car 没有覆盖 drive(),所以调用的是 Vehicledrive()
    • vehicle.honk(): 因为 Car 覆盖了 honk(),所以运行时实际调用的是 Carhonk() 方法(多态的核心)。
    • vehicle.openSunroof(): 编译错误,因为编译器只认 Vehicle 类型的方法列表,里面没有 openSunroof()。如果想调用,需要向下转型 ((Car) vehicle).openSunroof(); (存在 ClassCastException 风险)。

六、 总结与注意事项

  • 理解“是一个”关系: 只有当子类和父类之间逻辑上存在“是一个”的关系时,才使用继承(例如 Car is a Vehicle)。不要为了复用代码而滥用继承。
  • 优先使用组合而非继承: 如果两个类之间是“有一个”的关系(例如 Car has an Engine),应使用组合(在一个类中包含另一个类的实例)而不是继承。
  • 谨慎使用继承层次: 过深的继承层次会增加系统复杂性,降低可维护性。
  • @Override 是好习惯: 总是使用 @Override 注解来标记覆盖的方法,提高代码可读性和安全性。
  • 理解多态: 继承是实现多态的基础,多态极大地提高了代码的灵活性和可扩展性。

更多推荐