什么是继承?
在Java中,继承 是面向对象编程的四大基本特性(封装、继承、多态、抽象)之一,它允许一个类(称为子类 或 派生类)获取另一个类(称为父类 或 基类 或 超类)的属性(字段)和方法。
继承的核心思想是 “代码复用” 和 “建立类之间的层次关系”,通过继承,我们可以创建一个新类,该类不仅拥有自己的新特性,还自动拥有父类的所有特性,从而避免了重复编写相同的代码。
继承的语法
在Java中,使用 extends 关键字来声明一个类继承自另一个类。
// 父类 (基类 / 超类)
class ParentClass {
// 父类的属性和方法
public void parentMethod() {
System.out.println("这是父类的方法");
}
}
// 子类 (派生类)
class ChildClass extends ParentClass {
// 子类自己的属性和方法
public void childMethod() {
System.out.println("这是子类自己的方法");
}
}
关键点:
extends的英文意思是 “扩展”,这非常形象地描述了继承——子类在父类的基础上进行了扩展。- Java 不支持多继承,即一个类不能同时继承自多个父类,这主要是为了避免“菱形问题”(Diamond Problem),即当多个父类拥有相同的方法时,子类会不知道该继承哪一个,Java通过单继承和接口(
implements)来解决这个问题。 - Java支持多层继承,即一个类可以继承自另一个类,而这个类又可以再继承自第三个类,形成一条继承链。
class Animal {}
class Mammal extends Animal {} // Mammal 继承自 Animal
class Dog extends Mammal {} // Dog 继承自 Mammal,Dog 间接继承了 Animal
继承的特性与规则
理解继承的规则对于正确使用它至关重要。
a) 子类继承父类的什么?
子类会继承父类中非私有的成员(包括字段和方法)。
public成员:子类完全继承,可以直接访问。protected成员:子类继承,并且可以在子类内部直接访问。- 默认(包私有)成员:如果子类和父类在同一个包中,子类可以继承并访问;如果不在同一个包中,则不能访问。
private成员:不继承,父类的私有成员对子类是不可见的,子类无法直接访问父类的私有字段或方法。
示例:
class Father {
public String publicName = "Public Name";
protected String protectedName = "Protected Name";
String defaultName = "Default Name"; // 包私有
private String privateName = "Private Name"; // 私有
public void publicMethod() {
System.out.println("Father's public method");
}
protected void protectedMethod() {
System.out.println("Father's protected method");
}
void defaultMethod() {
System.out.println("Father's default method");
}
private void privateMethod() {
System.out.println("Father's private method");
}
}
class Son extends Father {
public void testInheritance() {
System.out.println(this.publicName); // 可以访问
System.out.println(this.protectedName); // 可以访问
System.out.println(this.defaultName); // 如果Son和Father在同一个包,可以访问
// System.out.println(this.privateName); // 编译错误!无法访问private成员
this.publicMethod(); // 可以调用
this.protectedMethod(); // 可以调用
this.defaultMethod(); // 如果在同一个包,可以调用
// this.privateMethod(); // 编译错误!无法调用private方法
}
}
b) 方法重写
这是继承中一个非常重要的概念,如果子类认为从父类继承来的某个方法不能满足自己的需求,可以在子类中提供一个具有相同方法名、相同参数列表、相同返回值类型(或其子类)的新实现,这就叫做方法重写。
- 目的:实现多态,让子类的行为更具体化。
- 语法:在子类中重新定义父类的方法。
@Override注解:强烈建议在重写方法上使用@Override注解,它不是必需的,但它可以帮助编译器检查你是否正确地重写了方法(比如方法名写错了),可以有效避免很多低级错误。
示例:
class Animal {
public void makeSound() {
System.out.println("动物发出声音");
}
}
class Dog extends Animal {
// 重写父类的 makeSound 方法
@Override
public void makeSound() {
System.out.println("汪汪汪!");
}
}
class Cat extends Animal {
// 重写父类的 makeSound 方法
@Override
public void makeSound() {
System.out.println("喵喵喵!");
}
}
public class Test {
public static void main(String[] args) {
Animal myDog = new Dog();
Animal myCat = new Cat();
myDog.makeSound(); // 输出: 汪汪汪!
myCat.makeSound(); // 输出: 喵喵喵!
}
}
c) super 关键字
super 关键字用于在子类中引用父类的成员(字段、方法或构造函数)。
-
访问父类被重写的方法:
class Dog extends Animal { @Override public void makeSound() { super.makeSound(); // 调用父类 Animal 的 makeSound 方法 System.out.println("汪汪汪!"); } } // 输出: // 动物发出声音 // 汪汪汪! -
访问父类的隐藏字段(如果子类定义了与父类同名的字段):
class Father { String name = "Father"; } class Son extends Father { String name = "Son"; public void printNames() { System.out.println(this.name); // 输出: Son System.out.println(super.name); // 输出: Father } } -
调用父类的构造函数: 这是非常重要的一点,子类在创建对象时,会隐式或显式地调用父类的构造函数,构造函数不能被继承。
- 隐式调用:如果子类构造函数的第一行没有使用
super(...),那么默认会调用父类的无参构造函数。 - 显式调用:如果子类构造函数的第一行使用了
super(...),则会调用父类对应的有参构造函数。 - 规则:
super(...)必须是子类构造函数中的第一条语句。
class Father { public Father() { System.out.println("Father's no-arg constructor"); } public Father(String name) { System.out.println("Father's constructor with name: " + name); } } class Son extends Father { public Son() { // super(); // 这是隐式调用的,可以省略不写 System.out.println("Son's no-arg constructor"); } public Son(String name) { super(name); // 显式调用父类的有参构造函数 System.out.println("Son's constructor with name: " + name); } } public class Test { public static void main(String[] args) { System.out.println("--- 创建 Son() 对象 ---"); Son son1 = new Son(); // 输出: // Father's no-arg constructor // Son's no-arg constructor System.out.println("\n--- 创建 Son(\"Tom\") 对象 ---"); Son son2 = new Son("Tom"); // 输出: // Father's constructor with name: Tom // Son's constructor with name: Tom } } - 隐式调用:如果子类构造函数的第一行没有使用
继承的访问权限和可见性
| 修饰符 | 本类 | 同包 | 不同包的子类 | 其他类 |
|---|---|---|---|---|
public |
✓ | ✓ | ✓ | ✓ |
protected |
✓ | ✓ | ✓ | ✗ |
| 默认 (无) | ✓ | ✓ | ✗ | ✗ |
private |
✓ | ✗ | ✗ | ✗ |
这个表格清晰地展示了,子类能直接访问父类的 public 和 protected 成员,但不能访问 private 成员。
Object 类:所有类的祖先
在Java中,所有类都直接或间接地继承自 java.lang.Object 类,如果你没有使用 extends 关键字来指定父类,那么你的类就会默认继承 Object。
这意味着:
- 任何Java对象都可以调用
Object类中的方法,toString(),equals(),hashCode()等。 - 这就是为什么你可以直接在任何对象上调用
.toString()的原因。
继承的优缺点
优点
- 代码复用:这是最主要的好处,子类可以复用父类的代码,减少了冗余。
- 建立类层次结构:使得代码结构清晰,逻辑关系明确。
Animal -> Mammal -> Dog的层次结构。 - 为多态奠定基础:继承是实现多态的前提,没有继承,就没有方法重写,也就没有多态。
缺点
- 破坏封装性:子类对父类有很强的依赖关系,如果父类的实现发生了改变(比如修改了方法签名),可能会影响到所有子类。
- 灵活性差:继承关系在编译时就确定了,是静态的,它非常僵硬,不够灵活,如果设计不当,会导致一个庞大而脆弱的继承体系。
- “is-a” 关系滥用:继承应该用于描述 “is-a” (是一个) 的关系。
Dog is an Animal,如果错误地用于 “has-a” (有一个) 的关系,Car has an Engine,使用继承会导致设计错误,此时应该使用组合。
Java的继承关系是一个强大但需要谨慎使用的特性,它通过 extends 关键字建立单层次的类结构,核心目的是实现代码复用和为多态提供支持,子类继承父类的非私有成员,并可以重写父类的方法来定义自己的行为。super 关键字是子类访问父类成员的桥梁,虽然继承带来了诸多好处,但也可能引入耦合度高、灵活性差等问题,因此在设计时需要严格遵循 “is-a” 原则,并考虑是否组合是更优的选择。
