1 多态
1.1 概述
引⼊
多态是继封装、继承之后,⾯向对象的第三⼤特性。
⽣活中,⽐如跑的动作,⼩猫、⼩狗和⼤象,跑起来是不⼀样的。再⽐如⻜的动作,昆⾍、⻦类和⻜机,⻜起来也是不⼀样的。可⻅,同⼀⾏为,通过不同的事物,可以体现出来的不同的形态。多态,描述的就是这样的状态。
定义
多态:是指同⼀⾏为,具有多个不同表现形式 。
前提【重点】
- 继承或者实现【⼆选⼀】
- ⽅法的重写【意义体现:不重写,⽆意义】
- ⽗类引⽤指向⼦类对象【格式体现】
1.2 多态的实现
多态体现的格式:
当使⽤多态⽅式调⽤⽅法时,⾸先检查⽗类中是否有该⽅法,如果没有,则编译错误;如果有,执⾏的是⼦类重写后⽅法。
1.3 多态的好处
实际开发的过程中,⽗类类型作为⽅法形式参数,传递⼦类对象给⽅法,进⾏⽅法的调⽤,更能体现出多态的扩展性与便利。代码如下:
定义测试类:
public class Test {
public static void main(String[] args) {
// 多态形式,创建对象
Cat c = new Cat();
Dog d = new Dog();
// 调⽤showCatEat
showCatEat(c);
// 调⽤showDogEat
showDogEat(d);
/*
* 以上两个⽅法,均可以被showAnimalEat(Animal a)⽅法所替代
* ⽽执⾏效果⼀致
*/
showAnimalEat(c);
showAnimalEat(d);
}
public static void showCatEat(Cat c) {
c.eat();
}
public static void showDogEat(Dog d) {
d.eat();
}
public static void showAnimalEat(Animal a) {
a.eat();
}
}
由于多态特性的⽀持, showAnimalEat ⽅法的 Animal 类型,是 Cat 和 Dog 的⽗类类型,⽗类类型接收⼦类对象,当然可以把 Cat 对象和 Dog 对象,传递给⽅法。
当 eat ⽅法执⾏时,多态规定,执⾏的是⼦类重写的⽅法,那么效果⾃然与 showCatEa t、 showDogEat ⽅法⼀致, 所以 showAnimalEat 完全可以替代以上两⽅法。
不仅仅是替代,在扩展性⽅⾯,⽆论之后再多的⼦类出现,我们都不需要编写 showXxxEat ⽅法了,直接使⽤ showAnimalEat 都可以完成。
所以,多态的好处,体现在,可以使程序编写的更简单,并有良好的扩展。
1.4 引⽤类型转换
多态的转型分为向上转型与向下转型两种:
向上转型
向上转型:多态本身是⼦类类型向⽗类类型向上转换的过程,这个过程是默认的。
当⽗类引⽤指向⼀个⼦类对象时,便是向上转型。
向下转型
向下转型:⽗类类型向⼦类类型向下转换的过程,这个过程是强制的。
⼀个已经向上转型的⼦类对象,将⽗类引⽤转为⼦类引⽤,可以使⽤强制类型转换的格式,便是向下转型。
为什么要转型
当使⽤多态⽅式调⽤⽅法时,⾸先检查⽗类中是否有该⽅法,如果没有,则编译错误。
也就是说,不能调⽤⼦类拥有,⽽⽗类没有的⽅法。编译都错误,更别说运⾏了。这也是多态给我们带来的⼀点"⼩麻烦"。所以,想要调⽤⼦ 类特有的⽅法,必须做向下转型。
转型演示,代码如下:
定义类:
abstract class Animal {
abstract void eat();
}
class Cat extends Animal {
public void eat() {
System.out.println("吃⻥");
}
public void catchMouse() {
System.out.println("抓⽼⿏");
}
}
class Dog extends Animal {
public void eat() {
System.out.println("吃⻣头");
}
public void watchHouse() {
System.out.println("看家");
}
}
定义测试类:
public class Test {
public static void main(String[] args) {
// 向上转型
Animal a = new Cat();
a.eat(); // 调⽤的是 Cat 的 eat
// 向下转型
Cat c = (Cat)a;
c.catchMouse(); // 调⽤的是 Cat 的 catchMouse
}
}
转型的异常
转型的过程中,⼀不⼩⼼就会遇到这样的问题,请看如下代码:
public class Test {
public static void main(String[] args) {
// 向上转型
Animal a = new Cat();
a.eat(); // 调⽤的是 Cat 的 eat
// 向下转型
Dog d = (Dog) a;
d.watchHouse(); // 调⽤的是 Dog 的 watchHouse 【运⾏报错】
}
}
这段代码可以通过编译,但是运⾏时,却报出了 ClassCastException ,类型转换异常!这是因为,明明创建了Cat类型对象,运⾏时,当然不能转换成Dog对象的。这两个类型并没有任何继承关系,不符合类型转换的定义。
为了避免 ClassCastException 的发⽣,Java提供了
instanceof 关键字,给引⽤变量做类型的校验,格式如下:
所以,转换前,我们最好先做⼀个判断,代码如下:
public class Test {
public static void main(String[] args) {
// 向上转型
Animal a = new Cat();
a.eat(); // 调⽤的是 Cat 的 eat
// 向下转型
if (a instanceof Cat) {
Cat c = (Cat) a;
c.catchMouse(); // 调⽤的是 Cat 的 catchMouse
} else if (a instanceof Dog) {
Dog d = (Dog) a;
d.watchHouse(); // 调⽤的是 Dog 的 watchHouse
}
}
}
2 接⼝多态的综合案例
2.1 笔记本电脑
笔记本电脑(laptop)通常具备使⽤USB设备的功能。在⽣产时,笔记本都预留了可以插⼊USB设备的USB接⼝,但具体是什么USB设备,笔记本⼚商并不关⼼,只要符合USB规格的设备都可以。
定义USB接⼝,具备最基本的开启功能和关闭功能。⿏标和键盘要想能在电脑上使⽤,那么⿏标和键盘也必须遵守USB规范,实现USB接⼝,否则⿏标和键盘的⽣产出来也⽆法使⽤。
2.2 案例分析
进⾏描述笔记本类,实现笔记本使⽤USB⿏标、USB键盘
USB接⼝,包含开启功能、关闭功能
笔记本类,包含运⾏功能、关机功能、使⽤USB设备功能
⿏标类,要实现USB接⼝,并具备点击的⽅法
键盘类,要实现USB接⼝,具备敲击的⽅法
2.3 案例实现
定义USB接⼝:
interface USB {
void open(); // 开启功能
void close(); // 关闭功能
}
定义⿏标类:
class Mouse implements USB {
public void open() {
System.out.println("⿏标开启,红灯闪⼀闪");
}
public void close() {
System.out.println("⿏标关闭,红灯熄灭");
}
public void click() {
System.out.println("⿏标单击");
}
}
定义键盘类:
class KeyBoard implements USB {
public void open() {
System.out.println("键盘开启,绿灯闪⼀闪");
}
public void close() {
System.out.println("键盘关闭,绿灯熄灭");
}
public void type() {
System.out.println("键盘打字");
}
}
定义笔记本类:
class Laptop {
// 笔记本开启运⾏功能
public void run() {
System.out.println("笔记本运⾏");
}
// 笔记本使⽤usb设备,这时当笔记本对象调⽤这个功能时,必须给其传递⼀个符合USB规
则的USB设备
public void useUSB(USB usb) {
// 判断是否有USB设备
if (usb != null) {
usb.open();
// 类型转换,调⽤特有⽅法
if(usb instanceof Mouse) {
Mouse m = (Mouse) usb;
m.click();
} else if (usb instanceof KeyBoard) {
KeyBoard kb = (KeyBoard) usb;
kb.type();
}
usb.close();
}
}
public void shutDown() {
System.out.println("笔记本关闭");
}
}
测试类,代码如下:
public class Test {
public static void main(String[] args) {
// 创建笔记本实体对象
Laptop lt = new Laptop();
// 笔记本开启
lt.run();
// 创建⿏标实体对象
Usb u = new Mouse();
// 笔记本使⽤⿏标
lt.useUSB(u);
// 创建键盘实体对象
KeyBoard kb = new KeyBoard();
// 笔记本使⽤键盘
lt.useUSB(kb);
// 笔记本关闭
lt.shutDown();
}
}
3 final关键字
3.1 概述
学习了继承后,我们知道,⼦类可以在⽗类的基础上改写⽗类内容,⽐如,⽅法重写。那么我们能不能随意的继承API中提供的类,改写其内容呢?显然这是不合适的。为了避免这种随意改写的情况,Java提供了 final 关键字, ⽤于修饰不可改变内容。
final:不可改变。可以⽤于修饰类、⽅法和变量。
类:被修饰的类,不能被继承。
⽅法:被修饰的⽅法,不能被重写。
变量:被修饰的变量,不能被重新赋值。
3.2 使⽤⽅式
修饰类
查询API发现像 public final class String 、 public final class Math 、 public final class Scanner 等,很多我们学习过的类,都是被 final 修饰的,⽬的就是供我们使⽤,⽽不让我们所以改变其内容。
修饰⽅法
重写被 final 修饰的⽅法,编译时就会报错。
修饰变量
1. 局部变量 – 基本类型
基本类型的局部变量,被 final 修饰后,只能赋值⼀次,不能再更改。代码如下:
public class FinalDemo1 {
public static void main(String[] args) {
// 声明变量,使⽤final修饰
final int a;
// 第⼀次赋值
a = 10;
// 第⼆次赋值
a = 20; // 报错,不可重新赋值
// 声明变量,直接赋值,使⽤final修饰
final int b = 10;
// 第⼆次赋值
b = 20; // 报错,不可重新赋值
} }
根据 final 的定义,写法1报错!写法2,为什么通过编译呢?因为每次循环,都是⼀次新的变量c。这也是⼤家需要注意的地⽅。
2. 局部变量 – 引⽤类型
引⽤类型的局部变量,被 final 修饰后,只能指向⼀个对象,地址不能再更改。但是不影响对象内部的成员变量值的修改,代码如下:
3. 成员变量
成员变量涉及到初始化的问题,初始化⽅式有两种,只能⼆选⼀:
被final 修饰的常量名称,⼀般都有书写规范,所有字⺟都⼤写。
4 权限修饰符
4.1 概述
在Java中提供了四种访问权限,使⽤不同的访问权限修饰符修饰时,被修饰的内容会有不同的访问权限:
4.2 不同权限的访问能⼒
5 内部类
5.1 概述
什么是内部类
将⼀个类A定义在另⼀个类B⾥⾯,⾥⾯的那个类A就称为内部类,B则称为外部类。
成员内部类
成员内部类:定义在类中⽅法外的类。
访问特点
内部类可以直接访问外部类的成员,包括私有成员。
外部类要访问内部类的成员,必须要建⽴内部类的对象。
内部类仍然是⼀个独⽴的类,在编译之后会内部类会被编译成独⽴的.class⽂件,但是前⾯冠以外部类的类名和$符号 。
⽐如,Person$Heart.class
5.2 匿名内部类【重点】
匿名内部类:是内部类的简化写法。它的本质是⼀个 带具体实现的 ⽗类或者⽗接⼝的 匿名的 ⼦类对象。
开发中,最常⽤到的内部类就是匿名内部类了。以接⼝举例,当你使⽤⼀个接⼝时,似乎得做如下⼏步操作:
- 定义⼦类
- 重写接⼝中的⽅法
- 创建⼦类对象
- 调⽤重写后的⽅法
我们的⽬的,最终只是为了调⽤⽅法,那么能不能简化⼀下,把以上四步合成⼀步呢?匿名内部类就是做这样的快捷⽅式。
前提
匿名内部类必须继承⼀个⽗类或者实现⼀个⽗接⼝。
格式
使⽤⽅式
以接⼝为例,匿名内部类的使⽤,代码如下:
、
创建匿名内部类,并调⽤:
以上两步,也可以简化为⼀步,代码如下:
public class InnerDemo3 {
public static void main(String[] args) {
/*
* 创建匿名内部类,直接传递给showFly(FlyAble f)
*/
showFly(new FlyAble() {
public void fly() {
System.out.println("我⻜了~~~");
}
});
}
public static void showFly(FlyAble f) {
f.fly();
}
}
6 引⽤类型⽤法总结
实际的开发中,引⽤类型的使⽤⾮常重要,也是⾮常普遍的。我们可以在理解基本类型的使⽤⽅式基础上,进⼀步去掌握引⽤类型的使⽤⽅式。基本类型可以作为成员变量、作为⽅法的参数、作为⽅法的返回值,那么当然引⽤类型也是可以的。
6.1 class作为成员变量
使⽤ int 类型表示⻆⾊id和⽣命值,使⽤ String 类型表示名。此时, String 本身就是引⽤类型,由于使⽤的⽅式类似常量,所以往往忽略了它是引⽤类型的存在。如果我们继续丰富这个类的定义,给 Role 增加武器,穿戴装备等属性,我们将如何编写呢?
定义⻆⾊类:
class Role {
int id;
int blood;
String name;
// 添加武器属性
Weapon wp;
// 添加盔甲属性
Armour ar;
// 提供get/set⽅法
public Weapon getWp() {
return wp;
}
public void setWeapon(Weapon wp) {
this.wp = wp;
}
public Armour getArmour() {
return ar;
}
public void setArmour(Armour ar) {
this.ar = ar;
}
// 攻击⽅法
public void attack() {
System.out.println("使⽤" + wp.getName() + ", 造成" + wp.getHurt() + "点
伤害");
}
// 穿戴盔甲
public void wear() {
// 增加防御,就是增加blood值
this.blood += ar.getProtect();
System.out.println("穿上" + ar.getName() + ", ⽣命值增加" +
ar.getProtect());
}
}
测试类:
public class Test {
public static void main(String[] args) {
// 创建Weapon 对象
Weapon wp = new Weapon("屠⻰⼑", 999999);
// 创建Armour 对象
Armour ar = new Armour("麒麟甲", 10000);
// 创建Role 对象
Role r = new Role();
// 设置武器属性
r.setWeapon(wp);
// 设置盔甲属性
r.setArmour(ar);
// 攻击
r.attack();
// 穿戴盔甲
r.wear();
}
}
输出结果:
使⽤屠⻰⼑,造成999999点伤害
穿上麒麟甲,⽣命值增加10000类作为成员变量时,对它进⾏赋值的操作,实际上,是赋给
它该类的⼀个对象。
6.2 interface作为成员变量
接⼝是对⽅法的封装,对应游戏当中,可以看作是扩展游戏⻆⾊的技能。所以,如果想扩展更强⼤技能,我们在 Role 中,可以增加接⼝作为成员变量,来设置不同的技能。
定义接⼝:
// 法术攻击
public interface FaShuSkill {
public abstract void faShuAttack();
}
定义⻆⾊类:
public class Role {
FaShuSkill fs;
public void setFaShuSkill(FaShuSkill fs) {
this.fs = fs;
}
// 法术攻击
public void faShuSkillAttack() {
System.out.print("发动法术攻击:");
fs.faShuAttack();
System.out.println("攻击完毕");
}
}
定义测试类:
public class Test {
public static void main(String[] args) {
// 创建游戏⻆⾊
Role role = new Role();
// 设置⻆⾊法术技能
role.setFaShuSkill(new FaShuSkill() {
@Override
public void faShuAttack() {
System.out.println("纵横天下");
}
});
// 发动法术攻击
role.faShuSkillAttack();
// 更换技能
role.setFaShuSkill(new FaShuSkill() {
@Override
public void faShuAttack() {
System.out.println("逆转乾坤");
}
});
// 发动法术攻击
role.faShuSkillAttack();
}
}
输出结果:
发动法术攻击:纵横天下
攻击完毕
发动法术攻击:逆转乾坤
攻击完毕
我们使⽤⼀个接⼝,作为成员变量,以便随时更换技能,这
样的设计更为灵活,增强了程序的扩展性。接⼝作为成员变量时,对它进⾏赋值的操作,实际上,是赋
给它该接⼝的⼀个⼦类对象。
6.3 interface作为⽅法参数和返回值类型
当接⼝作为⽅法的参数时,需要传递什么呢?当接⼝作为⽅法的返回值类型时,需要返回什么呢?对,其实都是它的⼦类对象。 ArrayList 类我们并不陌⽣,查看API我们发现,实际上,它是 java.util.List 接⼝的实现类。所以,当我们看⻅ List 接⼝作为参数或者返回值类型时,当然可以将 ArrayList 的对象进⾏传递或返回。
请观察如下⽅法:获取某集合中所有的偶数。
定义⽅法:
public static List<Integer> getEvenNum(List<Integer> list) {
// 创建保存偶数的集合
ArrayList<Integer> evenList = new ArrayList<>();
// 遍历集合list,判断元素为偶数,就添加到evenList中
for (int i = 0; i < list.size(); i++) {
Integer integer = list.get(i);
if (integer % 2 == 0) {
evenList.add(integer);
}
}
/*
* 返回偶数集合
* 因为getEvenNum⽅法的返回值类型是List,⽽ArrayList是List的⼦类,
* 所以evenList可以返回
*/
return evenList; }
调⽤⽅法:
public class Test {
public static void main(String[] args) {
// 创建ArrayList集合,并添加数字
ArrayList<Integer> srcList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
srcList.add(i);
}
/*
* 获取偶数集合
* 因为getEvenNum⽅法的参数是List,⽽ArrayList是List的⼦类,
* 所以srcList可以传递
*/
List list = getEvenNum(srcList);
System.out.println(list);
}
}
接⼝作为参数时,传递它的⼦类对象。
接⼝作为返回值类型时,返回它的⼦类对象。