继承英文单词叫作 inherit,在日常生活中也非常常见。回想一下在日常生活中、客户端阶段所学习到的 CSS 中,是不是都有出现过继承的概念?生活中继承的概念,通常是发生在具有上下级(父子)关系中。子不用做任何操作就可以得到父所有的内容,无论是 DNA 还是财富或是社会关系。

在面向对象编程中扩展一个已有的类,并继承这个类属性和方法来创建一个新的类,就称为继承。之前我们就提到过在类与类的关系中,除了 “has - a” 和 “use - a” 的关系以外,还有一种 is - a 的关系。

is 翻译成中文表示 “是”,当某个对象、物品是属于某个类的时候,我们就可以称之为产生了继承关系,就像学生是人、狗是动物、玫瑰是花,一样:

  • 学生是人,学生可以继承人类的所有属性和方法(姓名、年龄、性别、吃饭、走路等)
  • 狗是动物,狗可以继承动物类的所有属性和方法(眼睛、鼻子、腿、吃饭、喝水等)
  • 玫瑰是花,玫瑰也可以继承所有花类的属性和方法(科、目、属、生长的地点、花期等)

所以继承就是面向对象的第二个特征,提出了“类可以划分为父类和子类的关系”,凡是父类具备的属性和行为子类无需再次书写。

继承的出现既满足复用与分离的原则,又满足设计上的开闭原则。在进行面向对象分析设计时可以将共有的属性和行为定义在父类身上,将特有的属性和行为单独设计到子类身上。

分析父子类关系

假设当前某公司里有 3 种类型的员工:

  • 普通员工:每天工作 9 小时,薪水月结,正常购买各类保险
  • 小时工:每天工作 1 小时,薪水日结,不购买各类保险
  • 年薪员工:正常上下班时间,固定拿年薪,正常购买各类保险

在该场景中,普通员工、小时工、年薪工都 “是” 员工,如果不考虑 is 的关系,那么在设计时就可能会被设计为:

  • 3 个员工类创建出 3 个 class
  • 3 个员工类中的共有属性和方法就要重复写 3 次

我们站在 “分离” 与 “复用” 的角度去看待,肯定不希望三种员工类混在一起,同时又希望提取出一些共有的行为和属性进行复用,减少工作量和代码量,这时就可以使用 “继承”。

在面向对象编程中,同样如此。我们可以把场景中的类进行分析,将它们共有的属性和方法提取出来,定义在 “员工” 这个父类中。
一个基本的共有的层次结构就出现了:

  1. 员工类:Employee
  2. 属性:姓名 name, 薪水 salary
  3. 方法:工作 work

对于员工分类来说,还有 “买不买社保” 的区别,甚至于还可以有 “是否发工资条(email)” 的区别,那么对于子类来说还可以被各自特殊的属性行为分为:

  1. 普通员工类:
  2. 属性:姓名 name, 薪水 salary, 社保 SSN
  3. 行为:工作 work, 发工资清单 sendEmail
  4. 小时工:
  5. 属性:姓名 name, 薪水 salary
  6. 行为:工作 work

uml 类图

uml (Unified Modeling Language) 类图是一种统一建模语言,类图描述了模型的静态结构,包括模型中,类与类的内部结构,以及与其他类的关系。在结构化设计一个系统的时候类图可以让我们的思路更加清晰。
在画类图时,子类箭头朝向父类,这是规范。
image-20220510175119891.png
uml 类图说明:
1.类(Class):使用三层矩形框表示。第一层显示类的名称,如果是抽象类,则就用斜体显示。第二层是字段和属性。第三层是类的方法。前面的符号 ‘+’ 表示 public,‘-’ 表示 private,‘#’ 表示protected。2.UML类图符号之接口:使用两层矩形框表示,与类图的区别主要是顶端有<>显示。第一行是接口名称。第二行是接口方法。
3.UML类图符号之继承类(extends):用空心三角形+实线来表示。
4.UML类图符号之实现接口(implements):用空心三角形+虚线来表示
5.UML类图符号之关联(Association):用实线箭头来表示,例如:大雁与气候
6.UML类图符号之聚合(Aggregation):用空心的菱形+实线箭头来表示聚合:表示一种弱的‘拥有’关系,体现的是A对象可以包含B对象,但B对象不是A对象的一部分,例如:公司和员工组合(Composition):用实心的菱形+实线箭头来表示组合:部分和整体的关系,并且生命周期是相同的。例如:人与手
7.UML类图符号之依赖(Dependency):用虚线箭头来表示,例如:动物与氧气
8.UML类图符号之基数:连线两端的数字表明这一端的类可以有几个实例,比如:一个鸟应该有两只翅膀。如果一个类可能有无数个实例,则就用‘n’来表示。关联、聚合、组合是有基数的。
同时还需要注意在设计时,不要因为两个类有相同属性和方法就去做继承关系,继承是描述 类与类的关系,必须要有 “is - a“ 的关系才能去做继承。学生 is 人,狗 is 动物。不能说人有名字,狗有名字,人能吃饭,狗能吃饭,就是狗继承了人。

继承语法

使用 extends 关键字,单词表示“扩展”。
语法:

  1. public class 子类 extends 父类{
  2. // 子类的内容
  3. }
  1. // 学生继承人 自然会有 Student 类和 Human 类
  2. public class Student extends Humman{
  3. }
  4. // 狗继承动物 自然会有 Dog 类和 Animal 类
  5. public class Dog extends Animal{
  6. }

属性在继承中的表现

在继承的情况下,子类会自动继承父类所有属性。只是由于访问修饰符的限制,导致某些特殊访问修饰符的属性在子类中不能被直接访问。

  1. // Parent.java
  2. public class Parent {
  3. public String name;
  4. public String age;
  5. public String gender = "male";
  6. public void talk() {
  7. System.out.println("嘿嘿");
  8. }
  9. }
  10. // Child.java
  11. public class Child extends Parent {
  12. }
  13. // TestMain.java
  14. public class TestMain {
  15. public static void main(String[] args) {
  16. Child zhangsan = new Child();
  17. System.out.println(zhangsan.gender);
  18. zhangsan.talk();
  19. }
  20. }

image-20220511094456676.png

访问修饰符

在 java 中一共提供了 3 个关键字 4 种情况用来表现访问修饰符:

  1. public 公共
  2. protected 受保护
  3. 不写(默认)同包
  4. private 私有

访问权限,T 表示 true:

修饰符 本类 同包非子类 同包子类 非同包子类 非同包非子类
public 公共 T T T T T
protected 受保护 T T T T
不写(默认) 同包 T T T
private 私有 T

在实际开发中,最常用的,其实也就是 publicprivate 使用最多。

当父类与子类属性同名

从语法层面来说是允许的,但应该避免这种设计,否则就违背了继承的初衷——提取共有属性。
当出现有父子类存在同名属性,在子类中拥有了两个重名属性同时存在(不是覆盖)根据就近原则会使用到本类声明的属性。如果添加了 “this” 那么在子类中写的 this 仍然指子类的当前对象所拥有的那个属性。
如果在这种情况下一定想要访问父类中的同名属性,那么要把 this. 修改为 super. 后面再来说 super 的问题。

构造函数在继承中的表现

首先需要明确一点:子类无法继承父类构造器。

  1. 从语法层面上来说构造函数的名称与类名一致,若子类继承了父类的构造方法,那么相当于:子类中有个构造方法,方法名字是父类的名字。语法体系出现冲突
  2. 从面向对象场景来看,构造器的作用是用于产生对象的。父类的构造器产生父类对象,子类的构造器产生子类对象。如果子类继承了父类的构造,等于子类有方法来产生父类对象,这不符合常理

虽然没有继承,但父类的构造方法在继承的实现机制中,起到了不可替代的作用的。这就是 java 中实现继承的本质——内存叠加。

内存叠加

当 new 一个子类对象时,它会首先 new 父类对象,也就是首先调用父类构造产生父类对象部分,然后再调用子类的构造,在内存中父类对象下面叠加子类对象特有部分,从而形成完成的子类对象。
image-20220511111305466.png

普通方法在继承中的表现

父类中的所有方法都会被子类继承,至于子类能否调用到,仍然受到访问修饰符的限制。

方法重写

如果子类中重新定义了一个同名方法,那么父类方法则会被覆盖——专业上称为方法的重写。
重写方法的规范要求:

  1. 函数名一致
  2. 参数列表一致
  3. 返回类型一致
  4. 重写后的该方法的访问修饰符,访问范围不变或更大

image-20220511115357398.png

重载 overload 对比重写 override

相同处:

  1. 重载、重写都特指方法
  2. 它们体现的思想都是相同的行为不同的实现

不同处:

  1. 方法重载是在一个类中,有多个重名方法,各有各的实现,以参数个数、类型、顺序不同来区分
  2. 方法重写一定存在继承关系,父类有一个方法,子类继承后也有该方法,但子类的实现不同于父类的实现

    super

    super 跟 this 有点相似,也有两种用法 super.super() 。this 关键字是对象对自身的引用,super 关键字就是子对象对父对象的引用。

    super()

    super() 的写法是非常简单和常用的,写在子类的构造器第一句,指定调用父类的构造函数。将需要继承的父类的属性都放进 super() 的参数里:
    image-20220512114813333.png

    super.

    super. 的书写位置在子类的方法中,. 操作符后面跟上来自父类中定义的属性或方法,且受到访问修饰符的限制。
    image-20220512135415431.png
    用 this 和用 super 的使用规则:

  3. 当子类对象在引用一个继承来的字段或方法时用 this 或 super 都可以

  4. 当子类对象在访问自己类里声明的字段或方法就只能使用 this 引用
  5. 一般只有在访问被子类重写后的方法才会用到 super

    继承的层次结构

继承可以从上往下顺序继承,这个层次结构理论上可以有无限层。

  • 直接继承关系上层叫 “父类”,下层是 “子类”
  • 非直接的继承关系上层叫作 “基类” 或 “超类”,下层叫作 “派生类”,这是纵向结构。

在日常生活中,每个人除了有父亲以外,还有母亲,所以我们是从他俩同时继承而来的,这种情况称为多继承,像 C++ 就选择了多继承。但 java 语言在设计时采用的却是 “单继承”,也就是说 每个类有且只有一个父类 。

单继承对比多继承

正因为面向对象的设计思想存在单继承与多继承的差别,因此也常会将两者进行对比。

多继承优点:

  • 丰富度,像混血儿就很漂亮聪明

多继承缺点:

  • 类结构复杂,容易出现网状结构

单继承优点:

  • 层级结构简单,都是树状自上而下的

单继承缺点:

  • 灵活度、丰富度欠缺