继承、抽象类
1. 继承
1.1 概述
1.1.1 引入
- 假如我们要定义如下类:学生类,老师类和工人类,分析如下:
- 学生类
属性
:姓名,年龄行为
:吃饭,睡觉
- 老师类
属性
:姓名,年龄,薪水行为
:吃饭,睡觉,教书
- 班主任
属性
:姓名,年龄,薪水行为
:吃饭,睡觉,管理
- 学生类
- 如果我们定义了这三个类去开发一个系统,那么这三个类中就存在
大量重复
的信息(属性:姓名,年龄。行为:吃饭,睡觉)。这样就导致了相同代码大量重复,代码显得很臃肿和冗余
,那么如何解决呢?- 假如
多个类中存在相同属性和行为
时,我们可以将这些内容抽取到单独一个类中,那么多个类无需再定义
这些属性和行为,只要继承那一个类
即可。如图所示: - 其中,
多个类
可以称为子类
,单独被继承
的那一个类称为父类、超类
(superclass)或者基类
。
- 假如
1.1.2 继承的含义
继承
描述的是事物之间的所属关系
,这种关系是:is-a 的关系。例如,兔子属于食草动物,食草动物属于动物。可见,父类更通用,子类更具体
。我们通过继承,可以使多种事物之间形成一种关系体系。继承
:就是子类继承父类的属性和行为
,使得子类对象可以直接具有与父类相同的属性、相同的行为。子类可以直接访问父类中的非私有的属性和行为
。非私有哦~~~
1.1.3 继承的好处
提高代码的复用性
(减少代码冗余,相同代码重复利用)。使类与类之间产生了关系
。
1.2 继承的格式
- 通过
extends
关键字,可以声明一个子类继承另外一个父类,定义格式如下:
class 父类 {
...
}
class 子类 extends 父类 {
...
}
- 需要注意:
Java是单继承
的,一个类只能继承一个直接父类,跟现实世界很像,但是Java中的子类是更加强大的。
1.3 继承案例
1.3.1 案例
- 请使用继承定义以下类:
- 学生类
- 属性:姓名,年龄
- 行为:吃饭,睡觉
- 老师类
- 属性:姓名,年龄,薪水
- 行为:吃饭,睡觉,教书
- 班主任
- 属性:姓名,年龄,薪水
- 行为:吃饭,睡觉,管理
- 学生类
1.3.2 案例图解分析
- 老师类,学生类,还有班主任类,实际上
都是属于人类
的,我们可以定义一个人类,把他们相同的属性和行为都定义在人类中,然后继承人类即可,子类特有的属性和行为就定义在子类中了。 - 如下图所示。
1.3.3 案例代码实现
1.父类Human类
public class Human {
// 合理隐藏
private String name ;
private int age ;
// 合理暴露
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
2.子类Teacher类
public class Teacher extends Human {
// 工资
private double salary ;
// 特有方法
public void teach(){
System.out.println("老师在认真教技术!");
}
public double getSalary() {
return salary;
}
public void setSalary(double salary) {
this.salary = salary;
}
}
3.子类Student类
public class Student extends Human{
}
4.子类BanZhuren类
public class BanZhuRen extends Human {
// 工资
private double salary ;
// 特有方法
public void admin(){
System.out.println("班主任强调纪律问题!");
}
public double getSalary() {
return salary;
}
public void setSalary(double salary) {
this.salary = salary;
}
}
5.测试类
public class Test {
public static void main(String[] args) {
Teacher dlei = new Teacher();
dlei.setName("播仔");
dlei.setAge("31");
dlei.setSalary(1000.99);
System.out.println(dlei.getName());
System.out.println(dlei.getAge());
System.out.println(dlei.getSalary());
dlei.teach();
BanZhuRen linTao = new BanZhuRen();
linTao.setName("灵涛");
linTao.setAge("28");
linTao.setSalary(1000.99);
System.out.println(linTao.getName());
System.out.println(linTao.getAge());
System.out.println(linTao.getSalary());
linTao.admin();
Student xugan = new Student();
xugan.setName("播仔");
xugan.setAge("31");
//xugan.setSalary(1000.99); // xugan没有薪水属性,报错!
System.out.println(xugan.getName());
System.out.println(xugan.getAge());
}
}
1.3.4 小结
- 1.
继
承实际上是子类相同的属性和行为可以定义在父类中,子类特有的属性和行为由自己定义
,这样就实现了相同属性和行为的重复利用,从而提高了代码复用性
。 - 2.
子类继承父类
,就可以直接得到父类的成员变量和方法
。是否可以继承所有成员呢?请看下节!
1.4 子类不能继承的内容
1.4.1 引入
并不是父类的所有内容都可以给子类继承
的:- 1.
子类不能继承父类的构造器
,因为子类有自己的构造器。 - 2.值得注意的是
子类不能直接使用父类中私有的东西
,但是可以间接使用
- 1.
31.4.1 演示代码
public class Demo03 {
public static void main(String[] args) {
Zi z = new Zi();
System.out.println(z.num1);
// System.out.println(z.num2); // 私有的子类无法使用
// 通过getter/setter方法访问父类的private成员变量
System.out.println(z.getNum2());
z.show1();
// z.show2(); // 私有的子类无法使用
}
}
class Fu {
public int num1 = 10;
private int num2 = 20;
public void show1() {
System.out.println("show1");
}
private void show2() {
System.out.println("show2");
}
public int getNum2() {
return num2;
}
public void setNum2(int num2) {
this.num2 = num2;
}
}
class Zi extends Fu {
}
1.5 继承后的特点—成员变量
- 当类之间产生了继承关系后,其中各类中的成员变量,又产生了哪些影响呢?
1.5.1 成员变量不重名
- 如果子类父类中出现不重名的成员变量,这时的访问是没有影响的。代码如下:
class Fu {
// Fu中的成员变量
int num = 5;
}
class Zi extends Fu {
// Zi中的成员变量
int num2 = 6;
// Zi中的成员方法
public void show() {
// 访问父类中的num
System.out.println("Fu num="+num); // 继承而来,所以直接访问。
// 访问子类中的num2
System.out.println("Zi num2="+num2);
}
}
class Demo04 {
public static void main(String[] args) {
// 创建子类对象
Zi z = new Zi();
// 调用子类中的show方法
z.show();
}
}
- 演示结果:
Fu num = 5
Zi num2 = 6
1.5.2 成员变量重名
- 如果子类父类中出现
重名的成员变量
,这时的访问是有影响的。代码如下:
class Fu1 {
// Fu中的成员变量。
int num = 5;
}
class Zi1 extends Fu1 {
// Zi中的成员变量
int num = 6;
public void show() {
// 访问父类中的num
System.out.println("Fu num=" + num);
// 访问子类中的num
System.out.println("Zi num=" + num);
}
}
class Demo04 {
public static void main(String[] args) {
// 创建子类对象
Zi1 z = new Zi1();
// 调用子类中的show方法
z1.show();
}
}
- 演示结果:
- Fu num = 6
- Zi num = 6
子父类中出现了同名的成员变量时
,子类会优先访问自己对象中的成员变量。如果此时想访问父类成员变量如何解决呢?我们可以使用super
关键字。
1.5.3 super访问父类成员变量
- 子父类中出现了同名的成员变量时,在子类中需要访问父类中
非私有成员变量
时,需要使用super 关键字,修饰父类成员变量,类似
于之前学过的this
。 - 需要注意的是:
super
代表的是父类对象的引用
,this
代表的是当前对象的引用
。 - 使用格式:
super.父类成员变量名
子类方法需要修改,代码如下:
class Fu {
// Fu中的成员变量。
int num = 5;
}
class Zi extends Fu {
// Zi中的成员变量
int num = 6;
public void show() {
int num = 1;
// 访问方法中的num
System.out.println("method num=" + num);
// 访问子类中的num
System.out.println("Zi num=" + this.num);
// 访问父类中的num
System.out.println("Fu num=" + super.num);
}
}
class Demo04 {
public static void main(String[] args) {
// 创建子类对象
Zi1 z = new Zi1();
// 调用子类中的show方法
z1.show();
}
}
- 演示结果:
- method num=1
- Zi num=6
- Fu num=5
- 小贴士:
Fu 类中的成员变量是非私有的,子类中可以直接访问
。若Fu 类中的成员变量私有了,子类是不能直接访问的。通常编码时,我们遵循封装
的原则,使用private
修饰成员变量,那么如何访问父类的私有成员变量呢
?对!可以在父类中提供
公共的getXxx方法和setXxx方法
。
1.6 继承后的特点—成员方法
- 当类之间产生了关系,其中各类中的成员方法,又产生了哪些影响呢?
1.6.1 成员方法不重名
- 如果子类父类中出现不重名的成员方法,这时的调用是没有影响的。
对象调用方法时,会先在子类中查找有没有对应的方法,若子类中存在就会执行子类中的方法,若子类中不存在就会执行父类中相应的方法
。代码如下:
class Fu {
public void show() {
System.out.println("Fu类中的show方法执行");
}
}
class Zi extends Fu {
public void show2() {
System.out.println("Zi类中的show2方法执行");
}
}
public class Demo05 {
public static void main(String[] args) {
Zi z = new Zi();
//子类中没有show方法,但是可以找到父类方法去执行
z.show();
z.show2();
}
}
1.6.2 成员方法重名
- 如果
子类父类中出现重名的成员方法
(也就是重写
),则创建子类对象调用该方法的时候,子类对象会优先调用自己的方法
。 - 代码如下:
class Fu {
public void show() {
System.out.println("Fu show");
}
}
class Zi extends Fu {
//子类重写了父类的show方法
public void show() {
System.out.println("Zi show");
}
}
public class ExtendsDemo05{
public static void main(String[] args) {
Zi z = new Zi();
// 子类中有show方法,只执行重写后的show方法
z.show(); // Zi show
}
}
1.7 方法重写
1.7.1 概念
方法重写
:子类中出现与父类一模一样的方法
时(返回值类型,方法名和参数列表都相同
),会出现覆盖效果
,也称为重写
或者复写
。声明不变,重新实现。
1.7.2 使用场景与案例
- 发生在
子父类之间
的关系。子类继承了父类的方法,但是子类觉得父类的这方法不足以满足自己的需求
,子类重新写了一个与父类同名的方法,以便覆盖父类的该方 法。 - 例如:我们定义了一个动物类代码如下:
public class Animal {
public void run(){
System.out.println("动物跑的很快!");
}
public void cry(){
System.out.println("动物都可以叫~~~");
}
}
- 然后定义一个猫类,猫可能认为父类cry()方法不能满足自己的需求
- 代码如下:
public class Cat extends Animal {
public void cry(){
System.out.println("我们一起学猫叫,喵喵喵!喵的非常好听!");
}
}
public class Test {
public static void main(String[] args) {
// 创建子类对象
Cat ddm = new Cat();
// 调用父类继承而来的方法
ddm.run();
// 调用子类重写的方法
ddm.cry();
}
}
1.7.2 @Override重写注解
@Override
:注解,重写注解
校验!- 这个注解标记的方法,就说明这个方法必须是重写父类的方法,
否则编译阶段报错
。 - 建议重写都加上这个注解,一方面可以
提高代码的可读性
,一方面可以防止重写出错
! - 加上后的子类代码形式如下:
public class Cat extends Animal {
// 声明不变,重新实现
// 方法名称与父类全部一样,只是方法体中的功能重写写了!
@Override
public void cry(){
System.out.println("我们一起学猫叫,喵喵喵!喵的非常好听!");
}
}
1.7.3 注意事项
- 方法重写是发生在
子父类之间
的关系。 - 子类方法覆盖父类方法,必须要保证
权限大于等于父类权限
。 - 子类方法覆盖父类方法,
返回值类型、函数名和参数列表
都要一模一样
。
1.8 继承后的特点—构造器
1.8.1 引入
- 当类之间产生了关系,其中各类中的构造器,又产生了哪些影响呢?首先我们要回忆两个事情,构造器的定义格式和作用。
构造器
的名字是与类名一致
的。所以子类是无法继承
父类构造方法的。构造器
的作用
是初始化对象成员变量数据的
。所以子类的初始化过程中,必须先执行父类的初始化动作。子类的构造方法中默认有一个super() ,表示调用父类的构造方法,父类成员变量初始化后,才可以给子类使用
。(先有爸爸,才能有儿子
)
- 继承后
子类构造器
特点:子类所有构造器的第一行都会先调用父类的无参构造器,再执行自己
1.8.2 案例演示
- 按如下需求定义类:
- 人类
- 成员变量: 姓名,年龄
- 成员方法: 吃饭
- 学生类
- 成员变量: 姓名,年龄,成绩
- 成员方法: 吃饭
- 人类
- 代码如下:
class Person {
private String name;
private int age;
public Person() {
System.out.println("父类无参");
}
// getter/setter省略
}
class Student extends Person {
private double score;
public Student() {
//super(); // 调用父类无参,默认就存在,可以不写,必须再第一行
System.out.println("子类无参");
}
public Student(double score) {
//super(); // 调用父类无参,默认就存在,可以不写,必须再第一行
this.score = score;
System.out.println("子类有参");
}
}
public class Demo07 {
public static void main(String[] args) {
Student s1 = new Student();
System.out.println("----------");
Student s2 = new Student(99.9);
}
}
- 输出结果:
父类无参
子类无参
----------
父类无参
子类有参
1.8.3 小结
子类构造器执行的时候
,都会在第一行默认先调用父类无参数构造器一次
。- 子类构造器的第一行都
隐含
了一个super()
去调用父类无参数构造器,super()可以省略不写
。
1.9 super(…)和this(…)
1.9.1 引入
- 请看上节中的如下案例:
class Person {
private String name;
private int age;
public Person() {
System.out.println("父类无参");
}
// getter/setter省略
}
class Student extends Person {
private double score;
public Student() {
//super(); // 调用父类无参构造器,默认就存在,可以不写,必须再第一行
System.out.println("子类无参");
}
public Student(double score) {
//super(); // 调用父类无参构造器,默认就存在,可以不写,必须再第一行
this.score = score;
System.out.println("子类有参");
}
// getter/setter省略
}
public class Demo07 {
public static void main(String[] args) {
// 调用子类有参数构造器
Student s2 = new Student(99.9);
System.out.println(s2.getScore()); // 99.9
System.out.println(s2.getName()); // 输出 null
System.out.println(s2.getAge()); // 输出 0
}
}
- 我们发现,
子类有参数构造器只是初始化了自己对象中的成员变量score,而父类中的成员变量name和age依然是没有数据的
,怎么解决这个问题呢,我们可以借助与super(...)
去调用父类构造器,以便初始化继承自父类对象的name和age.
1.9.2 super和this的用法格式
- super和this完整的用法如下,其中this,super访问成员我们已经接触过了。
-
this.成员变量→本类的
-
super.成员变量→父类的
-
this.成员方法名()→本类的
-
super.成员方法名()→父类的
-
- 接下来我们使用调用构造器格式:
- super(…)→调用父类的构造器,根据参数匹配确认
- this(…) →调用本类的其他构造器,根据参数匹配确认
1.9.3 super(…)用法演示
- 代码如下:
class Person {
private String name ="凤姐";
private int age = 20;
public Person() {
System.out.println("父类无参");
}
public Person(String name , int age){
this.name = name ;
this.age = age ;
}
// getter/setter省略
}
class Student extends Person {
private double score = 100;
public Student() {
//super(); // 调用父类无参构造器,默认就存在,可以不写,必须再第一行
System.out.println("子类无参");
}
public Student(String name , int age,double score) {
super(name ,age);// 调用父类有参构造器Person(String name , int age)初始化name和age
this.score = score;
System.out.println("子类有参");
}
// getter/setter省略
}
public class Demo07 {
public static void main(String[] args) {
// 调用子类有参数构造器
Student s2 = new Student("张三",20,99);
System.out.println(s2.getScore()); // 99
System.out.println(s2.getName()); // 输出 张三
System.out.println(s2.getAge()); // 输出 20
}
}
- 注意:
- 子类的每个构造方法中均有默认的super(),调用父类的空参构造。
手动调用父类构造会覆盖默认的super()
。 super() 和 this() 都必须是在构造方法的第一行,所以不能同时出现
。super(..)是根据参数
去确定调用父类哪个构造器的
。
- 子类的每个构造方法中均有默认的super(),调用父类的空参构造。
1.9.4 super(…)案例图解
父类空间优先于子类对象产生
- 在每次创建子类对象时,先初始化父类空间,再创建其子类对象本身。目的在于子类对象中包含了其对应的父类空间,便可以包含其父类的成员,如果父类成员非private修饰,则子类可以随意使用父类成员。代码体现在子类的构造器调用时,一定先调用父类的构造器。理解图解如下:
- 在每次创建子类对象时,先初始化父类空间,再创建其子类对象本身。目的在于子类对象中包含了其对应的父类空间,便可以包含其父类的成员,如果父类成员非private修饰,则子类可以随意使用父类成员。代码体现在子类的构造器调用时,一定先调用父类的构造器。理解图解如下:
1.9.5 this(…)用法演示
- this(…)
默认
是去找本类中的其他构造器
,根据参数来确定具体调用哪一个构造器。- 为了借用其他构造器的功能。
package com.han._08this和super调用构造器;
/**
* this(...):
* 默认是去找本类中的其他构造器,根据参数来确定具体调用哪一个构造器。
* 为了借用其他构造器的功能。
*
*/
public class ThisDemo01 {
public static void main(String[] args) {
Student xuGan = new Student();
System.out.println(xuGan.getName()); // 输出:徐干
System.out.println(xuGan.getAge());// 输出:21
System.out.println(xuGan.getSex());// 输出: 男
}
}
class Student{
private String name ;
private int age ;
private char sex ;
public Student() {
// 很弱,我的兄弟很牛逼啊,我可以调用其他构造器:Student(String name, int age, char sex)
this("徐干",21,'男');
}
public Student(String name, int age, char sex) {
this.name = name ;
this.age = age ;
this.sex = sex ;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public char getSex() {
return sex;
}
public void setSex(char sex) {
this.sex = sex;
}
}
1.9.6 小结
子类
的每个构造方法
中均有默认的super(),调用父类的空参构造
。手动调用父类构造会覆盖
默认的super()
。- super() 和 this() 都
必须是在构造方法的第一行,所以不能同时出现
。 - super(…)和this(…)是
根据参数去确定调用父类哪个构造器
的。 - super(…)可以调用父类构造器初始化继承自父类的成员变量的数据。
- this(…)可以调用本类中的其他构造器。
1.10 继承的特点
- Java只支持单继承,不支持多继承。
// 一个类只能有一个父类,不可以有多个父类。
class A {}
class B {}
class C1 extends A {} // ok
// class C2 extends A, B {} // error
- 一个类可以有多个子类。
// A可以有多个子类
class A {}
class C1 extends A {}
class C2 extends A {} - 可以多层继承。
class A {}
class C1 extends A {}
class D extends C1 {} - 顶层父类是Object类。所有的类默认继承Object,作为父类。
2. 抽象类
2.1 概述
2.1.1 抽象类引入
- 父类中的方法,被它的子类们重写,子类各自的实现都不尽相同。那么父类的方法声明和方法主体,只有声明还有意义,而方法主体则没有存在的意义了(因为子类对象会调用自己重写的方法)。换句话说,
父类可能知道子类应该有哪个功能,但是功能具体怎么实现父类是不清楚的
(由子类自己决定),父类完全只需要提供一个没有方法体的方法签名即可,具体实现交给子类自己去实现
。 抽象方法
:被abstract
关键字修饰的方法
- 特点 :
没有方法体
- 特点 :
抽象类
:被abstract
关键字修饰的类
- 特点 :
不能实例化
- 特点 :
2.2.1 抽象方法
- 使用
abstract
关键字修饰方法,该方法就成了抽象方法,抽象方法只包含一个方法名,而没有方法体
。 - 定义格式:
修饰符 abstract 返回值类型 方法名 (参数列表);
- 代码举例:
public abstract void run();
2.2.2 抽象类
-
如果一个类包含抽象方法,那么该类必须是抽象类
。注意:抽象类不一定有抽象方法,但是有抽象方法的类必须定义成抽象类。 -
定义格式:
abstract class 类名字 {
}
- 代码举例:
public abstract class Animal {
public abstract void run();
}
2.2.3 抽象类的使用
- 注意 : 由于
抽象类不能直接创建对象
, 我们不能直接使用
抽象类, 只能使用抽象类的子类 - 要求:
继承抽象类的子类必须重写父类所有的抽象方法
。否则,该子类也必须声明为抽象类。 - 代码举例:
// 父类,抽象类
abstract 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;
}
// 抽象方法
// 抽象方法必须要放在抽象类中
abstract public void work();
}
// 定义一个子类继承抽象类
class Manager extends Employee {
public Manager() {
}
public Manager(String id, String name, double salary) {
super(id, name, salary);
}
// 2.重写父类的抽象方法
@Override
public void work() {
System.out.println("管理其他人");
}
}
// 定义一个子类继承抽象类
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 Demo10 {
public static void main(String[] args) {
// 创建抽象类,抽象类不能创建对象
// 假设抽象类让我们创建对象,里面的抽象方法没有方法体,无法执行.所以不让我们创建对象
// Employee e = new Employee();
// e.work();
// 3.创建子类
Manager m = new Manager();
m.work();
Cook c = new Cook("ap002", "库克", 1);
c.work();
}
}
- 此时的方法重写,是子类对父类抽象方法的完成实现,我们将这种方法重写的操作,也叫做实现方法。
2.3 抽象类的特征
- 相对于普通类而言,
抽象类多了一个可以定义抽象方法的权限
- 相对于普通类而言,
抽象类不能直接实例化
2.4 抽象类的注意事项
- 关于抽象类的使用,以下为语法上要注意的细节,虽然条目较多,但若理解了抽象的本质,无需死记硬背。
抽象类不能创建对象
,如果创建,编译无法通过而报错。只能创建其非抽象子类的对象
。- 理解:假设创建了抽象类的对象,调用抽象的方法,而
抽象方法没有具体的方法体
,没有意义
。
- 理解:假设创建了抽象类的对象,调用抽象的方法,而
- 抽象类中,
可以有构造器
,是供子类创建对象时,初始化父类成员使用的。- 理解:
子类的构造方法中,有默认的super(),需要访问父类构造方法
。
- 理解:
- 抽象类中,
不一定包含抽象方法,但是有抽象方法的类必定是抽象类
。- 理解:
未包含抽象方法的抽象类
,目的就是不想让调用者创建该类对象
(比如工具类可以定义成抽象类
),通常用于某些特殊的类结构设计。
- 理解:
抽象类的子类,必须重写抽象父类中所有的抽象方法
,否则子类也必须定义成抽象类
,编译无法通过而报错。- 理解:假设不重写所有抽象方法,则类中可能包含抽象方法。那么创建对象后,调用抽象的方法,没有意义。
- 抽象类存在的
意义
是定义规则和规范,强制子类按照规则规范做事情
- 理解:抽象类定义抽象方法, 强制要求子类必须实现, 起到了定义规则和规范的效果
2.5 抽象类存在的意义
- 抽象类存在的
意义
就是定义规则和规范
, 让子类来去按照规则和规范编写代码.
2.6 案例
2.6.1 案例1
- 已知猫和狗都有姓名和年龄, 且都有吃饭的方法, 不同的吃: 猫吃鱼, 狗吃肉. 它们都有跑步的方法, 而且仅仅是跑步. 猫独有自己的抓老鼠(catchMouse)的功能, 狗独有自己的看门(lookHome)的功能. 请用所学, 模拟该知识.
2.6.2 案例2
- 咱们国家各个地区人们的风俗习惯不太一样, 大家都有姓名,年龄, 体重等属性, 但是北方地区和南方地区的饮食习惯不太一样, 南方喜欢吃大米, 北方喜欢吃面, 北方地区的各个省份结婚的流程,以及费用也不太一样,南方地区各个省份结婚的流程,以及费用也不太一样, 比如: 河南省份结婚费大约是万紫千红, 山东省份结婚费用大约是万紫千红一片绿. 江苏省份结婚费用大约是万紫千红一片绿一动一不动, 广东省份结婚费用大约是万紫千红一片绿一动二不动,请利用继承和抽象类来描述各国的人们