回忆一下在面向对象编程一开始学习的时候我们提到过面向对象的特征:
- 封装,在面向对象中体现的是“归纳”和“信息的隐藏”,让我们能够把现实环境的复杂内容进行归类
- 继承,在面向对象中体现的是“复用性”和“is-a关联”,表达“重复”,能够让我们通过找到类型的共性 进行进一步提取和划分
- 多态,在面向对象中体现的是丰富度,多样性、可扩展性。面对丰富的和可能不断变化的问题域让程序能有更大的容纳性去模拟、适应变化
在面向对象特征中,封装和继承已经学习过了,其中还涉及到 “重载” 和 “重写” 的概念。封装和继承的概念在生活中还多多少少接触过,还能够做一定层度上的类推,但“多态”这个非生活用词就显得比较陌生了。多态实际上是生物学的词汇,在生物学中存在一个基本原则:一个生活或物种可以有多种不同的形式或阶段。
- 像比如说青蛙,小时候是蝌蚪,长大了成为了青蛙
- 南美种群中存在两种颜色的美洲虎:浅黄色的和黑色的
- 猫狗等都是动物,猫爱吃鱼腥,狗爱啃骨头
多态在面向对象中的含义则是指:相同的行为,不同的实现。相同的行为其本质上就是同名方法。不同的实现在于虽方法同名,但是各自有各自的方法实现体,也就是说 {}
及其里面的代码不同。
所以在 java 中,多态的实现其实就是一个父类带有一个方法,子类会根据各自需求重写这个方法。再使用这个父类型的引用就能调用所有子对象中被重写的这个方法,这样,一个父类型引用就可以根据它所指向的类,采取多种形式。
多态的分类
多态可以被分为 静态多态 与 动态多态,这里的“静态”和 static 关键字没有任何关系。
- 这里的“静”指的是在编译期就能够确定,哪种类型的对象,该对象的哪种行为。运行起来以后就能非常明确
- 实现形式:重载、重写
“动” 则指的是程序必须在运行期才能够根据具体绑定的对象类型从而知道要调用的是哪个方法
小类型的自动转换为大类型
- 大类型的要强制转换为小类型
这里的范围是必须在继承关系下进行探讨,父类(超类)代表的范围就要比子类(派生类)要大。只有这种情况下“大小”关系才是明确的,可以讨论的。
转换规则
自动类型转换 向上转型
在设计继承树时,都是把父类画在上,子类画在下,这种转型是沿着继承树往上走,所以又被称为向上转型。父类的引用指向子类对象一定成功的。
语法:
父类类型 对象变量名 = 子类对象;
Pet p = new Dog();
强制类型转换 向下转型
原理同样是与继承树相关,不多赘述。
子类类型 对象变相名 = (子类类型)new 父类();
Cat c = (Cat)new Pet(); // 报错 需要的类型: 子类 提供的类型: 父类
引用数据类型转换的本质
无论是强转还是自动转换,都不是改变对象本身,而只是变换一个类型的引用变量去指向这个对象。
编译或运行能否通过,依据是这个引用变量的类型和对象类型是否匹配。本质上只有两种情况是被允许的:
- 本类引用指向本类对象
- 父类引用指向子类对象
就像是我们说 “一个动物是一只猫对象” 这是完全正确的。
从内存的角度同样可以对此作出解释:每一个子类对象身上有一个完整的父类对象部分,所以用父类引用指过去是可以看到一个父类对象的完整内容的。
父类引用指向子类对象特点
父类引用指向子类对象后,只能看到来自于父类当中的属性或行为(受访问修饰符的限制)
// Parent.java 只保留关键代码
public class Parent {
public String name;
public int age;
public String gender = "male";
public int money = 500;
}
// Child.java 只保留关键代码
public class Child extends Parent {
public String job;
public Child(String name, int age, String gender) {
super(name, age, gender);
}
public void sing() {
System.out.println("我会中文说唱");
}
}
// TestMain.java
Parent c = new Child("zhangsan", 18, "male");
c.sing(); // 报错 无法解析 'Parent' 中的方法 'sing'
如果这个方法被子类重写了,那么父类引用看到的这个行为,执行的效果是子类重写后的效果
// Parent.java
public class Parent {
public String name;
public int age;
public String gender = "male";
public int money = 500;
public void sing() {
System.out.println("我会民谣");
}
}
// Child.java
public class Child extends Parent {
public String job;
public Child(String name, int age, String gender) {
super(name, age, gender);
}
public void sing() {
System.out.println("我会中文说唱");
}
}
// TestMain.java
Parent c = new Child("zhangsan", 18, "male");
c.sing(); // 我会中文说唱
多态前提条件
通过以上的内容我们可以得出多态存在 3 个前提条件:
- 必须存在继承关系
- 必须有方法重写
必须有父类引用指向子类对象
父类 引用变量 = new 子类();
父类引用指向子类对象后的弊端
由于变量的类型是父类类型,在用变量做
.
操作的时候,只能看到子类对象从父类继承而来的属性或方法,看不到子类特有的属性或方法。// Gun.java
public class Gun {
public String name;
public String address;
public Gun(String name) {
this.name = name;
}
}
// MachineGun.java
public class MachineGun extends Gun {
public int bulletCount;
public MachineGun(String name) {
super(name);
}
}
// TestMain.java
Gun gun4 = new MachineGun("机枪");
System.out.println(gun4.bulletCount); // 报错 无法解析符号 'bulletCount'
解决方案:第一步:通过强转,把父类的引用赋值给一个子类类型的变量。这样这两个引用都是指向的同一个对象,而父类类型变量能看到继承而来的属性和行为,子类类型的变量能看到所有的属性和行为。// TestMain.java
Gun gun4 = new MachineGun("机枪");
MachineGun gun = (MachineGun) gun4; // 强转
System.out.println("强转后:" + gun.bulletCount); // 0
第二步:强转是有风险的,而且引用类型的风险是运行时异常,它会导致程序停止运行直接报错。所以,在强转前,我们必须通过判断,保证传进来的是一个可以被强转的类型。如何保证呢?
instanceof
instanceof 是一个关键字,同时它也是一个 boolean 运算符,用于判断一个对象是否属于某个类型。
语法:对象 instanceof 类型
在强转之前,可以用 instanceof 做一次判断,确实该对象确实属于某个类型的时候,才做强转。
由于父类引用可以指向子类对象,所以当我们拿到一个父类引用的时候,并不知道它到底指的是那个类型的对象。有可能是父类对象,也有可能是子类 A 的对象,或子类 B 的对象,那么 instanceof 就可以帮我们判断它到底指的是谁。// TestMain.java
Gun gunObj = new MachineGun("机枪");
if (gunObj instanceof Rufle) {
MachineGun gun = (MachineGun) gunObj;
System.out.println("强转后:" + gun.bulletCount);
} else {
System.out.println("不同类型");
}
多态的应用
多态参数
当一个函数需要接收参数且为一个引用时,我们把形参的类型设计为父类类型,那么该父类下的所有子类对象都可以作为参数被传递进来。
// Solider.java 只保留了关键代码
public void fire(Gun gun) { // 形参以父类类型接受
System.out.println("士兵正使用[" + gun.name + "]进行攻击");
}
// TestMain.java
public class TestMain {
public static void main(String[] args) {
Solider solider = new Solider(); // 士兵实例
Gun gun1 = new Pistol("手枪");
Gun gun2 = new Rufle("步枪");
Gun gun3 = new Sniper("狙击");
Gun gun4 = new MachineGun("机枪");
solider.fire(gun1); // 士兵正使用[手枪]进行攻击
solider.fire(gun2); // 士兵正使用[手枪]进行攻击
solider.fire(gun3); // 士兵正使用[狙击]进行攻击
solider.fire(gun4); // 士兵正使用[机枪]进行攻击
}
}
异构集合
异构集合就是指不同对象的集合。
在前面的学习中,我们学过了数组这种集合方式,也知道数组有三大特点:只能存放同一类型的元素
- 空间大小一旦声明不可变更
- 在连续内存空间中存放元素
特点也是它的缺点,如果想要对数组进行修改,还要自己写方法来扩容缩容,就会很复杂。
例如当前我需要一个数组用来表示一个流浪动物收容中心,那么代码可能写作:
// 只保留关键代码
Cat[] cat = new Cat[100];
Dog[] Dog = new Dog[100];
// ... 可能会有其他动物
那么此时问题就出现了,每个类型 100 个引用会导致内存浪费,也不确定到底每个类型该有多少个元素。倘若新添加一个动物类型,还得修改代码,添加一个新数组。
多态就可以解决掉这个棘手的问题,我们可以将数组元素的类型声明为父类类型,那么该数组就可以装所有的子类对象了。
语法:
父类类型[] 数组名 = new 父类[长度];
数组名[下标] = 子类对象;
// 只保留关键代码
Animal[] animals = new Animal[5];
Cat cat1 = new Cat();
Cat cat2 = new Cat();
Dog dog1 = new Dog();
Dog dog2 = new Dog();
animals[0] = cat1;
animals[1] = cat2;
animals[2] = dog1;
animals[3] = dog2;
// 假设当前有人来喂流浪动物
Feeder feeder = new Feeder();
for(int i = 0; i < Animals.length; i++){
feeder.feedAnimal(Animals[i]);
}
当然更厉害的是 Object[]
,由于 Object 是所有类类型和数组类型的根类,因此除了基本数据类型的元素,其他的类型的对象都可以放进去。
// 示例
Object[] objects = new Object[10]; // 声明了一个长度为 10 的 Object 数组
// objects 数组中可以装任意引用类型元素
objects[0] = "hello";
objects[1] = new Scanner(System.in);
objects[2] = new MachineGun();
objects[3] = new Girl("安吉拉宝贝儿");
objects[4] = new int[4];
objects[5] = new Object();