继承

概述

封装: 对象代表什么, 就得封装对应的数据, 并提供数据对应的行为.

利用封装, 就可以把一些零散的数据和一些行为封装为一个整体, 这个整体就是对象.

假设现在要在方法当中打印学生的信息, 如果没有封装, 那么就要把这些零散的数据全部单独地传递给方法, 那么方法就要写很多很多的参数. 但是有了封装之后, 就可以将这些参数作为一个整体, 即对象传递给方法.


图 1

但是当这样的 Javabean 类越来越多之后, 问题就会出现了. 假设现在又有了第二个 Javabean 类去描述老师, 老师和学生这两个 Javabean 类有大量重复的代码. 于是可以将两个 Javabean 类中相同的代码全部抽取出来放到另一个位置, 这两个 Javabean 类共同取用这个位置的代码.


图 2

图 3

Java 中提供了一个关键字 extends, 利用这个关键字, 就可以让一个类和另一个类建立起继承关系. 例如:

public class Student extends Person{
// Student 类的具体的代码
}

Student 称为子类或派生类, Person 称为父类或基类, 超类.

子类自动获得父类功能.

子类可以覆盖父类中的方法. 覆盖的意思是子类重新定义继承下来的方法, 以改变或延伸此方法的行为.

使用继承的好处:

  1. 可以把多个子类中重复的代码抽取到父类中, 提高了代码的复用性.

  2. 子类可以在父类的基础上, 增加其他的功能, 使子类更强大.

关于继承最主要的需要学会的内容:

  1. 自己设计一个继承结构并将代码写出来.

  2. 学会使用别人已经写好的继承结构.

  3. 子类可以从父类中继承哪些内容?

  4. 继承中, 成员变量的访问特点.

  5. 继承中, 成员方法的访问特点.

  6. 继承中, 构造方法的访问特点.

  7. this 和 super 的使用方法.

什么时候使用继承: 当类与类之间, 存在相同 (共性) 的内容, 并满足子类是父类中的一种, 就可以考虑使用继承, 来优化代码.

继承的特点: Java 中只支持单继承, 不支持多继承, 但是支持多层继承.

单继承: 一个子类只能继承一个父类.
不支持多继承: 子类不能同时继承多个父类.
多层继承: 子类 A 继承父类 B, 父类 B 可以继承自父类 C. C 是 A 的间接父类, B 是 A 的直接父类.

Java 中有一个最大的祖类, 叫做 Object. 这个类是 Java 提前写好的, 每一个类都直接或间接继承自 Object. 如果写了一个类, 没有指定其继承自哪个类, 就默认其直接继承自 Object 类. 这是在虚拟机运行的时候自动帮我们加上去的, 如果当前类有父类, 那虚拟机不做其他操作, 如果当前类没有指定父类, 则虚拟机会自动增加一个 Object 类作为当前类的父类.

指定了父类的类间接继承自 Object 类. 没有指定父类的类直接继承自 Object 类.

当 Java 的类写得越来越多时, 就形成了一个继承体系. 在继承体系最上面的类就默认是继承自 Object 类, 下面的所有的类都是间接地继承自 Object. 在继承体系中, 任何一个子类, 既可以使用直接父类里面的内容, 也可以使用间接父类里面的内容, 但是不能使用类似于叔叔类的内容.

子类只能访问父类中非私有的成员, 即不是用 private 修饰的内容.


图 4

继承的练习: 自己设计一个继承体系
现在有四种动物: 布偶猫, 中国狸花猫, 哈士奇, 泰迪. 暂时不考虑属性, 只要考虑行为. 请按照继承的思想特点进行继承体系的设计.
四种动物分别有以下的行为:
布偶猫: 吃饭, 喝水, 抓老鼠
中国狸花猫: 吃饭, 喝水, 抓老鼠
哈士奇: 吃饭, 喝水, 看家, 拆家
泰迪: 吃饭, 喝水, 看家, 蹭一蹭

思路:
采用画图法, 从下往上画图, 下面是子类, 上面是父类, 将子类中的共性内容抽取到父类中.
但是书写的代码要从上往下写.


图 5

Javabean 类:

public class Animal {
public void eat() {
System.out.println("吃饭");
}
public void drink() {
System.out.println("看家");
}
}
public class Cat extends Animal {
public void catchRat() {
System.out.println("猫抓老鼠");
}
}
public class Dog extends Animal {
public void watchDoor() {
System.out.println("狗在看家");
}
}
public class RollCat extends Cat {
}
public class LiHua extends Cat {
}
public class Husky extends Dog {
public void destroyHome() {
System.out.println("哈士奇在拆家");
}
}
public class Teddy extends Dog {
public void touch() {
System.out.println("泰迪在蹭一蹭");
}
}

测试类:

public class Test {
public static void main(String[] args) {
// 创建对象并使用
// 创建一个布偶猫的对象并调用其方法
Ragdoll ragdoll = new Ragdoll();
ragdoll.catRat();
ragdoll.eat();
ragdoll.drink();
System.out.println("---------------------");
// 创建一个哈士奇的对象并调用其方法
Husky husky = new Husky();
husky.drink();
husky.eat();
husky.breakHome();
husky.watchHome();
}
}

执行结果:

猫抓老鼠
吃饭
喝水
---------------------
喝水
吃饭
哈士奇在拆家
狗看家

图 6

继承的内容

子类可以从父类中继承哪些内容?

要回答这个问题, 首先要看父类中到底有哪些东西? 其实父类中只有三种内容: 构造方法, 成员变量, 成员方法. 这三个统称为类的成员.

不同的成员, 其修饰符是不一样的. 非私有就是不是用 private 来修饰的, 私有就是用 private 来修饰的. 非私有的修饰符包括 public 等等, 它们统称为非私有, 它们在是否能被子类继承这方面的规则是相同的.


图 1

构造方法不论是用何种修饰符, 都不能被子类继承.

成员变量不论是用何种修饰符, 都可以被子类继承. 继承下来和能否被调用不是一个概念, 父类中私有的成员变量确实可以被子类继承下来, 在子类中也保存有一份, 但是并不能被子类调用.

成员方法如果是非私有的, 则可以被子类继承, 如果是私有的, 就不能被子类继承下来.

构造方法


图 2

如果子类可以继承父类的构造方法, 则在子类中, 违反了构造方法的方法名必须和所在类的类名相同的原则.

构造方法不论是私有还是非私有, 都不能被子类所继承. 如果构造方法可以继承, 那么在子类中的构造方法的方法名, 将和父类的类名相同, 然而子类的构造方法的方法名, 必须和子类的类名相同, 于是引起了冲突.

子类中所有的构造方法默认先访问父类中的无参构造, 再执行自己. 因为子类在初始化的时候, 有可能会使用到父类中的数据, 如果父类没有完成初始化, 子类将无法使用父类的数据. 因此, 子类的所有的构造方法都默认先执行父类的无参构造, 先完成父类数据空间的初始化, 且是默认初始化 (因为是无参构造) .

子类虽然不能继承父类的构造方法, 但是可以用 super 调用父类的构造方法.

子类的无参构造方法和有参构造方法的第一行语句都是默认的 super(); 语句, 即调用父类的无参构造, 即使不写也是存在的, 且必须在第一行. 子类中所有的构造方法默认先访问父类中的无参构造, 再执行自己这个构造方法体.

如果想要调用父类的有参构造, 必须手动写 super(参数); 进行调用.

super(); 上面写语句会报错:


图 3

程序示例:

Javabean 类:

public class Person {
private String name;
private int age;
public Person() {
System.out.println("Person constructor with no parameters");
}
public Person(String name, int age) {
System.out.println("Person constructor with all parameters");
this.name = name;
this.age = age;
}
}
public class Student extends Person {
public Student() {
super();
System.out.println("Student constructor with no parameters");
}
public Student(String name, int age) {
super(name, age);
System.out.println("Student constructor with all parameters");
}
}

测试类:

public class Test {
public static void main(String[] args) {
// 利用学生类的无参构造创建学生对象, 查看构造方法的调用情况
Student s1 = new Student();
System.out.println("----------------");
// 利用学生类的带全部参数的构造方法创建学生对象, 查看构造方法的调用情况
Student s2 = new Student("zhangsan", 20);
System.out.println(s2.getName() + ", " + s2.getAge());
}
}

执行结果:

Person constructor with no parameters
Student constructor with no parameters
----------------
Person constructor with all parameters
Student constructor with all parameters
zhangsan, 20

可以看出来, 先执行了父类的无参 / 带全部参数的构造方法, 再执行了子类的无参 / 带全部参数的构造方法.

父类中的非私有的 getter 和 setter 方法都被子类继承下来了, 子类的对象可以直接调用这些方法.

同时也可以看出来, 父类中私有的成员变量, 是可以被子类继承下来的, 只是子类无法直接访问, 可以通过 getter 和 setter 方法来访问.

在子类的带参构造中可以手动调用父类的带参构造给成员变量赋值, 示意图:


图 4

成员变量

不论是私有还是非私有, 成员变量都可以被子类继承. 子类能继承不代表能调用, 继承的父类的私有的成员变量, 虽然被继承下来, 存在于子类当中, 但是却无法被直接调用, 只能通过 setXXX / getXXX 来调用.

成员变量的继承内存图:

注意, 这里父类的成员变量是非私有的.


图 5

首先是测试类 TestStudent 先执行, 其字节码文件 TestStudent.class 加载到方法区中, 这里面存储的就是 main 方法.

main 方法被虚拟机自动调用, main 方法进栈.

然后执行 main 方法里面的第一行代码 Zi z = new Zi();, 用到了 Zi 这个类, 所以要先把 Zi 这个类的字节码文件 Zi.class 加载到方法区中, 这里面存储着 Zi 这个类里面的成员变量 game.

加载 Zi.class 文件时, 虚拟机发现这个类还有一个被明确指定的父类, 即 Fu, 所以虚拟机还会把 Fu.class 文件加载到方法区中, 这里面存储着 Fu 这个类里面的 name 和 age. 而 Fu 这个类默认继承自 Object 类, 所以虚拟机还会将 Object 这个类的字节码文件加载到方法区. 但是现在这个代码中和 Object 这个类的关系不大, 所以暂时不考虑 Object 这个类.

字节码文件加载完毕后, 再来执行等号的左边, 即 Zi = z, 这是在栈里面声明了一个变量, 变量的名字叫做 z, 同时在栈里面开辟了一块属于这个变量的小空间, Zi 是这个变量的类型限定, 表示这个小空间可以存储 Zi 这个对象的地址值.

接下来到等号的右边, 即 new Zi();, 遇到了 new 关键字, 就一定是在堆内存中开辟了一块小空间, 用来存放对象. 假设这一块内存的地址值为 001.

以前没有讲到继承时, 这块小空间是一个整体, 现在遇到继承了, 这块小空间会被一分为二, 一部分被用来记录父类里面的成员变量, 另一部分被用来记录子类本身的成员变量. 所以左侧这个小块里面有 name 和 age, 是从父类里面继承过来的, 右侧这一小部分是子类本身的成员变量 game. 同时, 要给这三个成员变量赋予默认初始化值, name 和 game 是 String 类型的, 默认初始化值为 null, age 是 int 类型的, 默认初始化值为 0.

接下来再把地址值 001 赋值给变量 z. z 通过地址值 001 就能找到堆内存中创建的这个对象. 此时, 这一行代码 Zi z = new Zi(); 才算是真正地运行完毕.

继续向下执行, 到了 sout(z);, 相当于是把变量 z 里面记录的东西进行打印. 记录什么就打印什么, 在上一行代码中, z 记录了地址值 001, 那么在控制台里面就是打印地址值 001.

继续向下执行, 到了 z.name = "钢门吹雪";, 此时会先去找堆内存中地址为 001 的这块空间中, 存储着子类的那一份小空间, 即图中的右侧, 此时是不会找到的, 没有找到则又去左侧存储父类成员变量的那部分去找. 就把默认值 null 覆盖了.

继续向下执行, 到了 z.age = 23;, 是把 23 赋值给 z 的 age 成员变量, 就把默认值 0 覆盖了.

继续向下执行, 到了 z.game = "王者农药";.

最后, 执行到了 sout(z.name + ", " + z.age + ", " + z.game);.

总结起来, 和没有继承时, 内存的利用的不同点有:

  1. 加载类的字节码文件到方法区时, 会把父类的字节码文件也加载到方法区;

  2. 在堆内存中创建对象时, 会将小空间一分为二, 一部分空间是存储从父类继承下来的成员变量, 另一部分空间是存储子类本身的成员变量;

  3. 在利用对象名称去调用成员变量时, 在堆内存中寻找成员变量时, 优先寻找子类本身的成员变量, 找不到的话再去寻找从父类继承下来的成员变量.

当 mian 方法执行完毕, main 方法从栈中出去, 一旦方法出栈了, 方法里面的变量也就消失了, 变量消失了, 针对堆内存中的对象而言, 就没有变量再使用它了, 这块堆内存就变成垃圾, 虚拟机里面有垃圾回收器, 会在合适的时候, 将这些用不到的垃圾进行清理. 这个清理是自动的, 不需要我们管. 我们只需知道, 一旦对象变为垃圾, 我们就不能再去使用了.

成员变量的继承内存图:

注意, 这里父类的成员变量私有的.


图 6

在执行到语句 z.name = "钢门吹雪"; 之前, 过程和上面都是一样的. 但是到了这里, 就不一样了. 首先, 先去堆内存中那块小空间的左边部分, 即存储子类自身的成员变量的位置, 去找变量 name, 显然找不到, 因为这是从父类继承下来的成员变量, 然后就去右边部分存储着从父类继承下来的成员变量中去找, 然而, 这个成员变量是被 private 修饰的, 即私有的. 一旦私有, 那么这个用 z.name 直接调用的方式, 是找不到的. 于是赋值失败, 程序报错.

语句 z.age = 23; 同样的道理. 于是堆内存中的 name 和 age 还是原来的默认初始化值 null 和 0, 因为赋值失败了. 其实程序走到这里就报错了, 此处只是继续向下看代码会执行什么, 只是在分析. 但是如果使用相应的 getter 和 setter 方法, 是可以访问从父类继承下来的私有成员变量的.

z.game = "王者农药"; 是会执行成功的.

成员变量的访问遵循就近原则.

程序示例:

class Fu {
String name = "Fu";
}
class Zi extends Fu {
String name = "Zi";
public void ziShow() {
String name = "ziShow";
System.out.println(name); // 打印 ziShow, 这个变量 name 是方法内的局部变量, 非成员变量
}
}
class Fu {
String name = "Fu";
}
class Zi extends Fu {
String name = "Zi";
public void ziShow() {
System.out.println(name); // 打印 Zi, 这个变量 name 是 Zi 类中的成员变量
}
}
class Fu {
String name = "Fu";
}
class Zi extends Fu {
public void ziShow() {
System.out.println(name); // 打印 Fu, 这里的变量 name 是从父类 Fu 中继承下来的非私有的成员变量
}
}
class Fu {
}
class Zi extends Fu {
public void ziShow() {
System.out.println(name); // 报错, 找不到变量 name
}
}

这种变量同名的情况可以用关键字进行区分.

程序示例:

public class Test {
public static void main(String[] args) {
Zi z = new Zi();
z.ziShow();
}
}
class Fu {
String name = "Fu"; // Fu 类的成员变量 name
}
class Zi extends Fu {
String name = "Zi"; // Zi 类的成员变量 name
public void ziShow() {
String name = "ziShow"; // 方法的局部变量 name
System.out.println(name); // 访问局部变量, 打印 ziShow
System.out.println(this.name); // 访问成员变量, 打印 Zi
System.out.println(super.name); // 访问父类变量, 打印 Fu
}
}

如果父类的成员变量是 private 的则不可以用 super 来访问. 程序示例:

public class Test {
public static void main(String[] args) {
Zi z = new Zi();
z.ziShow();
}
}
class Fu {
private String name = "Fu"; // Fu 类的成员变量 name
}
class Zi extends Fu {
private String name = "Zi"; // Zi 类的成员变量 name
public void ziShow() {
String name = "ziShow"; // 方法的局部变量 name
System.out.println(name); // 访问局部变量, 打印 ziShow
System.out.println(this.name); // 访问成员变量, 打印 Zi
// System.out.println(super.name); // 报错: The field Fu.name is not visible
}
}

可见, 这里的 super.name 等价于 Fu.name, 即 super 等价于 Fu.

程序示例:

public class Test {
public static void main(String[] args) {
Zi z = new Zi();
z.ziShow();
}
}
class Ye {
String name = "Ye"; // Ye 类的成员变量 name
}
class Fu {
// String name = "Fu"; // Fu 类的成员变量 name
}
class Zi extends Fu {
// String name = "Zi"; // Zi 类的成员变量 name
public void ziShow() {
String name = "ziShow"; // 方法的局部变量 name
System.out.println(name); // 访问局部变量, 打印 ziShow
// System.out.println(this.name); // 报错: name cannot be resolved or is not a field
// System.out.println(super.name); // 报错: name cannot be resolved or is not a field
}
}

this 先在本类中找成员变量, 找不到就到父类中找成员变量, 如果还没有, 是不会接着往上找的, 直接报错.

super 直接去直接父类中找, 找不到则报错.

程序示例:

public class Test {
public static void main(String[] args) {
Zi z = new Zi();
z.ziShow();
}
}
class Fu {
String name = "Fu"; // Fu 类的成员变量 name
}
class Zi extends Fu {
// String name = "Zi"; // Zi 类的成员变量 name
public void ziShow() {
String name = "ziShow"; // 方法的局部变量 name
System.out.println(name); // 访问局部变量, 打印 ziShow
System.out.println(this.name); // 访问成员变量, 打印 Fu
}
}

程序示例:

public class Test {
public static void main(String[] args) {
Zi z = new Zi();
z.ziShow();
}
}
class Fu {
String name = "Fu";
String hobby = "喝茶";
}
class Zi extends Fu {
String name = "Zi";
String game = "吃鸡";
public void ziShow() {
// 打印Zi
System.out.println(name); // Zi
System.out.println(this.name); // Zi
// 打印喝茶
System.out.println(hobby); // 喝茶
System.out.println(this.hobby); // 喝茶, 继承下来了
System.out.println(super.hobby); // 喝茶
// 打印吃鸡
System.out.println(game); // 吃鸡
System.out.println(this.game); // 吃鸡
}
}

建议: 访问方法内的变量, 直接用变量名; 访问子类的成员变量, 用 this.成员变量名; 访问父类的成员变量, 用 super.成员变量名.

成员方法

虚方法: 没有被 private, static 和 final 修饰的方法.

一个类里面提供了一个虚方法表, 存放了这个类中的所有虚方法. 如果这个类有子类, 则这个类将自己的虚方法表复制一份交给子类, 子类的虚方法表是从父类继承来的虚方法加上自己的虚方法所构成.


图 7

在 A 类中, 如果调用 C 类的虚方法, 那么直接在自己的虚方法表中查找就能找到, 如果要调用 C 类的虚方法之外的方法, 那么就要先去 B 类中查找, 如果没有找到, 就再到 C 类中去查找.

有了虚方法表, 程序的性能会大大提高.

方法重写的关键点也在虚方法表中.

父类的方法中, 只有虚方法才能被子类继承, 父类中的其他方法是不会被子类继承下来的.

子类执行一个方法时, 如果是父类中的虚方法, 那就到子类自己的虚方法表中去找这个方法, 如果不是父类的虚方法, 那么这个方法就不会被继承到子类中, 然后就去子类自己的空间中去找这个方法, 如果找到了, 那就执行这个方法, 如果没找到, 那就要一层一层往上到父类中去找这个方法, 如果找到了这个方法且方法不是私有的, 那么就再去执行这个方法, 如果父类中这个方法是私有的, 那么用对象加点号直接访问的方式是无法访问这个私有方法的, 程序报错.

Object 类有 5 个虚方法.

示例:


图 8

首先 TestStudent.class 字节码文件先被加载进方法区, 接着 main 方法进栈.

然后执行语句 Zi z = new Zi();, 用到了 Zi 类, 则 Zi.class 加载进方法区. 由于 Zi 类继承自 Fu 类, 则 Fu.class 加载进栈中. 由于 Fu 类继承自 Object, 则 Object.class 加载进栈中. 方法区中存放了类的全部成员变量和成员方法, 此处只关注成员方法.

Object 类有 5 个虚方法, 到了 Fu 类中就有了 6 个虚方法, 到了 Zi 类中就有了 7 个虚方法. 至此, 整个字节码文件才算是加载完毕.


图 9

执行到语句 z.ziShow(); 时, 首先看这个方法是不是虚方法, 如果是虚方法, 就到调用这个方法的对象的类的虚方法表中去找这个方法. 显然, ziShow() 方法是存在于 Zi.class 的虚方法表中的.


图 10

执行语句 z.fuShow1(); 也是同样的道理.


图 11

执行语句 z.fuShow2(); 时, 发现这并不是一个虚方法, 则不会到本类的虚方法表中去找, 而是会先在自己的这个类中找有没有这个方法, 发现没有, 然后去父类 Fu 类中去找有没有这个方法, 结果找到了, 并且发现这个方法是私有的, 直接通过对象名去调用这个方法是不可行的, 于是程序报错.

成员方法的访问同样遵循就近原则, 谁离得近就访问谁, 也可以用关键字 super 直接访问父类中的方法.

程序示例:

public class Test {
public static void main(String[] args) {
Student s = new Student();
s.lunch();
System.out.println("--------------------------------");
OverseasStudents os = new OverseasStudents();
os.lunch();
}
}
class Person {
public void eat() {
System.out.println("吃米饭, 吃菜. ");
}
public void drink() {
System.out.println("喝开水. ");
}
}
class OverseasStudents extends Person {
public void lunch() {
eat();
drink();
this.eat();
this.drink();
super.eat();
super.drink();
}
public void eat() { // 重写父类方法
System.out.println("吃意大利面. ");
}
public void drink() { // 重写父类方法
System.out.println("喝凉水. ");
}
}
class Student extends Person {
public void lunch() {
// 先在子类中查看 eat() 和 drink() 方法, 如果有, 就调用子类的, 如果没有, 就去父类中找从父类中继承下来的 eat() 和 drink()
eat(); // 方法在调用的时候需要有调用者, 这里的 eat() 等价于 this.eat()
drink();
// 直接调用父类中的 eat 和 drink 方法, 不会去子类中查找
super.eat();
super.drink();
}
}

执行结果:

吃米饭, 吃菜.
喝开水.
吃米饭, 吃菜.
喝开水.
--------------------------------
吃意大利面.
喝凉水.
吃意大利面.
喝凉水.
吃米饭, 吃菜.
喝开水.

方法的重写

应用场景: 当父类的方法不能满足子类的需求时, 就需要进行方法的重写.

书写格式: 在继承体系中, 子类出现了和父类中一模一样的方法声明, 我们就称子类这个方法是重写的方法.

@Override 重写注解: @Override 放在重写后的方法上, 校验子类重写时语法是否正确.

注解和注释都是给程序的解释说明, 注释是给程序员看的, 注解是给程序员和虚拟机看的. 当虚拟机看到一个方法当中有 @Override 的时候, 虚拟机就知道了这个方法是重写的父类中的方法, 于是就会去检查重写的语法是否正确. 如果重写语法是正确的, 那虚拟机就没有任何的提示, 如果语法错误, 就会有红色波浪线提示有语法错误. 建议在重写方法的时候, 都要加上 @Override 注解, 代码安全且优雅.

程序示例:

public class Test {
public static void main(String[] args) {
OverseasStudents os = new OverseasStudents();
os.lunch();
}
}
class Person {
public void eat() {
System.out.println("吃米饭, 吃菜. ");
}
public void drink() {
System.out.println("喝开水. ");
}
}
class OverseasStudents extends Person {
public void lunch() {
eat();
drink();
this.eat();
this.drink();
super.eat();
super.drink();
}
@Override
public void eat() {
System.out.println("吃意大利面. ");
}
@Override
public void drink() {
System.out.println("喝凉水. ");
}
}

方法的重写一定是建立在子类, 父类的关系上的.

重写方法的名称, 形参列表必须和父类中保持一致.

子类重写父类方法时, 访问权限子类必须大于等于父类. 空着不写 < protected < public.

子类重写父类方法时, 返回值类型必须小于等于父类. 返回值类型也可能是自定义的某个类, 父类的返回值类型必须是子类的返回值类型的父类或者是相同类型.

建议: 重写方法时, 尽量和父类保持一致.

方法重写的本质是覆盖了从虚方法表中继承过来的方法. 如果一个方法不能加到虚方法表中, 那么这个方法就不能被重写. 只有能够被添加到虚方法表中的方法才能够被重写.


图 12

练习:
利用方法的重写设计继承结构
现在有三种动物: 哈士奇, 沙皮狗, 中华田园犬
暂时不考虑属性, 只要考虑行为.
请按照继承的思想特点进行继承体系的设计.
三种动物分别有以下的行为:
哈士奇: 吃饭 (吃狗粮) , 喝水, 看家, 拆家
沙皮狗: 吃饭 (吃狗粮, 吃骨头) , 喝水, 看家
中华田园犬: 吃饭 (吃剩饭) , 喝水, 看家


图 13

重写父类方法时, 也要分为需要调用父类方法, 在父类方法的基础上进行修改, 或者完全不需要用到父类方法.

Javabean 类:

public class Dog {
public void eat(){
System.out.println("狗在吃狗粮");
}
public void drink(){
System.out.println("狗在喝水");
}
public void watchHome(){
System.out.println("狗在看家");
}
}
public class Husky extends Dog{
// 不需要重写, 需要添加新的方法
public void breakHome(){
System.out.println("哈士奇在拆家");
}
}
public class Sharpei extends Dog {
// 当父类的方法不能满足子类的需求时, 就需要进行方法的重写
// 此处的重写的方法中, 用到了父类中的方法, 用 super 调用父类中的方法
@Override
public void eat() {
super.eat();
System.out.println("狗啃骨头");
}
}
public class ChineseDog extends Dog {
// 当父类的方法不能满足子类的需求时, 就需要进行方法的重写
// 此处, 重写的方法完全用不到父类中的方法, 就不需要用 super
@Override
public void eat() {
System.out.println("狗吃剩饭");
}
}

测试类:

public class DogTest {
public static void main(String[] args) {
// 创建哈士奇对象
Husky husky = new Husky();
// 调用哈士奇对象的方法
husky.breakHome();
husky.drink();
husky.eat();
husky.watchHome();
System.out.println("-------------------------------------------");
// 创建沙皮狗对象
Sharpei sharpei = new Sharpei();
// 调用沙皮狗对象的方法
sharpei.eat();
sharpei.drink();
sharpei.watchHome();
System.out.println("-------------------------------------------");
// 创建中华田园犬对象
ChineseDog chineseDog = new ChineseDog();
// 调用中华田园犬对象的方法
chineseDog.eat();
chineseDog.drink();
chineseDog.watchHome();
}
}

this 和 super 总结

this 可以理解为一个局部变量, 表示调用者的地址.

super 代表父类的存储空间.


图 14

在无参构造方法中, 可以用 this 调用同一个类中的其他构造方法, 此时 this 语句必须写在最上面一行, 且不需要写 super, 因为这个构造方法调用了别的构造方法, 而别的构造方法里面有 super, 只要有一个 super 就可以了. 这样做就相当于是提供了默认值.

程序示例:

Javabean 类:

public class Student {
String name;
int age;
String school;
// 需求: 学校默认为小学
public Student() {
this(null, 0, "小学"); // 调用别的构造方法, 根据重载的规则, 自行判断调用的是哪个构造方法
}
public Student(String name, int age, String school) { // 这里面默认有一个 super();, 且是在第一行
this.name = name;
this.age = age;
this.school = school;
}
}

测试类:

public class StudentTest {
public static void main(String[] args) {
Student s = new Student();
System.out.println(s.school); // 小学
}
}

练习:
带有继承结构的标准 Javabean 类

  1. 经理
    成员变量: 工号, 姓名, 工资, 管理奖金
    成员方法: 工作 (管理其他人) , 吃饭 (吃米饭)
  2. 厨师
    成员变量: 工号, 姓名, 工资
    成员方法: 工作 (炒菜) , 吃饭 (吃米饭)

程序:

Javabean 类:

public class Employee {
private String id;
private String name;
private double salary;
public Employee() {
}
public Employee(String id, String name, double salary) {
this.id = id;
this.name = name;
this.salary = salary;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getSalary() {
return salary;
}
public void setSalary(double salary) {
this.salary = salary;
}
// 两个子类都重写了这个工作方法, 那么还需要抽取吗?
// 答案是需要, 抽取出来就表示这是一个通用的工作方式.
// 如果以后又来了一个子类收银员, 而收银员没有指定工作方法, 那就能用这个通用的工作方法了.
public void work() {
System.out.println("员工在工作");
}
public void eat() {
System.out.println("吃米饭");
}
}
public class Manager extends Employee {
// 经理有一个单独的成员变量, 即管理奖金
private double bouns;
// 空参构造, 这里可以选择手动写一个空参的, 或者还是用 alt+insert 快捷键生成, 但是生成后需要手动删除一些东西
public Manager() {
}
// 带全部参数的构造方法, 要把从父类中继承来的成员变量和子类自己的成员变量全部放进去
public Manager(String id, String name, double salary, double bouns) {
super(id, name, salary);
this.bouns = bouns;
}
// 从父类继承来的成员变量, 不需要再写 get 和 set, 父类中有, 可以直接用
public double getBouns() {
return bouns;
}
public void setBouns(double bouns) {
this.bouns = bouns;
}
// 重写父类中的工作方法
@Override
public void work() {
System.out.println("管理其他人");
}
}
public class Cook extends Employee{
// 即便没有新的成员变量, 也要重新写明属于这个类的两个构造方法
public Cook() {
}
public Cook(String id, String name, double salary) {
super(id, name, salary);
}
// 重写父类的工作方法
@Override
public void work() {
System.out.println("厨师在炒菜");
}
}

测试类:

public class Test {
public static void main(String[] args) {
Manager zhangsan = new Manager("001", "zhangsan", 15000, 8000);
System.out.println(zhangsan.getId() + ", " + zhangsan.getName() + ", " + zhangsan.getSalary() + ", " + zhangsan.getBouns());
zhangsan.work();
zhangsan.eat();
Cook cook = new Cook();
cook.setId("002");
cook.setName("lisi");
cook.setSalary(14000);
System.out.println(cook.getId() + ", " + cook.getName() + ", " + cook.getSalary());
cook.work();
cook.eat();
}
}

Manager 类中定义构造方法时的操作:

使用快捷键生成空参构造和全参构造:


图 15

练习:
带有继承结构的标准 Javabean 类
在黑马程序员中有很多员工 (Employee)
按照工作内容不同分教研部员工 (Teacher) 和行政部员工 (AdminStaff)

  1. 教研部根据教学的方式不同又分为讲师 (Lecturer) 和助教 (Tutor)
  2. 行政部根据负责事项不同, 又分为维护专员 (Maintainer) 和采购专员 (Buyer)
  3. 公司的每一个员工都编号, 姓名和其负责的工作.
  4. 每个员工都有工作的功能, 但是具体的工作内容又不一样.
posted @   有空  阅读(11)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
点击右上角即可分享
微信分享提示

目录导航