继承
概述
封装: 对象代表什么, 就得封装对应的数据, 并提供数据对应的行为.
利用封装, 就可以把一些零散的数据和一些行为封装为一个整体, 这个整体就是对象.
假设现在要在方法当中打印学生的信息, 如果没有封装, 那么就要把这些零散的数据全部单独地传递给方法, 那么方法就要写很多很多的参数. 但是有了封装之后, 就可以将这些参数作为一个整体, 即对象传递给方法.

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


Java 中提供了一个关键字 extends, 利用这个关键字, 就可以让一个类和另一个类建立起继承关系. 例如:
public class Student extends Person{ // Student 类的具体的代码 }
Student 称为子类或派生类, Person 称为父类或基类, 超类.
子类自动获得父类功能.
子类可以覆盖父类中的方法. 覆盖的意思是子类重新定义继承下来的方法, 以改变或延伸此方法的行为.
使用继承的好处:
-
可以把多个子类中重复的代码抽取到父类中, 提高了代码的复用性.
-
子类可以在父类的基础上, 增加其他的功能, 使子类更强大.
关于继承最主要的需要学会的内容:
-
自己设计一个继承结构并将代码写出来.
-
学会使用别人已经写好的继承结构.
-
子类可以从父类中继承哪些内容?
-
继承中, 成员变量的访问特点.
-
继承中, 成员方法的访问特点.
-
继承中, 构造方法的访问特点.
-
this 和 super 的使用方法.
什么时候使用继承: 当类与类之间, 存在相同 (共性) 的内容, 并满足子类是父类中的一种, 就可以考虑使用继承, 来优化代码.
继承的特点: Java 中只支持单继承, 不支持多继承, 但是支持多层继承.
单继承: 一个子类只能继承一个父类.
不支持多继承: 子类不能同时继承多个父类.
多层继承: 子类 A 继承父类 B, 父类 B 可以继承自父类 C. C 是 A 的间接父类, B 是 A 的直接父类.
Java 中有一个最大的祖类, 叫做 Object. 这个类是 Java 提前写好的, 每一个类都直接或间接继承自 Object. 如果写了一个类, 没有指定其继承自哪个类, 就默认其直接继承自 Object 类. 这是在虚拟机运行的时候自动帮我们加上去的, 如果当前类有父类, 那虚拟机不做其他操作, 如果当前类没有指定父类, 则虚拟机会自动增加一个 Object 类作为当前类的父类.
指定了父类的类间接继承自 Object 类. 没有指定父类的类直接继承自 Object 类.
当 Java 的类写得越来越多时, 就形成了一个继承体系. 在继承体系最上面的类就默认是继承自 Object 类, 下面的所有的类都是间接地继承自 Object. 在继承体系中, 任何一个子类, 既可以使用直接父类里面的内容, 也可以使用间接父类里面的内容, 但是不能使用类似于叔叔类的内容.
子类只能访问父类中非私有的成员, 即不是用 private 修饰的内容.

继承的练习: 自己设计一个继承体系
现在有四种动物: 布偶猫, 中国狸花猫, 哈士奇, 泰迪. 暂时不考虑属性, 只要考虑行为. 请按照继承的思想特点进行继承体系的设计.
四种动物分别有以下的行为:
布偶猫: 吃饭, 喝水, 抓老鼠
中国狸花猫: 吃饭, 喝水, 抓老鼠
哈士奇: 吃饭, 喝水, 看家, 拆家
泰迪: 吃饭, 喝水, 看家, 蹭一蹭
思路:
采用画图法, 从下往上画图, 下面是子类, 上面是父类, 将子类中的共性内容抽取到父类中.
但是书写的代码要从上往下写.

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(); } }
执行结果:
猫抓老鼠 吃饭 喝水 --------------------- 喝水 吃饭 哈士奇在拆家 狗看家

继承的内容
子类可以从父类中继承哪些内容?
要回答这个问题, 首先要看父类中到底有哪些东西? 其实父类中只有三种内容: 构造方法, 成员变量, 成员方法. 这三个统称为类的成员.
不同的成员, 其修饰符是不一样的. 非私有就是不是用 private 来修饰的, 私有就是用 private 来修饰的. 非私有的修饰符包括 public 等等, 它们统称为非私有, 它们在是否能被子类继承这方面的规则是相同的.

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

如果子类可以继承父类的构造方法, 则在子类中, 违反了构造方法的方法名必须和所在类的类名相同的原则.
构造方法不论是私有还是非私有, 都不能被子类所继承. 如果构造方法可以继承, 那么在子类中的构造方法的方法名, 将和父类的类名相同, 然而子类的构造方法的方法名, 必须和子类的类名相同, 于是引起了冲突.
子类中所有的构造方法默认先访问父类中的无参构造, 再执行自己. 因为子类在初始化的时候, 有可能会使用到父类中的数据, 如果父类没有完成初始化, 子类将无法使用父类的数据. 因此, 子类的所有的构造方法都默认先执行父类的无参构造, 先完成父类数据空间的初始化, 且是默认初始化 (因为是无参构造) .子类虽然不能继承父类的构造方法, 但是可以用 super
调用父类的构造方法.
子类的无参构造方法和有参构造方法的第一行语句都是默认的 super();
语句, 即调用父类的无参构造, 即使不写也是存在的, 且必须在第一行. 子类中所有的构造方法默认先访问父类中的无参构造, 再执行自己这个构造方法体.
如果想要调用父类的有参构造, 必须手动写 super(参数);
进行调用.
在 super();
上面写语句会报错:

程序示例:
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 方法来访问.
在子类的带参构造中可以手动调用父类的带参构造给成员变量赋值, 示意图:

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

首先是测试类 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);
.
总结起来, 和没有继承时, 内存的利用的不同点有:
-
加载类的字节码文件到方法区时, 会把父类的字节码文件也加载到方法区;
-
在堆内存中创建对象时, 会将小空间一分为二, 一部分空间是存储从父类继承下来的成员变量, 另一部分空间是存储子类本身的成员变量;
-
在利用对象名称去调用成员变量时, 在堆内存中寻找成员变量时, 优先寻找子类本身的成员变量, 找不到的话再去寻找从父类继承下来的成员变量.
当 mian 方法执行完毕, main 方法从栈中出去, 一旦方法出栈了, 方法里面的变量也就消失了, 变量消失了, 针对堆内存中的对象而言, 就没有变量再使用它了, 这块堆内存就变成垃圾, 虚拟机里面有垃圾回收器, 会在合适的时候, 将这些用不到的垃圾进行清理. 这个清理是自动的, 不需要我们管. 我们只需知道, 一旦对象变为垃圾, 我们就不能再去使用了.
成员变量的继承内存图:
注意, 这里父类的成员变量私有的.

在执行到语句 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 修饰的方法.
一个类里面提供了一个虚方法表, 存放了这个类中的所有虚方法. 如果这个类有子类, 则这个类将自己的虚方法表复制一份交给子类, 子类的虚方法表是从父类继承来的虚方法加上自己的虚方法所构成.

在 A 类中, 如果调用 C 类的虚方法, 那么直接在自己的虚方法表中查找就能找到, 如果要调用 C 类的虚方法之外的方法, 那么就要先去 B 类中查找, 如果没有找到, 就再到 C 类中去查找.
有了虚方法表, 程序的性能会大大提高.
方法重写的关键点也在虚方法表中.
父类的方法中, 只有虚方法才能被子类继承, 父类中的其他方法是不会被子类继承下来的.
子类执行一个方法时, 如果是父类中的虚方法, 那就到子类自己的虚方法表中去找这个方法, 如果不是父类的虚方法, 那么这个方法就不会被继承到子类中, 然后就去子类自己的空间中去找这个方法, 如果找到了, 那就执行这个方法, 如果没找到, 那就要一层一层往上到父类中去找这个方法, 如果找到了这个方法且方法不是私有的, 那么就再去执行这个方法, 如果父类中这个方法是私有的, 那么用对象加点号直接访问的方式是无法访问这个私有方法的, 程序报错.
Object 类有 5 个虚方法.
示例:

首先 TestStudent.class
字节码文件先被加载进方法区, 接着 main 方法进栈.
然后执行语句 Zi z = new Zi();
, 用到了 Zi 类, 则 Zi.class 加载进方法区. 由于 Zi 类继承自 Fu 类, 则 Fu.class
加载进栈中. 由于 Fu 类继承自 Object, 则 Object.class
加载进栈中. 方法区中存放了类的全部成员变量和成员方法, 此处只关注成员方法.
Object 类有 5 个虚方法, 到了 Fu 类中就有了 6 个虚方法, 到了 Zi 类中就有了 7 个虚方法. 至此, 整个字节码文件才算是加载完毕.

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

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

执行语句 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.
子类重写父类方法时, 返回值类型必须小于等于父类. 返回值类型也可能是自定义的某个类, 父类的返回值类型必须是子类的返回值类型的父类或者是相同类型.
建议: 重写方法时, 尽量和父类保持一致.
方法重写的本质是覆盖了从虚方法表中继承过来的方法. 如果一个方法不能加到虚方法表中, 那么这个方法就不能被重写. 只有能够被添加到虚方法表中的方法才能够被重写.

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

重写父类方法时, 也要分为需要调用父类方法, 在父类方法的基础上进行修改, 或者完全不需要用到父类方法.
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 代表父类的存储空间.

在无参构造方法中, 可以用 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 类
- 经理
成员变量: 工号, 姓名, 工资, 管理奖金
成员方法: 工作 (管理其他人) , 吃饭 (吃米饭)- 厨师
成员变量: 工号, 姓名, 工资
成员方法: 工作 (炒菜) , 吃饭 (吃米饭)
程序:
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 类中定义构造方法时的操作:
使用快捷键生成空参构造和全参构造:

练习:
带有继承结构的标准 Javabean 类
在黑马程序员中有很多员工 (Employee)
按照工作内容不同分教研部员工 (Teacher) 和行政部员工 (AdminStaff)
- 教研部根据教学的方式不同又分为讲师 (Lecturer) 和助教 (Tutor)
- 行政部根据负责事项不同, 又分为维护专员 (Maintainer) 和采购专员 (Buyer)
- 公司的每一个员工都编号, 姓名和其负责的工作.
- 每个员工都有工作的功能, 但是具体的工作内容又不一样.
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术