什么是继承?
继承是 OOP(面向对象编程)的一个核心特性。它是指从一个已有的类(称为父类或基类)派生出新的类(称为子类或派生类)。子类会继承父类的数据属性和行为方法,并可以在此基础上扩展新的能力。
基类一旦被继承,就构成了代码架构的基础,修改会就会产生连锁反应。因此,在设计时务必全面考量,力求避免对基类的后续改动。
本文将系统地介绍 Java 继承的使用理由、is-a 关系、Protected 关键字,方法重写(Override)、Super 关键字、继承的优缺点并提供相应的示例代码以帮助理解。
is-a 关系
继承用于表示类之间的严格is-a
关系,只有在两个类确实符合这种关系时,才应使用继承。
✅ 例如,以下情况适合使用继承:
- 橙子是一种水果
- 开创者是一个网站
- 外科医生也是医生
✅ 而以下情况则不适合使用继承,因为它们描述的是 has-a
(拥有)关系,而非is-a
关系:
- 汽车拥有发动机
- 房子拥有窗户
✅ 以下是使用继承关系实现的实例:
class Animal {
public void eat() {
System.out.println("I can eat food.");
}
public void sleep() {
System.out.println("I can sleep.");
}
}
class Cat extends Animal {
public void catchMice() {
System.out.println("I can catch mice.");
}
}
class Main {
public static void main(String[] args) {
Cat tabbyCat = new Cat();
tabbyCat.eat();
tabbyCat.sleep();
tabbyCat.catchMice();
}
执行上面的程序后,运行结果:
I can eat food.
I can sleep.
I can catch mice.
在上面的代码中,我们从父类 Animal 继承了 Cat 子类。Cat 类从 Animal 类继承了eat()
和sleep()
方法。
Protected 关键字
✅ 在前面的章节中,我们已经初步了解了在 Java 中访问控制修饰符基本概念与作用:
private
:仅允许在定义该成员的类内部访问。该修饰符提供了最严格的封装保护,同时也限制了代码的可重用性和扩展性。public
:可以被任何其他类直接访问。虽然提供了最高的灵活性,但过度使用会破坏封装性。protected
:为继承体系设计,允许同一包及所有子类访问,平衡封装性与扩展性。
您可以将方法和字段设置为 protected
。受保护的成员可以在定义它的类、同一包中的其他类以及任何子类中被访问。
✅ 下面是一个清晰的 Java 访问修饰符作用域的对照表:
修饰符 | 本类 | 同包 | 子类 | 全局 |
---|---|---|---|---|
public |
可访问 | 可访问 | 可访问 | 可访问 |
private |
可访问 | 不可访问 | 不可访问 | 不可访问 |
protected |
可访问 | 可访问 | 可访问 | 不可访问 |
方法重写(Override)
在继承机制中,子类可以复用父类的方法,省去了重复编写代码的麻烦,大大提升了代码的复用效率。不过,并不是每一个从父类继承而来的方法,都完全契合子类实际的业务场景。
这时候,我们就需要对这些方法进行重写——既保留原有逻辑的精华,又融入子类自己的特色。通过这种方式,子类既延续了父类的能力,又拥有了改变和拓展的自由度,在保持一致接口的同时,实现属于自己的行为逻辑。
✅ 下面是一个方法重写的示例:
class Animal {
protected String type = "animal";
public void eat() {
System.out.println("I can eat food.");
}
public void sleep() {
System.out.println("I can sleep.");
}
}
class Cat extends Animal {
@Override
public void eat() {
System.out.println("I love eating cat food.");
}
public void catchMice() {
System.out.println("I can catch mice.");
}
}
class Main {
public static void main(String[] args) {
Cat tabbyCat = new Cat();
tabbyCat.eat();
tabbyCat.sleep();
tabbyCat.catchMice();
}
执行上面的程序后,运行结果:
I love eating cat food.
I can sleep.
I can catch mice.
在这里,eat()
方法同时存在于超类Animal
和子类Cat
中。通过重写eat()
方法,子类Cat
可以根据自身的需求提供特定的实现,从而覆盖从超类继承而来的默认行为。这种机制使得子类能够在不改变方法签名的情况下,定制属于自己的功能逻辑,充分体现了面向对象编程中Java 多态的特性。
在上面的示例中,我们使用了@Override
注解来明确告诉编译器当前方法正在重写父类的方法。虽然这个注解不是强制要求的,但使用它是一个良好的编程习惯。在接下来的教程中,我们将深入探讨方法重写的细节和使用场景。
Super 关键字
super
关键字在 Java 中主要用于在子类中访问和调用父类的成员(包括方法、构造方法和属性)。
✅ 其主要用途体现在以下几个方面:
- 调用父类构造方法:必须是子类构造方法中的第一条语句。
- 调用父类方法:用于调用在子类中被重写(Override) 的父类方法。
- 访问父类属性:用于访问在子类中被隐藏(Hide)(即同名)的父类字段。
1. 调用父类的构造方法
在子类的构造方法中,使用super()
来调用父类的构造方法,必须放在子类构造方法的第一行。
class Animal {
String name;
Animal(String name) {
this.name = name;
}
}
class Cat extends Animal {
int age;
Cat(String name, int age) {
super(name); // 调用父类构造方法
this.age = age;
}
}
2. 调用父类被重写的方法
当子类重写了父类的方法后,如果仍需调用父类的原始方法,可以使用 super.方法名()
。
class Animal {
public void eat() {
System.out.println("Animal is eating.");
}
}
class Cat extends Animal {
@Override
public void eat() {
super.eat(); // 先调用父类的eat方法
System.out.println("Cat is eating fish.");
}
}
执行上面的程序后,运行结果:
Animal is eating.
Cat is eating fish.
3. 访问父类的属性(不常用)
当子类和父类有同名字段时,可以使用super.字段名
来明确访问父类的属性(但通常建议避免字段重名)。
class Animal {
String sound = "Animal sound";
}
class Cat extends Animal {
String sound = "Meow";
void printSounds() {
System.out.println(super.sound); // 访问父类的sound
System.out.println(this.sound); // 访问子类的sound
}
}
执行上面的程序后,运行结果:
Animal is eating.
Cat is eating fish.
注意:在调用构造函数与使用super
方法时存在一些关键区别。要了解更多信息,请访问 Java super 关键字。
继承的优缺点
1. 继承的优点
- 子类可以继承父类的字段和方法,无需重新编写相同的代码。这大大减少了代码冗余,提高了开发效率。
- 子类可以根据自身需要,重写(Override)父类的方法,提供特定于自己的实现。还允许 “一个接口,多种实现”。
- 继承允许我们创建清晰、逻辑分明的类层次结构,使代码模型更易于理解和维护。
- 父类引用可以指向子类对象。这意味着你可以编写更通用、更灵活的代码。
2. 继承的缺点
- 不支持多重继承(一个类不能同时继承多个父类)。
- 父类实现的任何改变(如修改方法签名、字段名)都可能 “破坏” 子类的功能。
- 过深的继承层次(例如:A -> B -> C -> D -> E)会使代码难以理解和调试。
- 继承会暴露父类的实现细节给子类(通过
protected
成员),这就违反了封装原则,是一种糟糕的设计。 - 因为继承会导致代码异常混乱, 99.99% 的人都用不好继承,继承都逐渐被替代为接口。
因此,在使用继承时需要权衡其优缺点,并根据具体问题选择合适地解决方案。
反馈提交成功
感谢您的反馈,我们将尽快处理您的反馈