Java面向对象编程:深入理解继承
面向对象编程(Object-Oriented Programming, OOP)的三大核心特性是封装、继承和多态。本文将深入探讨继承这一重要概念,它是Java中实现代码复用和构建层次化类结构的关键机制。
什么是继承?
继承是面向对象编程中的一个基本概念,它允许一个类(称为子类或派生类)获取另一个类(称为父类、超类或基类)的属性和方法。这种关系通常被描述为“is-a”(是一个)的关系。例如,Dog
is a Animal
,Car
is a Vehicle
。
通过继承,子类自动拥有了父类的非私有成员(字段和方法),并可以在此基础上添加自己特有的属性和行为,或者重写(Override)父类的方法以实现特定的功能。
为什么需要继承?
- 代码复用:子类可以直接使用父类的代码,减少冗余。
- 扩展性:轻松创建现有类的特殊版本,添加新功能。
- 多态的基础:继承是实现多态的前提,提高灵活性。(多态是另一个相关但更进阶的主题)
- 维护性:修改通用功能只需在父类进行,简化维护。
如何在Java中实现继承?
Java使用关键字 extends
来实现类之间的继承。
语法:
class SubclassName extends SuperclassName {
// 子类内容
}
示例:基本继承
在这个例子中,Dog
继承自 Animal
,可以直接使用 Animal
的 name
属性和 eat()
方法。
// 文件名: InheritanceDemo.java// 父类:Animal (非 public,可以在同一文件中)
class Animal {
String name; // 包访问权限(默认),子类在同一包中可以访问public void eat() {
System.out.println(name + " is eating.");
}
}// 子类:Dog,继承自 Animal
class Dog extends Animal {
// Dog 特有的方法
public void bark() {
// 可以直接访问从 Animal 继承来的 name 属性
System.out.println(name + " says: Woof! Woof!");
}
}// 主类:包含 main 方法
public class InheritanceDemo {
public static void main(String[] args) {
// 创建 Dog 对象
Dog myDog = new Dog();
myDog.name = "Buddy"; // 设置继承自 Animal 的属性myDog.eat(); // 调用继承自 Animal 的方法
myDog.bark(); // 调用 Dog 类自己的方法
}
}
输出:
Buddy is eating.
Buddy says: Woof! Woof!
继承中的细节
1. 继承的成员访问权限
子类对父类成员的访问权限取决于该成员的访问修饰符:
public
:子类可以访问。protected
:子类可以访问(无论是否在同一个包内)。同一包内的其他类也可以访问。- 默认(无修饰符,包访问权限):只有在同一个包内的子类可以访问。
private
:子类不能直接访问。私有成员被继承了(存在于子类对象中),但不可见。只能通过父类提供的public
或protected
方法(如getter/setter)间接操作。
示例:访问权限
// 文件名: AccessDemo.javaclass Parent {
public String publicVar = "I am public";
protected String protectedVar = "I am protected";
String defaultVar = "I am default (package-private)";
private String privateVar = "I am private";public void showPrivate() {
System.out.println("Accessing privateVar via public method: " + privateVar);
}
}class Child extends Parent {
public void testAccess() {
System.out.println("Accessing publicVar: " + publicVar); // OK
System.out.println("Accessing protectedVar: " + protectedVar); // OK
System.out.println("Accessing defaultVar: " + defaultVar); // OK (假设 Child 和 Parent 在同一包)
// System.out.println("Accessing privateVar: " + privateVar); // 编译错误!无法直接访问 private 成员// 间接访问 private 成员
super.showPrivate();
}
}public class AccessDemo {
public static void main(String[] args) {
Child c = new Child();
c.testAccess();
}
}
输出:
Accessing publicVar: I am public
Accessing protectedVar: I am protected
Accessing defaultVar: I am default (package-private)
Accessing privateVar via public method: I am private
2. 方法重写
子类可以提供一个与父类方法签名(方法名、参数列表)完全相同的方法,以实现自己的逻辑。
- 使用
@Override
注解是最佳实践,可以帮助编译器检查重写是否正确。 - 访问权限不能更低。
- 返回类型必须相同或是父类方法返回类型的子类型(协变返回类型,初学者阶段了解即可)。
示例:方法重写
// 文件名: OverrideDemo.javaclass Vehicle {
public void startEngine() {
System.out.println("Vehicle engine starts.");
}
}class ElectricCar extends Vehicle {
@Override // 明确表示重写
public void startEngine() {
System.out.println("Electric car engine starts silently."); // 子类的特定实现
}public void charge() {
System.out.println("Electric car is charging.");
}
}public class OverrideDemo {
public static void main(String[] args) {
Vehicle myVehicle = new Vehicle();
myVehicle.startEngine(); // 输出: Vehicle engine starts.ElectricCar myTesla = new ElectricCar();
myTesla.startEngine(); // 输出: Electric car engine starts silently. (调用重写后的方法)
myTesla.charge();
}
}
3. super
关键字
super
用于在子类中引用父类的成员。
super.methodName()
: 调用父类被重写的方法。super.fieldName
: 访问父类的属性(当子类有同名属性时尤其有用,但不推荐隐藏字段)。super()
或super(arguments)
: 调用父类的构造方法。必须是子类构造方法的第一条语句。
示例:使用 super
// 文件名: SuperDemo.javaclass Person {
String name;// 父类构造方法
public Person(String name) {
System.out.println("Person constructor executing.");
this.name = name;
}public void display() {
System.out.println("Name: " + name);
}
}class Employee extends Person {
String employeeId;// 子类构造方法
public Employee(String name, String employeeId) {
super(name); // 必须是第一句,调用父类 Person(String) 构造方法
System.out.println("Employee constructor executing.");
this.employeeId = employeeId;
}// 重写 display 方法
@Override
public void display() {
super.display(); // 调用父类的 display 方法来显示名字
System.out.println("Employee ID: " + employeeId); // 添加子类特有信息
}
}public class SuperDemo {
public static void main(String[] args) {
Employee emp = new Employee("Alice", "E123");
System.out.println("------");
emp.display();
}
}
输出:
Person constructor executing.
Employee constructor executing.
------
Name: Alice
Employee ID: E123
4. 构造方法的调用顺序
当创建一个子类的对象时,构造方法的调用链会从最顶层的父类开始,逐级向下执行,直到子类自己的构造方法。这个过程确保了对象的所有部分(从父类继承来的和子类自己定义的)都被正确初始化。
- 子类构造方法的第一行(无论是显式
super(...)
还是隐式super()
)会触发父类构造方法的调用。 - 这个调用会一直传递上去,直到
Object
类的构造方法。 - 然后,构造方法体按照从父到子的顺序依次执行完毕。
示例:构造方法调用顺序
// 文件名: ConstructorOrderDemo.javaclass Grandparent {
public Grandparent() {
System.out.println("Grandparent constructor executed.");
}
}class Parent extends Grandparent {
public Parent() {
// 隐式调用 super() -> Grandparent()
System.out.println("Parent constructor executed.");
}
}class Child extends Parent {
public Child() {
// 隐式调用 super() -> Parent()
System.out.println("Child constructor executed.");
}
}public class ConstructorOrderDemo {
public static void main(String[] args) {
System.out.println("Creating a Child object...");
Child myChild = new Child();
System.out.println("Child object created.");
}
}
输出:
Creating a Child object...
Grandparent constructor executed.
Parent constructor executed.
Child constructor executed.
Child object created.
这个输出清晰地展示了构造方法从最顶层父类(Grandparent
)开始,依次向下执行到子类(Child
)的顺序。
5. Java 的单继承性
Java 不允许一个类直接继承(extends
)多个类。这是为了避免多重继承可能带来的“菱形问题”(Diamond Problem),即当两个父类有同名方法时,子类不知道该继承哪一个实现,从而产生歧义。
非法示例(演示多重继承的错误):
// // 以下代码无法编译通过,演示 Java 不支持多重类继承
// class Father {
// void work() { System.out.println("Father working"); }
// }
// class Mother {
// void work() { System.out.println("Mother working"); }
// }
//
// // 错误: Class cannot extend multiple classes
// class Kid extends Father, Mother {
// // 如果允许多重继承,调用 work() 时会有歧义
// }
Java 只允许单继承,即一个子类只能有一个直接父类。这使得继承关系更清晰、简单。如果需要一个类表现出多种类型的特征,通常会用到更高级的概念(如接口或组合),但对于继承本身,请记住“一个子类只有一个直接父类”。
6. Object
类:所有类的根
所有 Java 类都隐式或显式地继承自 java.lang.Object
类。即使你没有写 extends Object
,编译器也会自动添加。这意味着所有对象都天然拥有 Object
类的方法,如 toString()
, equals()
, hashCode()
等。
示例:隐式继承 Object 和重写 toString
// 文件名: ObjectRootDemo.javaclass MySimpleClass { // 隐式继承自 Object
int value;public MySimpleClass(int value) {
this.value = value;
}// 重写 Object 类的 toString 方法
@Override
public String toString() {
// 默认的 toString() 返回类似 "MySimpleClass@hashcode"
// 我们提供一个更有意义的表示
return "MySimpleClass[value=" + value + "]";
}
}public class ObjectRootDemo {
public static void main(String[] args) {
MySimpleClass obj = new MySimpleClass(10);
// 当打印对象时,会自动调用它的 toString() 方法
System.out.println(obj);
// 输出: MySimpleClass[value=10]// 如果不重写 toString(),输出可能是类似 MySimpleClass@15db9742
}
}
继承的优缺点
优点:
- 代码复用
- 易于扩展和维护
- 实现多态的基础
缺点:
- 紧耦合:子类与父类紧密关联,父类的改变可能影响所有子类。
- 脆弱的基类问题:子类可能依赖父类的内部实现细节,父类实现的改变(即使方法签名不变)可能意外破坏子类的功能。
- 层次复杂性:过深的继承树会使代码难以理解和维护。
继承 vs. 组合
面向对象设计中有一个重要的原则:“组合优于继承”。
- 继承 (is-a): 代表“是一个”的关系,如
Dog
is anAnimal
。是一种强烈的类间关系。 - 组合 (has-a): 代表“有一个”的关系,如
Car
has anEngine
。一个类包含另一个类的对象作为其成员。
组合通常比继承更灵活,耦合度更低。当一个类需要另一个类的功能,但它们之间不是严格的“is-a”关系时,优先考虑使用组合。
示例(组合):
// 文件名: CompositionDemo.java// 代表引擎的类
class Engine {
public void start() {
System.out.println("Engine starting.");
}
public void stop() {
System.out.println("Engine stopping.");
}
}// Car 类包含一个 Engine 对象 (has-a)
class Car {
private String model;
private Engine engine; // Car "has an" Enginepublic Car(String model) {
this.model = model;
this.engine = new Engine(); // 在 Car 创建时创建 Engine
}// Car 的启动方法委托给 Engine 对象来完成
public void startCar() {
System.out.println(model + " is starting...");
engine.start();
}// Car 的停止方法也委托给 Engine 对象
public void stopCar() {
System.out.println(model + " is stopping...");
engine.stop();
}
}public class CompositionDemo {
public static void main(String[] args) {
Car myCar = new Car("Sedan");
myCar.startCar();
myCar.stopCar();
}
}
输出:
Sedan is starting...
Engine starting.
Sedan is stopping...
Engine stopping.
总结
继承是 Java OOP 的重要特性,通过 extends
关键字实现代码复用和类层次结构的构建。理解继承中的访问权限、方法重写、super
关键字的使用、构造方法的调用链、Java 的单继承限制以及所有类都源自 Object
类是掌握继承的基础。同时,也要认识到继承带来的耦合性问题,并在设计时权衡继承与组合,遵循“组合优于继承”的原则,以构建更健壮、灵活的应用。
练习题
练习 1:基础继承
- 创建一个名为
Shape
的基类,包含一个color
(String) 属性。 - 添加一个
displayColor()
方法,打印形状的颜色。 - 创建一个名为
Circle
的子类,继承自Shape
。 - 为
Circle
添加一个属性radius
(double)。 - 添加一个
calculateArea()
方法,计算并打印圆的面积 (Area = π * radius * radius, 使用Math.PI
)。 - 创建一个
public
类GeometryTest
,在main
方法中创建一个Circle
对象,设置其颜色和半径,并调用displayColor()
和calculateArea()
方法。
练习 2:构造方法和 super
- 为
Shape
类添加一个构造方法,接收color
作为参数并初始化它。 - 修改
Circle
类,添加一个构造方法,接收color
和radius
。确保此构造方法能正确调用Shape
类的构造方法来初始化color
。 - 在
GeometryTest
的main
方法中,使用新的构造方法创建Circle
对象。
练习 3:概念题
- Java 支持一个类同时
extends
多个父类吗?简单说明为什么。
答案
练习 1 和 2 的答案代码:
// 文件名: GeometryTest.java// 基类:Shape
class Shape {
String color;// 练习 2: 添加构造方法
public Shape(String color) {
System.out.println("Shape constructor called.");
this.color = color;
}public void displayColor() {
System.out.println("Color: " + color);
}
}// 子类:Circle
class Circle extends Shape {
double radius;// 练习 2: 添加构造方法并调用 super()
public Circle(String color, double radius) {
super(color); // 调用父类 Shape 的构造方法
System.out.println("Circle constructor called.");
this.radius = radius;
}// 练习 1: 添加计算面积方法
public void calculateArea() {
double area = Math.PI * radius * radius;
System.out.println("Radius: " + radius);
System.out.println("Area: " + area);
}
}// 主测试类
public class GeometryTest {
public static void main(String[] args) {
// 使用练习 2 的构造方法创建对象
Circle myCircle = new Circle("Red", 5.0);System.out.println("\nCircle Details:");
myCircle.displayColor(); // 调用继承自 Shape 的方法
myCircle.calculateArea(); // 调用 Circle 自己的方法
}
}
预期输出:
Shape constructor called.
Circle constructor called.Circle Details:
Color: Red
Radius: 5.0
Area: 78.53981633974483
练习 3 的答案:
- Java 不支持一个类同时
extends
多个父类(即不支持多重类继承)。主要原因是为了避免菱形问题,这会导致在继承体系中出现方法调用的歧义,使得程序难以理解和维护。Java 通过强制单继承简化了继承模型。