实验7:子类与继承
7.1 实验目的
- 掌握类的继承的语法;
- 掌握在子类中用子类对象调用父类定义的成员方法;
- 学习子类与父类构造方法之间的关系;
- 掌握继承时方法的覆盖;
- 掌握抽象类的声明,以及子类中实现父类中的抽象方法;
7.2 实验内容
7.2.1 编写一个Java程序,声明一个Person类,成员变量为私有成员变量name,分别编写相应的set和get方法,声明一个Person类的子类Programmer,其私有成员变量为company(即所在公司),编写相应的set和get方法。新建一个test类,新建Programmer的对象,并利用set方法设置程序员的姓名和公司,利用get方法显示其姓名和公司。
【前提引入】
💬 谈谈继承
1️⃣ 介绍
继承可以解决代码复用,让编程更加靠近人类的思维,当多个类存在相同的属性(变量)和方法时,可以从这些类中抽象出父类,再弗雷中定义这些相同的属性和方法,所有的子类不需要重新定义这些属性和方法,只需要通过 extends
来声明继承父类即可。
2️⃣ 基本语法
class 子类 extends 父类{ }
3️⃣ 注意事项
- 子类继承了父类所有的属性和方法,非私有的属性和方法可以在子类直接访问,但私有属性和方法需要通过父类提供的公共方法间接访问
- Java所有类都是Object类的子类,Object类所有类的基类
- Java中的继承是
单继承机制
,注意一下,这个单继承机制指的是子类只能有一个直接父类,即一个类只能直接继承另一个类,但是可以有多个超类。
我们以本题为例子:Programmer的直接父类是Person,这满足单继承机制只有一个直接父类,但Programmer还有一个父类是Object类。无论你创建哪个类,都会给你默认继承Object这个顶级父类。我们可以证明它的继承: - 不能滥用继承,子类和父类之间必须满足
is-a
的关系,比如本题:Programmer
是一个``Person`
稍微补充:接口与实现接口的类是
like-a
的关系
【核心类代码】
🌿 Person类
public class Person { /** * 姓名 */ private String name; /** * 无参构造器 */ public Person(){ } public String getName() { return name; } public void setName(String name) { this.name = name; } }
🌿 Programmer类
public class Programmer extends Person { /** * 所在公司 */ private String company; /** * 无参构造器 */ public Programmer(){ } public String getCompany() { return company; } public void setCompany(String company) { this.company = company; } }
【运行流程】
public class Test { public static void main(String[] args) { Programmer programmer = new Programmer(); //设置姓名 programmer.setName("狐狸半面添"); //设置公司 programmer.setCompany("中南林业科技大学"); System.out.println(programmer.getName() + ":" + programmer.getCompany()); } }
7.2.2 为Person类添加一个有参构造方法:设置姓名,为子类Programmer新增一个有参构造方法,对name和company变量进行赋值。
【前提引入】
- 子类创建时,必须先调用父类的构造器,完成父类的初始化
- 当创建子类构造器的时候,不管使用子类的哪个构造器,默认情况下总会去调用父类的无参构造器,如果父类没有提供无参构造器,必须在子类的构造器中使用super去指定使用父类的哪个构造器完成父类的初始化工作,否则编译是不会通过的。
- super在使用的时候必须放在子类构造器的第一行
- 如果你不做任何处理,可以理解为在子类构造器的代码块中的第一行默认加上
super();
【核心类代码】
🌿 为Person类添加的有参构造方法
/** * 给name赋值的有参构造器 * @param name 姓名 */ public Person(String name){ this.name = name; }
🌿 为Programmer类添加的有参构造器
/** * 有参构造器:给name和company进行赋值 * * @param name 姓名 * @param company 公司 */ public Programmer(String name,String company){ //这里就是调用父类的构造器,括号中的就是实参列表,对应父类构造器的形参 super(name); this.company = company; }
7.2.3 为Person类中增加一个实例方法businessCard(),用以显示name,在test类中,增加一个Programmer对象的声明,调用businessCard()方法。
【前提引入】
子类继承了父类所有的属性和方法,因此可以在子类Pragrammer中使用父类Person的方法businessCard()方法
【核心类代码】
🌿 为Person类添加实例方法businessCard()方法
/** * 这是用于显示 name 的方法 */ public void businessCard(){ System.out.println("name = " + this.name); }
【运行流程】
public static void main(String[] args) { Programmer programmer = new Programmer(); programmer.setName("狐狸半面添"); //调用 businessCard() 方法 programmer.businessCard(); }
7.2.4 Programmer类中重写businessCard()方法,显示姓名和公司,运行test类查看结果。
【前提引入】
这里涉及了两个知识点:方法重写/覆盖 与 动态绑定机制
1️⃣ 方法重写/覆盖
- 简单来说,方法覆盖就是子类有一个方法,和父类的某个方法返回值类型、方法名、形参列表一样,那么我们就说子类的这个方法覆盖了父类的方法
- 子类方法的返回值类型和父类的返回值类型要一样,或者是父类返回类型的子类
- 不允许子类方法缩小父类的方法的访问权限与范围:
public > protected > default(默认) > private
- 举个简单例子:父类有个方法 A 的访问修饰符是protected,那么重写这个方法时应该将重写方法的访问修饰符设置为
protected
或者访问权限更大的public
2️⃣ 动态绑定机制
这里其实还涉及到多态:多态就是方法或对象具有多种形态,是面向对象的第三大特点,建立在封装与继承的基础之上。方法多态就是方法重写和方法重载,对象多态就与向上转型、向下转型有关啦。这里不详说,简单聊聊动态绑定机制。
- 当调用
对象方法
的时候,该方法会和该对象的内存地址/运行类型
绑定。
什么是运行类型,又什么是编译类型呢?(不细说)
举个例子:Person p = new Student();
我们说,Student类是student这个对象的运行类型,Person类是编译类型。你也可以简单记忆:编译类型看左边(左边是Person),运行类型看右边(右边是Student),这是在
父类引用指向子类对象
即向上转型
的情况下进行判定。
- 如下面代码中,
programmer.businessCard()
我们调用的是Programmer类重写的那个方法,而不是Person类的方法,这是为什么呢?我们用动态绑定机制来解释,programmer这个对象的运行类型就是Programmer类,因此调用的是Programmer类重写的那个方法。 - 当调用
对象属性
时,没有动态绑定机制,哪里声明,哪里使用。
【核心类代码】
🌿 为Programmer类添加重写方法businessCard()方法
/** * 重写方法,显示姓名和公司 */ @Override public void businessCard() { System.out.println("姓名:" + getName() + " 公司:" + this.company); }
【运行流程】
public static void main(String[] args) { Programmer programmer = new Programmer(); programmer.setName("狐狸半面添"); programmer.setCompany("中南林业科技大学"); //调用 businessCard() 方法 programmer.businessCard(); }
7.2.5 将Person类改变为抽象类,businessCard()方法改为抽象方法(注意抽象方法不能有方法体),运行test类查看结果。
【前提引入】
💬 聊聊抽象方法 与 抽象类
1️⃣ 为什么需要抽象方法与抽象类
当父类的某些方法需要声明,但是呢,有不确定这个方法该如何实现时,可以使用abstract
关键字来修饰这个方法,即不做实现,交给我们子类去实现。用abstract修饰的类就是抽象类。
2️⃣ 语法
访问修饰符 abstract class 类名{}
//抽象方法没有代码体,写完形参列表直接以分号结尾 访问修饰符 abstract 返回值类型 方法名(形参列表);
3️⃣ 注意事项
- 抽象类不能被实例化
- 除非子类也是抽象类,不然父类的抽象方法必须在子类得到实现,否则编译报错,这是个
强制的方法重写
, - 如果我们把一个方法声明为了抽象方法,那么这个方法所在的类我们必须声明为抽象类
- 抽象类中一定要有抽象方法吗?是不一定要有的,但该类仍不能被实例化
- 抽象方法不能使用
private
,final
和static
来修饰,因为这些关键字都与方法重写想违背,抽象方法的实现也是方法重写
4️⃣ 抽象类最佳实践 —— 模板设计模式
抽象类体现的就是一种模板设计模式,抽象类作为多个子类的通用模板,子类在抽象类的基本上进行扩展与改造,但子类总体上会保留抽象类的行为方式。
做个小小补充:
我在做Javaweb原生项目的时候,有这样一个问题,一种请求对应一个Servlet,造成Servlet太多了,不利于管理。怎么办呢?我们设置一个
BasicServlet
抽象类,继承HttpServlet,其他类都来继承这个BasicServlet,同时配置Servlet路径,采用反射+模板设计模式+动态绑定实现了一种请求对应了一个方法,让归属一个业务的请求都对应一个继承BasicServlet的子类。妙啊!
【核心类代码】
🌿 新的Person类
public abstract class Person { /** * 姓名 */ private String name; /** * 无参构造器 */ public Person(){ } /** * 给name赋值的有参构造器 * @param name 姓名 */ public Person(String name){ this.name = name; } /** * 这是用于显示 name 的方法 --> 现在我们做成了抽象方法 */ public abstract void businessCard(); public String getName() { return name; } public void setName(String name) { this.name = name; } }
🌿 新的Programmer类
public class Programmer extends Person { /** * 所在公司 */ private String company; /** * 无参构造器 */ public Programmer(){ } /** * 有参构造器:给name和company进行赋值 * * @param name 姓名 * @param company 公司 */ public Programmer(String name,String company){ super(name); this.company = company; } /** * 重写抽象方法,显示姓名和公司 */ @Override public void businessCard() { System.out.println("姓名:" + getName() + " 公司:" + this.company); } public String getCompany() { return company; } public void setCompany(String company) { this.company = company; } }
【运行流程】
public static void main(String[] args) { Programmer programmer = new Programmer(); programmer.setName("狐狸半面添"); programmer.setCompany("中南林业科技大学"); //调用 businessCard() 方法 programmer.businessCard(); }