一、继承解决什么问题

狗和猫都有名字、年龄,也都会吃饭、睡觉。如果分别在 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 

执行过程:

  1. new Dog("小黑", 2) 进入 Dog 的构造过程。
  2. super(name) 调用 Animal(String name),完成父类部分的初始化。
  3. 返回后继续执行 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,没有创建发动机对象;必须让它指向有效对象后才能调用方法。

选择时先判断关系:“是一种”考虑继承,“有一个”考虑组合。 不要仅为了少写几行代码就强行建立继承关系;同样能满足需求时,优先考虑组合,避免过深的继承链。

九、复习自检

  1. 父类和子类有同名字段,父类字段消失了吗? 没有。子类字段隐藏父类字段,在子类实例方法中可用 super.字段名 明确访问父类字段。
  2. 子类有同名方法,就一定调用子类的吗? 不一定。重载要看参数;对于示例中的重写,可用 super.方法名(...) 调用父类实现。
  3. 父类只有有参构造,子类构造什么都不写可以吗? 不可以。隐式 super() 找不到父类无参构造,需要通过构造调用链选中父类已有的构造方法。
  4. super 能访问父类的 private 成员吗? 不能,super 不会突破访问权限。
  5. 第二次创建子类对象,还执行静态代码块吗? 不再执行;父子类的实例代码块和构造方法仍会执行。
  6. 构造方法能被继承吗? 不能。调用父类构造方法与继承父类构造方法是两回事。
  7. 汽车和发动机应该用继承吗? 不应该。汽车“有”发动机,适合用组合。