一、知识点
继承、重写、多态、抽象类、接口。
(Java语言三大特性:继承、封装、多态)
二、目标
-
深刻理解继承的作用。
-
掌握重写的规则。
-
深刻理解多态。
-
掌握抽象类与接口的区别。
三、内容分析
-
重点
-
继承、重写、多态、抽象类、接口。
-
-
难点
-
多态。
-
抽象类与接口的区别。
-
接口的作用。
-
四、内容
1、继承
继承是描述两个类的关系,如果两个类有重复的属性和方法,我们就可以使用继承的方式来实现/设计。
语法:
class 子类名称 extends 父类 {
属性名
方法名
}
-
继承中,子类会把父类的所有方法和属性都继承下来(除了private)。
-
子类继承父类后,还可以拥有自己的属性和方法。
-
java中,只能单继承。
// 人
public class Person {
public String name = "人";
// 0未知,1男,2女
public String sex = "0";
public void eat() {
System.out.println("吃饭");
}
}
// 中国人
public class ChinesePerson extends Person {
public String name = "中国人";
// 生肖
public String shengxiao;
public void kungfu() {
System.out.println("功夫");
}
}
// 测试
public static void main(String[] args) {
ChinesePerson chinesePerson = new ChinesePerson();
// 子类没有定义sex属性,但是也可以使用,因为从父类继承了
System.out.println(chinesePerson.sex); // 0
// 子类、父类可以有同名属性
System.out.println(chinesePerson.name); // 中国人
// 子类可以有自己的属性
System.out.println(chinesePerson.shengxiao); // null
// 子类没有定义eat方法,但是可以使用,因为从父类继承了
chinesePerson.eat();
// 子类可以有自己的方法
chinesePerson.kungfu();
}
2、super
super跟this比较
super所指向的是自身的父类
-
子类实例化的过程中,父类构造器先被调用,然后再调用子类的构造器(在子类构造器内部默认调用super())。
-
如果子类的构造器中调用了父类的有参构造方法,那么父类无参的构造器不会被调用。
-
super()的调用要放在第一行。
-
super可以表示父类的引用,我们可以使用super和this来区分父类和子类的同名属性和方法。
// 接着知识点1的案例
// Chinese添加方法test()
public void test() {
// 优先找当前类的name属性
System.out.println(name); // 中国人
// 优先找当前类的sex属性,找不到就到父类找
System.out.println(sex); // 0
// 使用this关键字指定获取子类还是父类的同名属性
System.out.println(this.name); // 中国人
System.out.println(super.name); // 人
// 方法和属性一样
super.eat();
}
3、重写
-
定义
重写(override):也称覆盖。重写是子类对父类非静态,非private,非final方法的实现过程进行重新编写,返回值(JDK7以后,被重写的方法返回值类型可以不同,但是必须是具有父子关系的)和形参都不能改变。即外壳不变,核心重写。
重载(overload):同一个类中,有相同的方法名,但是参数的类型或者参数的个数不同,这两个方法就是重载。
-
为什么需要重写
原有的方法无法满足新的需求,需要对这个方法进行改良来满足新的需求。子类可以根据需要,定义特定于自己的行为。既沿袭了父类的功能名称,又根据子类的需要重新实现父类方法,从而进行扩展增强。
示例:
// 人类
public class Person {
public String name = "人";
// 人都会吃饭,具体怎么吃饭的不清楚,每个区域的吃饭方式都不一样
public void eat() {
System.out.println("吃饭");
}
}
// 中国人
public class ChinesePerson extends Person{
public String name = "中国人";
// 对eat方法进行重写,延续了父类的动作,但是做了扩展,实现了具体的吃饭方式。
@Override
public void eat() {
System.out.println("使用筷子吃饭");
}
}
// 印度人
public class IndiaPerson extends Person{
public String name = "印度人";
@Override
public void eat() {
System.out.println("使用手吃饭");
}
}
4、多态
-
定义
同一个行为具有不同的表现形式。执行一段代码,Java 在运行时能根据不同的对象产生不同的结果。
多态不是一种新技术,而是设计模式,基于已有的技术实现的一种场景
-
前提条件
-
子类继承父类。
-
子类覆盖父类的方法(重写)。
-
父类引用指向子类对象。
-
示例
假设场景,学校食堂排队干饭。吃饭之前要先买饭,然后吃饭。
人都会吃饭,但是不同地区的人生活习惯不一样,吃饭方式也不一样。中国人使用筷子,英国人使用刀叉,印度人使用手。
-
封装Person类,设置方法eat()和buyFood();
-
再分别封装三个子类ChinesePerson、EnglishPerson、IndiaPerson;
-
子类重写eat()方法,因为每个人吃饭的方式可能不一样;
-
buyFood()不用重写,因为每个人都得排队买饭,行为是一样的。
// 人
public class Person {
public String name = "人";
// 人都会吃饭,具体怎么吃还不清楚
public void eat() {
System.out.println("吃饭");
}
public void buyFood() {
System.out.println("买饭");
}
}
// 中国人
public class ChinesePerson extends Person{
public String name = "中国人";
// 重写父类eat方法,实现具体的吃饭方式
@Override
public void eat() {
System.out.println(name + "用筷子吃饭");
}
}
// 英国人
public class EnglishPerson extends Person{
public String name = "英国人";
@Override
public void eat() {
System.out.println(name + "用刀叉吃饭");
}
}
// 用手吃饭
public class IndiaPerson extends Person{
public String name = "印度人";
@Override
public void eat() {
System.out.println(name + "用手吃饭");
}
}
// main函数测试1。
public static void main(String[] args) {
// 子类对象赋值给父类的引用(父类类型的变量)
Person person1 = new ChinesePerson();
Person person2 = new IndiaPerson();
// 执行的是子类的方法
person1.eat(); // 中国人用筷子吃饭
person2.eat(); // 印度人用手吃饭
}
// main函数测试2。实现食堂排队买饭场景。
// 我们封装ganfan()方法,过程为 买饭 + 吃饭。
public static void main(String[] args) {
// 使用数组模拟排队
Person[] persons = { new ChinesePerson(), new IndiaPerson(), new EnglishPerson(), new IndiaPerson() };
// 循环排队干饭
for(int i = 0; i < persons.length; i++) {
ganfan(persons[i]); // 运行过程中才确定是什么对象
System.out.println("-----------");
}
}
// 买饭 + 吃饭
// 干饭方法不需要具体知道是什么人吃饭
// 通过多态,实现不同的人使用不同的吃饭方式
public static void ganfan(Person person) {
person.buyFood();
person.eat();
}
注意:多态场景下,引用调用的方法是子类重写的方法,调用的属性是父类的属性
是什么?怎么用?有什么好处?
5、抽象类
5.1 定义
抽象类也是类,只是抽象类具备了一些特殊的性质。
我们以前编写一个类时,会为这个类编写具体的属性和方法,但有一些情况我们只知道一个类需要哪些属性方法,但不知道这些方法具体是什么,这时我们就要用到抽象类。
举个例子,有一位老师布置了一篇作文,要求以春天为题目写一篇字数不少于800字的写景作文,题材不限(诗歌除外)。在这个例子中,这篇作文就是一个抽象类,这个抽象类有两个属性:以春天为题目和字数不少于800,还有一个抽象方法:写景。现在全班学生就会按照老师所给的要求,即抽象类,去完成作业。抽象类就像一个大纲,一种规范。
抽象类体现的是一种模板式设计。以抽象类作为其子类的模板,从而避免了子类设计的随意性。
5.2 语法
关键字:abstract。
// 抽象类
public abstract class 类名 {
// 抽象方法
public abstract 返回值 方法名();
}
-
抽象类不能实例化,即不能创建对象,只能被继承,然后实例化。
-
继承抽象类的非抽象类必须实现抽象方法。
-
抽象类中可以没有抽象方法,有抽象方法的类一定是抽象类。
-
抽象类可以继承抽象类,可以不用实现抽象方法。
-
抽象的方法不能private,final,static修饰。
6、接口
6.1 定义
接口这个概念在生活中并不陌生,比如计算机往往有多个USB接口,可以插各种USB设备,如键盘、鼠标、U盘、摄像头、手机等。
接口声明了一组能力,但它自己并没有实现这个能力,它只是一个约定。接口涉及交互两方对象,一方需要实现这个接口,另一方使用这个接口,但双方对象并不直接互相依赖,它们只是通过接口间接交互,上面的USB接口来说,USB协议约定了USB设备需要实现的能力,每个USB设备都需要实现这些能力,计算机使用USB协议与USB设备交互,计算机和USB设备互不依赖,但可以通过USB接口相互交互。
6.2 作用
Java中是单继承的,怎么才能让类实现多继承呢?
(比如中国人继承人,同时还会功夫)
比如燕子会飞,飞机也会飞,他们具有相同的行为,但是他们是毫不相关的两个类,如果让燕子和飞机都继承于同一个抽象类,显然不合适。而且燕子会吃东西,飞机可以加油,这时候也不适合在同一个抽象类中定义让他们继承。
接口主要解决的问题:
-
多继承
-
不同子类实现相同的行为
6.3 语法
关键字:interface
interface 接口名 {
}
-
接口不能实例化,需要有类使用implements来实现接口,并且实现接口的抽象方法。
-
接口中的方法默认是抽象的。
-
接口的方法不能使用private和final、static修饰。
-
接口可以多继承。
-
接口可以定义常量,常量默认会带public static final 。该常量不能修改,一般我们通过【接口名.常量】来访问。
6.4 接口与抽象类的区别
-
一个类只能继承一个抽象类,而一个类却可以实现多个接口。
-
抽象类是对一种事物的抽象,即对类抽象,而接口是对行为的抽象。
示例:假设场景,家里的门。门都有open( )和close( )两个动作,此时我们可以通过抽象类和接口来定义这个抽象概念。
public abstract class Door {
public abstract void open();
public abstract void close();
}
public interface Door {
public abstract void open();
public abstract void close();
}
但是现在如果我们需要门具有报警alarm( )的功能,那么该如何实现?下面提供两种思路:
-
将这三个功能都放在抽象类里面,但是这样一来所有继承于这个抽象类的子类都具备了报警功能,但是有的门并不一定具备报警功能;
-
将这三个功能都放在接口里面,需要用到什么功能我们就实现什么接口。
从这里可以看出, Door的open() 、close()和alarm()根本就属于两个不同范畴内的行为,open()和close()属于门本身固有的行为特性,而alarm()属于延伸的附加行为。因此最好的解决办法是单独将报警设计为一个接口,包含alarm()行为,Door设计为单独的一个抽象类,包含open和close两种行为。
// 接口强调行为,报警的动作
public interface Alram {
void alarm();
}
// 抽象类强调事物,门具备开和关固有的行为特性
public abstract class Door {
void open();
void close();
}
// 如果是警报门。具备门基本的特征,所以继承Door。又具备报警的功能就实现Alarm接口进行扩展。
// 如果是普通门,把implements Alarm去掉即可。
class AlarmDoor extends Door implements Alarm {
void oepn() {
//....
}
void close() {
//....
}
void alarm() {
//....
}
}
// 假如是一个火灾警报器,不具备门的特性,我们直接实现Alarm接口就好了x
class FireAlarm implements Alarm {
void alarm() {
//....
}
}
6.5 JDK1.8之后(包括1.8)接口的变化
在jdk1.8之后接口可以定义static和default方法。
// 使用接口名.方法名调用
static void 方法名称() {
}
// 可以被子类继承,实例化子类之后调用
default void 方法名称() {
}
在1.8之前,接口与其实现类之间的耦合度太高,当需要为一个接口添加方法时,所有的实现类都必须随之修改。默认方法解决了这个问题,它可以为接口添加新的方法,而不会破坏已有的接口的实现。
五、小结
继续补充面向对象剩余知识点,重点理解多态的特性,以及普通类、抽象类、接口的区别。
六、练习
-
场景:公司给员工发工资。
-
Employee:所有员工总的父类。
属性:员工的姓名(String), 员工的生日月份(int)。
方法:double getSalary(int month),补贴工资,根据参数月份来确定工资,如果该月员工过生日,则公司会额外奖励100元。
-
SalariedEmployee:Employee的子类,拿固定工资的员工。
属性:月薪。
-
HourlyEmployee:Employee的子类,按小时拿工资的员工,每月工作超出160小时的部分按照1.5倍工资发放。
属性:每小时的工资、每月工作的小时数。
-
SalesEmployee:Employee的子类,销售人员,工资由月销售额和提成率决定。
属性:月销售额、提成率。
测试:创建SalariedEmployee对象,姓名:张三,生日月份:6,月薪:8000。
创建HourlyEmployee对象,姓名:李四,生日月份:12,每小时的工资:20,每月工作的小时数:180。
创建SalesEmployee对象,姓名:王五,生日月份:8,月销售额:1000000,提成率:0.05。
分别打印这三个员工的当前月工资(扩展:如何动态获取当前月份?不知道可以先假设当前月份是6月)。
2.moba游戏
假设正在开发一款moba游戏,设计一个抽象类 Hero 和一个接口 ISkill。
抽象类 Hero 包含通用属性如 name(名称)、level(等级)和 health(生命值),以及一个抽象方法 attack() 表示角色的攻击行为。
接口 ISkill定义了一个方法 useSkill(),表示角色使用技能的行为。
接下来,创建两个具体的英雄类 Houyi 和 Daji,它们都继承自 Hero 类并且实现 ISkill 接口。Houyi 类有一个射击技能 ShootSun,Daji类有一个魅惑术 Charm。