多态 polymorphism
前言 : 本 主要内容: 多态
多态: 字面 意义 多种 形态 / 状态 ,
如 : 一个 人 可以 有多种 状态 : 开心 , 生气, 郁闷。
但如 过你 面试 的时候 面到多态的 问题 ,如果 只说出 这语句 化 多半 要 凉凉。
想要了解 多态 我们需要 先 了解 这个 几个 方法面
1.重写
2.向上转型
3.动态绑定
1.重写
这里 先来 看我们的 代码
class Animal{
public String name;
public int age;
public void eat(){
System.out.println(name + "吃饭");
}
}
class Dog extends Animal{
}
class Cat extends Animal{
}
public class Test {
}
这里 Dog 和 Cat 都 继承 了 Animal , 但 我们 觉得 此时 eat 方法 有点不合适 ,狗 吃 狗粮 ,猫 吃 猫粮 , 这里 的 eat 方法中的 吃饭 就有点 草率了 。
那么 我们 就需要 重新 在 Dog 和 Cat 类 中 将 eat 方法 进行 重写 ,来 完成我们 狗 吃 狗粮, 猫 吃 猫粮 的 操作 。
此时 我们 的 Dog 中 的 eat 方法 和 Animal 中 的 eat 方法 就构成 了 我们 的重写。
重写 的 要求 :
1.方法名相同
2.返回值 相同
3.参数列表相同(数据类型,个数,顺序 都得相同)
小细节 :
1.重写 的 方法 访问 权限不做 要求 ,但是 父类 的 访问权限 要 低于或等于 子类 。
2.被 private 修饰 是 不能 被 重写的
下面 就来演示 一下:
1.父类 是 public 子类必须是 public
2.父类 是 包访问 权限 (什么都没加)
3.被prviate
修饰 的 方法是 不能 被 重写 的
此时 聪明 的 同学 们 ,就开始 说 ,我直接 父类 加上 个, private
修饰 直接 子类访问 修饰 符 想要啥 要啥了吗 ?
可惜 很 抱歉 , 我们 的 方法 如果 被 private
修饰 是 不能够 重写的
另外 : 我们 除了 被 private 修饰 的 方法 不能 重写其实 还有 两种 方式 会 导致 父类 方法 不能够 重写 。
1.被 final 修饰 的 父类方法 也不能够 重写
演示:
这里 final 修饰 的 方法 我们 叫做 密封 方法 ,不能被 重写。
2.被 static 修饰的 父类方法 也不能够 重写
演示:
这里我们 的 注解 报错 了 , 这里 可以 有 人 会 想 我们 删掉 注解 不就 ok了。
可惜 , 这里 我们还是报错 了 。
这里 就需要我们 注意 : @Override 注解 。 有了这个注解能帮我们进行一些``合法性校验.`
这 被 static 修饰 重写 本身 就 是 不和 法 的 这里 注解 就会 进行 合法性 校验 , 发现 你这个 错误就 在 注解处报错 了 ,没有 注解 那么 就直接 在 方法上 报错了。
补充 : 重写 其实 可以 返回值 不同
我们 上面 刚刚 说了 重写 返回 值 相同 ,这里 就 说 返回值 可以 不同 ,真 的 绕来绕去 , 为啥 这里 又能 返回值 不同呢 。
这里 就来看一下 :
这里我们 的 返回值 可以 不一样 但是 我们的 返回值 之间 必须 构成父子类 关系。
上面 差不多就算 重写 的 全部 内容, 下面 来 看看文字 描述 。
重写(override):
也称为覆盖。重写是子类对父类非静态、非private修饰,非final修饰,非构造方法等的实现过程进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写!重写的好处在于子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法
下面 我们 来 看一下 面试 问题 : 重载 和 重写 的 区别 是 什么?
重载 | 重写 | |
---|---|---|
相同点 | 1.方法名称相同 | 1.方法名称相同 |
不同点 | 2.返回值不做 要求 | 2.返回值相同 构成父子类关系也可以 |
不同点 | 3.参数列表不同 (数据类型 顺序,个数 都不同) | 3.参数列表相同 (数据的 类型 ,顺序 ,个数,都相同) |
不同点 | 4.在同一个类中 或 不同类 中 都能构成重载 | 4.重写 一定是 发生在 继承层次上(两个 或 以上的 类) |
不同点 | 重载 是 没有权限 要求的 | 5.重写 是 有权限要求的 (子类的权限要大于等于 父类) 另外父类 被 private 修饰 是不能够重写的 |
知道了 重写 ,那么 我们 要如何 去设计 我们的 重写 呢 (换种 说法 啥时候 使用 重写) ?
重写的 设计 原则
这里我们 就来 看一下 我们的 【重写的设计原则】
对于已经投入使用的类,尽量不要进行修改。最好的方式是:重新定义一个新的类,来重复利用其中共性的内容,并且添加或者改动新的内容。
例如:若干年前的手机,只能打电话,发短信,来电显示只能显示号码,而今天的手机在来电显示的时候,不仅仅可以显示号码,还可以显示头像,地区等。在这个过程当中,我们不应该在原来老的类上进行修改,因为原来的类,可能还在有用户使用,正确做法是:新建一个新手机的类,对来电显示这个方法重写就好了,这样就达到了我们当今的需求了。
看完 设计 原则 , 下面 继续 。
这里先 抛出 两个 概念 ,先来 学习 一下 静态 绑定 , 动态绑定 在 讲完 向上 转换 讲解。
静态绑定:也称为前期绑定(早绑定),即在编译时,根据用户所传递实参类型就确定了具体调用那个方法。典型代表函数重载。
演示:
public class Test {
public static void func(){
}
public static void func(int a){
}
public static void func(int a, int b){
}
public static void main(String[] args) {
func();
func(1);
func(1,2);
}
}
这里我们编译的 时候 根据 你 传 入 的 参数,能够确定你 调用那个方法 , 这种 就叫做 静态绑定。
动态绑定:也称为后期绑定(晚绑定),即在编译时,不能确定方法的行为,需要等到程序运行时,才能够确定具体调用那个类的方法。
这里我们 要了解 动态绑定 就需要 先了解我们的 向上转型 。
2.向上转型
向上转型:实际就是创建一个子类对象,将其当成父类对象来使用。
下面 看代码 :
class Animal {
public String name;
public int age;
public void eat() {
System.out.println(name + "开行的 吃饭");
}
}
class Dog extends Animal{
public void func(){
System.out.println("Dog 特有的 方法");
}
}
public class Test {
public static void main(String[] args) {
Dog dog = new Dog();
// animal 这个 引用 指向了 dog 这个对象
Animal animal = dog;
}
}
这里 就是 我们的 向上转型。
注意: 此时 animal 这个引用 虽然 拿到 到了 dog 对象, 但是不能调用 Animal 类 中 没有 ,但 Dog 这个 类 中 特有 的 方法 或 属性。
可以 看到 我们 是不能 将 dog 这个 对象 的 func 方法 给调用出来的。
这里我们的 Animal 没有 func 这个 方法 所以我们就 访问不了 。
此时 我们 继续 。
下面就来 了解 我们的 动态绑定 :
概念 :
当 子类 重写 了 父类 的 方法 时 , 通过 父类 引用 引用了 子类 对象, 调用 这个被 重写 了 的 方法时 ,就会 发生我们的动态 绑定。
这里 我们 动态绑定 的 条件 :
分析 上面这句话 我们能得到几个 关键条件 ,
1.子类 重写父类 的 方法 .
2. 父类 引用了子类 对象 。
3.需要 调用 这个 重写的 方法
下面我们 通过 观察 反汇编 来 看看 动态绑定 。
下面 我们 继续 回到 我们的 向上转型 。
向上转型的 两种 方式 :
1.直接赋值 (刚刚我们写 的 就是 直接 赋值 )
2.方法 传参
class Animal {
public String name;
public int age;
public void eat() {
System.out.println(name + "开行的 吃饭");
}
}
class Dog extends Animal {
@Override
public void eat() {
System.out.println("重写 的 eat 方法");
}
public void func() {
System.out.println("Dog 特有的 方法");
}
}
public class Test {
public static void func(Animal animal){
animal.eat();
}
public static void main(String[] args) {
Dog dog = new Dog();
func(dog);
}
}
这里 方法 传值 的 过程 中 , 就发生 了 向上 转型 , (传入 的 对象 是 子类 ,方法 接收 的 是 父类 引用)。
多态
介绍 到现在 , 接下来 一个 代码 就能 让 大家 立马 知道 多态是 是什么 。
请看:
class Animal {
public String name;
public int age;
public void eat() {
System.out.println(name + "开行的 吃饭");
}
}
class Dog extends Animal {
@Override
public void eat() {
System.out.println("重写 的 eat 方法");
}
public void func() {
System.out.println("Dog 特有的 方法");
}
}
class Cat extends Animal {
@Override
public void eat() {
System.out.println("重写 的 eat 方法");
}
public void func() {
System.out.println("Cat 特有 的方法");
}
}
public class Test {
public static void func(Animal animal) {
animal.eat();
}
public static void main(String[] args) {
Dog dog = new Dog();
func(dog);
Cat cat = new Cat();
func(cat);
}
}
看完 上面的代码 再来 看 概念 是不是 就 非常 清楚 了
多态:
多态是同一个行为具有多个不同表现形式或形态的能力,
例如:黑白打印机和彩色打印机相同的打印行为却有着不同的打印效果,
- 对象类型和引用类型之间存在着继承(类)/ 实现(接口)的关系;
- 当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;如果有,再去调用子类的同名方法。
- 如果子类重写了父类的方法,最终执行的是子类覆盖的方法,如果没有则执行的是父类的方法。
补充 : 第三种 方法 产生 向上 转型 : 作为 返回值
class Animal {
public String name;
public int age;
public void eat() {
System.out.println(name + "开行的 吃饭");
}
}
class Dog extends Animal {
@Override
public void eat() {
System.out.println("Dog 中 重写 的 eat 方法");
}
public void func() {
System.out.println("Dog 特有的 方法");
}
}
public class Test {
// 第三种 向上 转型 , 作为 返回 值 。
public static Animal func(){
return new Dog();
}
}
这里我们 原本 返回的是 Animal 类 行 的 , 但 我们 返回 了 一个 Dog 子类 对象 ,这里 同样 也会 发生 向上 转型。
有 向上 转型,我们 还有 向下 转型 。
下面 就来 学习 一下 。
3.向下 转型
我们 前面 说过 ,向上 转型 的 引用 是 调用 不了 子类 特有的 属性 或 方法 的 , 那么 我们 要如何 去 调用 子类 特有 的 方法 呢? 这里 我们 就可 以 使用 向下 转型 。
使用 向下 转型 调用 Dog 中 特有 的 func 方法。
这里 就 是 我们 的 向下 转型 , 这里 我们 运行 下 。
这里 就 成功 的 调用 处 Dog 特有 的 特有的 方法 , 但 向下 转型 存在 不安全的 地方 , 这里我们 继续 来 举例子 ,
这里 我们 有 两个 类 , 一个 鸟 类 , 一个 狗 类 。
当你 完成 下面 操作的 时候 ,你 就会 发现 会报错 。
这里 我们 狗 会 想 鸟 一样飞吗 ? (除掉 那种 一生 只能 飞一次的 可能)。
原本 我们 向上转型 ,将 Dog 对象 给了 父类 Animal 的 引用, 然后 又 向下 转型 ,将 这个 引用 狗的 对象 , 赋值 给 Dog类型的 引用, 这样是不会 出错的 , 但 赋值 给 鸟 这个 类 的引用 ,此时 就会报错。 但是我们 每 编译 时 是 不能 发现 类型 不匹配 的 , 只有 编译 后 才能 发现 。
那么 要如何 解决 这个 问题 呢?
此时我们 就需要一个 关键字 来 帮助我们 来判断
关键字 : instanceof
最后 :我们 一般 不太 使用 向下 转系 了解 即可 。
关于构造方法的 坑
在 本文 的 最后 我们 来 看一下 一个 关于 构造 方法 的 坑 .
观察下面的代码 分析 最后输出 什么 :
class Animal {
public String name = "hellow";
public int age;
private int count;
public Animal(String name, int age) {
this.name = name;
this.age = age;
eat();
}
public void eat() {
System.out.println(name + "ani::eat()");
}
}
class Dog extends Animal {
public Dog(String name, int age) {
super(name, age);
}
@Override
public void eat() {
System.out.println(name + "狼吞虎咽的eat()");
}
}
public class TestDome {
public static void main(String[] args) {
Dog dog = new Dog("小黑", 18);
}
}
是 ani:: eat() 还是 狼吞虎咽 的 eat() 呢 ?
答案揭晓:
你的 答案 对 吗?
下面 就来 梳理 一下 。
本文结束 :下文 预告 抽象类 or 接口