一、继承解决什么问题
狗和猫都有名字、年龄,也都会吃饭、睡觉。如果分别在 Dog 和 Cat 中编写这些成员,就会出现重复代码。把共同成员放入 Animal,再让狗和猫继承它,就能复用这些代码。
继承表示“是一个”(is-a)的关系:狗是一种动物,因此 Dog 可以继承 Animal。
class 子类 extends 父类 { // 子类新增的成员 } - 父类:也叫基类、超类,提供共同的属性和行为。
- 子类:也叫派生类,可以复用父类中允许继承的成员,并增加或调整自己的行为。
下面的完整示例可保存为 Test.java;后续完整示例也分别独立运行。
class Animal { String name; int age; public void eat() { System.out.println(name + "正在吃饭"); } public void sleep() { System.out.println(name + "正在睡觉"); } } class Dog extends Animal { public void bark() { System.out.println(name + "汪汪叫"); } } class Cat extends Animal { public void mew() { System.out.println(name + "喵喵叫"); } } public class Test { public static void main(String[] args) { Dog dog = new Dog(); dog.name = "小黑"; dog.age = 2; dog.eat(); dog.sleep(); dog.bark(); } } 输出:
小黑正在吃饭 小黑正在睡觉 小黑汪汪叫 Dog 没有重复声明 name、age、eat()、sleep(),仍然能使用它们,这就是代码复用。
子类通常会增加特有成员或调整行为,但语法并不强制子类必须新增成员。继承也是后续学习多态的基础,本章先掌握成员复用和访问规则。
二、先看权限:父类的成员能不能访问
有继承关系,不代表能直接访问父类的所有成员。 判断成员访问是否合法,必须先看权限。
| 访问位置 | private | 不写修饰符(包访问权限) | protected | public |
|---|---|---|---|---|
| 声明该成员的类内部 | 可以 | 可以 | 可以 | 可以 |
| 同一个包中的其他类 | 不可以 | 可以 | 可以 | 可以 |
| 不同包中的子类内部 | 不可以 | 不可以 | 可以,但有限制 | 可以 |
| 不同包中的非子类 | 不可以 | 不可以 | 不可以 | 可以 |
“包访问权限”就是不写访问修饰符,不是写一个 default 关键字。
2.1 private:子类不能直接访问
class Animal { private String name = "小黑"; public String getName() { return name; } } class Dog extends Animal { public void show() { // System.out.println(name); // 错误:不能直接访问父类的 private 字段 // System.out.println(super.name); // 错误:super 不能绕过权限 System.out.println(getName()); // 正确:通过父类公开的方法读取 } } 父类的 private 成员不被子类继承,也不能由子类直接访问;但子类对象仍然具有由父类管理的这部分状态,父类自己的方法可以使用它。
2.2 protected:重点看跨包情况
同包时,protected 成员可以被包内其他类访问,不要求有继承关系。
跨包时,需要同时考虑访问代码写在哪里和通过什么类型的引用访问。对于父类的 protected 实例成员:
- 访问代码必须位于子类内部。
- 在该子类内部,可以通过
this、super,或该子类类型及其子类型的引用访问。 - 不能通过父类类型的引用,或另一个“兄弟子类”类型的引用访问。
父类放在 demo1 包:
// demo1/Base.java package demo1; public class Base { protected int value = 100; } 两个子类放在另一个包:
// demo2/Derived.java package demo2; import demo1.Base; class Other extends Base { } public class Derived extends Base { public void show() { System.out.println(this.value); // 正确 System.out.println(super.value); // 正确 Derived d = new Derived(); System.out.println(d.value); // 正确:通过 Derived 类型的引用访问 Base b = new Base(); // System.out.println(b.value); // 错误:通过 Base 类型的引用访问 Other o = new Other(); // System.out.println(o.value); // 错误:Other 是兄弟子类 } } 虽然 Derived 和 Other 在同一个包,但 value 声明在另一个包的 Base 中,所以这里仍受跨包访问规则限制。
选择权限时,只开放必要的范围:内部实现尽量隐藏,需要给子类使用的成员再考虑 protected,需要对外公开的成员才用 public。
三、再看名字:到底访问哪个成员
以下讨论以成员有访问权限为前提。
3.1 成员变量:同名时优先使用子类的
在子类实例方法中,直接写变量名时,先看局部变量或参数,再看子类字段,最后沿父类查找。父子类存在同名字段时,两个字段都存在,子类字段会隐藏父类字段。
class Base { int value = 10; int count = 30; } class Derived extends Base { int value = 20; public void show(int value) { System.out.println(value); // 参数 System.out.println(this.value); // 子类字段 System.out.println(super.value); // 父类字段 System.out.println(count); // 子类没有,使用父类字段 } } public class Test { public static void main(String[] args) { Derived d = new Derived(); d.show(100); } } 输出:
100 20 10 30 注意:
-
this.value不会选中同名参数;它访问当前对象的字段。 -
super.value明确访问父类字段,不会改成访问子类的同名字段。 - 字段同名即会发生隐藏,不要求类型相同。实际编写代码时,尽量避免父子类字段同名。
- 当前作用域和继承链中都找不到对应成员时,编译报错。
3.2 成员方法:不能只看方法名
| 情况 | 如何理解 |
|---|---|
| 子类没有对应方法,父类有可访问的方法 | 可以使用父类的方法 |
| 父子类方法同名,但参数列表不同 | 可以构成重载,按调用时的参数选择 |
| 子类重新实现父类中可重写的同签名实例方法 | 构成重写,可以用 super.方法名(...) 调用父类实现 |
重写先理解为:子类为父类已有的某个实例方法提供自己的实现。这里先用返回类型也相同的例子,具体规则留到后续学习。
class Base { public void show() { System.out.println("Base.show()"); } public void work() { System.out.println("Base.work()"); } } class Derived extends Base { public void show(int n) { System.out.println("Derived.show(int):" + n); } public void work() { System.out.println("Derived.work()"); } public void test() { show(); // 调用父类的无参方法 show(5); // 调用子类的有参方法 work(); // 调用子类重写后的实现 super.work(); // 明确调用父类实现 } } public class Test { public static void main(String[] args) { new Derived().test(); } } 输出:
Base.show() Derived.show(int):5 Derived.work() Base.work() show() 和 show(int) 是重载;两个 work() 是重写。不要把方法调用简单理解成“只要子类有同名方法,就一定调用子类的”。
3.3 super 的作用
super 用于在子类中明确访问父类成员,常见形式有:
super.字段名 super.方法名(参数) super(参数) // 调用父类构造方法,下一节说明 this 是当前对象的引用;super 是访问父类成员的特殊语法,不是另一个独立父类对象的引用。二者都不能在 static 方法中使用,super 也不能突破访问权限。
四、创建子类对象:先初始化父类部分
一个子类对象既有父类定义的实例状态,也有子类自己定义的实例状态。因此,创建子类对象时,需要先通过父类构造方法初始化父类部分,再继续初始化子类部分。
构造方法不会被继承。 子类需要自己的构造方法,并在构造过程中调用父类构造方法。
4.1 用 super(...) 选择父类构造方法
class Animal { String name; public Animal(String name) { this.name = name; System.out.println("Animal 构造:" + name); } } class Dog extends Animal { int age; public Dog(String name, int age) { super(name); this.age = age; System.out.println("Dog 构造:" + age); } } public class Test { public static void main(String[] args) { Dog dog = new Dog("小黑", 2); } } 输出:
Animal 构造:小黑 Dog 构造:2 执行过程:
-
new Dog("小黑", 2)进入Dog的构造过程。 -
super(name)调用Animal(String name),完成父类部分的初始化。 - 返回后继续执行
Dog构造方法中剩余的语句。
整个过程创建的是一个 Dog 对象,不是先创建独立的 Animal 对象,再创建一个 Dog 对象。
4.2 没写 super(...),为什么有时可以、有时报错
如果构造方法没有显式调用 this(...) 或 super(...),编译器会隐式添加 super()。
class Base { public Base(int n) { } } class Derived extends Base { public Derived() { // 隐式调用 super(),但 Base 没有无参构造方法,因此编译失败 } } 上面的错误不是“子类没有构造方法”,而是默认调用的父类无参构造方法不存在。
解决方法是显式调用已有的父类构造方法,例如在第一条语句写 super(10);;也可以根据设计需要,为父类提供可访问的无参构造方法。
这里需要分清两个规则:
- 一个类完全没有声明构造方法时,编译器才会提供默认的无参构造方法;已经写了有参构造,就不会再自动补无参构造。
- 隐式
super()能否成功,取决于父类是否有可访问的无参构造方法,与父类是否还有其他有参构造无关。
4.3 this(...) 与 super(...) 的关系
| 写法 | 含义 |
|---|---|
this.字段 / this.方法(...) | 通过当前对象访问成员,本类没有时也可能使用继承的成员 |
super.字段 / super.方法(...) | 明确访问父类成员或方法实现 |
this(...) | 调用本类的另一个构造方法 |
super(...) | 调用父类构造方法 |
按照本章采用的构造方法写法,this(...) 或 super(...) 放在构造方法的第一条语句,二者不能同时写在同一个构造方法中。
class Base { public Base(int n) { System.out.println("Base:" + n); } } class Derived extends Base { public Derived() { this(10); System.out.println("Derived 无参构造"); } public Derived(int n) { super(n); System.out.println("Derived 有参构造"); } } public class Test { public static void main(String[] args) { new Derived(); } } 输出:
Base:10 Derived 有参构造 Derived 无参构造 调用链是 Derived() → Derived(10) → Base(10),然后逐层返回。
因此,不是每个子类构造方法中都直接出现 super(...);通过 this(...) 调用本类其他构造方法后,最终仍会调用父类构造方法。
“不能同时出现”指的是构造调用 this(...) 与 super(...)。super(...) 后面当然可以继续写 this.age = age;。
五、有继承时,代码块按什么顺序执行
静态代码块属于类,在类初始化时执行一次;实例代码块属于对象,每次创建对象都会执行。
当父子类都尚未初始化,第一次创建子类对象时,顺序是:
父类静态代码块 ↓ 子类静态代码块 ↓ 父类实例代码块 ↓ 父类构造方法体 ↓ 子类实例代码块 ↓ 子类构造方法体中 super(...) 后的部分 class Parent { static { System.out.println("父类静态块"); } { System.out.println("父类实例块"); } public Parent() { System.out.println("父类构造"); } } class Child extends Parent { static { System.out.println("子类静态块"); } { System.out.println("子类实例块"); } public Child() { System.out.println("子类构造"); } } public class Test { public static void main(String[] args) { new Child(); System.out.println("--- 再创建一个对象 ---"); new Child(); } } 输出:
父类静态块 子类静态块 父类实例块 父类构造 子类实例块 子类构造 --- 再创建一个对象 --- 父类实例块 父类构造 子类实例块 子类构造 记忆重点:静态先完成,实例先父后子;静态只执行一次,实例每次创建都执行。
不要误记成“先执行所有实例代码块,再执行所有构造方法”:父类的实例初始化和构造先完成,才轮到子类的实例初始化和构造。
六、Java 支持哪些继承关系
Java 的类支持:
- 单继承:一个类只有一个直接父类。
- 多层继承:
C继承B,B继承A,C也就间接继承了A。 - 多个类继承同一个父类:例如
Dog和Cat都继承Animal。
class A { } class B extends A { } class C extends B { } // 多层继承 class D extends A { } // B 和 D 有同一个直接父类 // class E extends B, D { } // 错误:一个类不能同时继承两个类 多层继承不等于多继承:前者是一条逐层延伸的继承链,后者是一个类同时拥有多个直接父类,Java 的类不支持后者。
除 Object 本身外,Java 的类都直接或间接继承自 java.lang.Object;这里知道它是类继承体系的根即可。
继承层次过深会增加理解和维护的难度,应尽量保持简单。“尽量不超过三层”是设计建议,不是语法限制。
七、final:限制修改、重写和继承
| 修饰对象 | 作用 |
|---|---|
| 变量或字段 | 一旦赋值,就不能再次赋值 |
| 实例方法 | 子类不能重写这个方法 |
| 类 | 不能再被其他类继承 |
7.1 修饰变量
final int MAX = 10; // MAX = 20; // 编译错误:不能重新赋值 如果修饰的是引用变量,不能改变的是引用指向,并不自动禁止修改对象内部的状态。
7.2 修饰类
final class Animal { } // class Dog extends Animal { } // 编译错误:Animal 不能被继承 常用的 String 类就是 final 类,不能继承它。
7.3 修饰方法
class Base { public final void show() { System.out.println("固定实现"); } } class Derived extends Base { // public void show() { } // 编译错误:不能重写 final 方法 } Derived 仍然可以继承和调用这个可访问的方法,只是不能重新实现它。这里先记住限制,重写的详细规则以后再学。
八、继承与组合:是一个,还是有一个
继承和组合都能复用代码,但表达的关系不同。
| 方式 | 关系 | 示例 | 实现方式 |
|---|---|---|---|
| 继承 | is-a:是一个 | 奔驰是一种汽车 | class Benz extends Car |
| 组合 | has-a:有一个 | 汽车有发动机 | 在 Car 中定义 Engine 类型的字段 |
组合不需要特殊关键字,就是让一个对象持有另一个对象,并使用它提供的功能。
class Engine { public void start() { System.out.println("发动机启动"); } } class Car { private Engine engine = new Engine(); public void start() { engine.start(); System.out.println("汽车启动"); } } class Benz extends Car { } public class Test { public static void main(String[] args) { Benz car = new Benz(); car.start(); } } 输出:
发动机启动 汽车启动 这里同时存在两种关系:
-
Benz extends Car:奔驰是一种汽车,复用汽车的行为。 -
Car持有Engine:汽车有发动机,Car.start()通过engine.start()复用发动机的功能。
engine 是 Car 的私有字段,Benz 不能直接访问,但可以通过继承来的 start() 使用这项功能。
注意,单写 private Engine engine; 只声明了引用,默认值是 null,没有创建发动机对象;必须让它指向有效对象后才能调用方法。
选择时先判断关系:“是一种”考虑继承,“有一个”考虑组合。 不要仅为了少写几行代码就强行建立继承关系;同样能满足需求时,优先考虑组合,避免过深的继承链。
九、复习自检
- 父类和子类有同名字段,父类字段消失了吗? 没有。子类字段隐藏父类字段,在子类实例方法中可用
super.字段名明确访问父类字段。 - 子类有同名方法,就一定调用子类的吗? 不一定。重载要看参数;对于示例中的重写,可用
super.方法名(...)调用父类实现。 - 父类只有有参构造,子类构造什么都不写可以吗? 不可以。隐式
super()找不到父类无参构造,需要通过构造调用链选中父类已有的构造方法。 -
super能访问父类的private成员吗? 不能,super不会突破访问权限。 - 第二次创建子类对象,还执行静态代码块吗? 不再执行;父子类的实例代码块和构造方法仍会执行。
- 构造方法能被继承吗? 不能。调用父类构造方法与继承父类构造方法是两回事。
- 汽车和发动机应该用继承吗? 不应该。汽车“有”发动机,适合用组合。