本文介绍了Java面向对象多态特性, 多态的介绍. 多态的实现条件–1.发生继承.2.发生重写(重写与重载的区别)3.向上转型与向下转型.4.静态绑定和动态绑定5. 实现多态 举例总结多态的优缺点 避免在构造方法内调用被重写的方法…
Java面向对象:多态特性的学习
- 一.什么是多态?
- 二.多态实现条件
- 1.认识多层继承
- 2.认识重写
- ①.重写和重载的区别
- 3.向上转型和向下转型
- ①.认识向上转型
- ②.认识向下转型
- 4.静态绑定和动态绑定
- ①.认识静态绑定
- ②.认识动态绑定
- 5.多态的实现
- 三.多态的优缺点
- 四.避免在构造方法内调用被重写的方法
一.什么是多态?
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成某一个行为时会产生出不同的状态。
彩色打印机 和黑白打印机 都具有打印行为,它们分别去打印图片,最后会产生不同的状态
总的来说:同一件事情,发生在不同对象身上,就会产生不同的结果。
二.多态实现条件
在java中要实现多态,必须要满足如下几个条件,缺一不可:
- 必须在继承体系下(多个子类继承一个父类)
- 发生向上转型(父类引用接受子类对象地址)
- 子类必须要对父类中方法进行重写(子类里有父类同名的方法)
- 动态绑定(通过父类的引用调用被重写的方法)
多态体现:在代码运行时,当父类引用接收不同子类对象时,调用父类方法实际会运行对应不同子类中的重写的该父类方法
1.认识多层继承
在这篇博客中讲到了继承->继承特性的学习
在继承体系下 也就是要有一个父类 其派生出多个子类,或者子类还派生出自己的子类
发生多态前提至少要有两个子类,才能体现出不同的状态!
示例:
class Animal{ //父类 动物类String name;int age;void eat(){System.out.println(this.age+"岁的"+this.name+"正在吃食物");}Animal(String name,int age){this.name=name;this.age=age;}Animal(){this.eat(); }
}
class Dog extends Animal{ //子类 :狗类继承动物类@Override //注解 表示下面的方法需要重写父类的方法 如果达到重写条件会报错, 帮你检查重写的错误void eat(){System.out.println(this.age+"岁的"+this.name+"正在吃狗粮");}}void bark(){System.out.println(this.age+"岁的"+this.name+"正在犬吠");}Dog(String name, int age){super(name,age);}Dog(){super();}
}
class Bird extends Animal{ //子类: 鸟类继承动物类@Overridevoid eat(){System.out.println(this.age+"岁的"+this.name+"正在吃稻谷");}void fly(){System.out.println(this.age+"岁的"+this.name+"正在飞");}Bird(String name,int age){super(name,age);}
}
class Huskies extends Dog{ //子类: 哈士奇类继承狗类 狗类又继承动物类 (多层继承)Huskies(String name, int age){super(name,age);}void pullOf(){System.out.println(this.age+"岁的"+this.name+"正在拆家");}@Overridevoid eat(){System.out.println(this.age+"岁的"+this.name+"正在吃主人食物");}Huskies(){super();}
}
上面代码实现了一个Animal父类有两个子类 Dog类和Bird类 而Dog类还有子类Huskies类
在此继承体系上可以发生多态,但是下一步还需满足 子类重写父类的方法,可以看到上面代码子类里有和父类同名的方法也就是发生了重写,而重写具体是什么呢?
2.认识重写
重写(override):也称为覆盖。重写是子类对父类非静态、非private修饰,非final修饰,非构造方法等的实现过程进行重新编写, 返回值和形参都不能改变。 即外壳不变,核心重写!
重写的好处在于子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要重新改写父类的方法。
注意:
重写针对的是方法而不是成员变量!
被静态static修饰的方法是属于类不属于对象,所以不能被继承更不能重写.
被private修饰的成员方法,虽然子类能够继承一份,但是没有访问权限,子类并不能重写此方法!
被final修饰的成员方法为密封方法,限制是不能被重写的,即虽然被继承,也能被访问,但是子类不同出现和其同名的方法!
构造方法是当前类对象特有的,是为当前对象成员初始化的,不会被继承到子类中所以不能被重写
【方法重写的规则】
子类在重写父类的方法时,一般必须与父类方法原型一致: 返回值类型 方法名 (参数列表) 要完全一致
特殊情况: 被重写的方法返回值类型和父类可以不同,但是必须是父子关系,即父类内方法返回值是父类类型,子类重写的方法返回值需是其子类类型 , 这种用法也叫作协变类型
子类访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类方法被public修饰,则子类中重写该方法就不能声明为 protected(子类访问权限必须大于等于父类访问权限)
父类被static、private修饰的方法、构造方法都不能被重写。
重写的方法, 可以使用 @Override 注解来显式指定. 有了这个注解能帮我们进行一些合法性校验. 例如不小心将方法名字拼写错了
(比如写成 aet), 那么此时编译器就会发现父类中没有 aet 方法, 就会编译报错, 提示无法构成重写.
当发生重写后,子类对象调用的子类和父类同名方法执行的便是子类方法,同时多个不同子类都可以有自身和父类同名的方法,即每个子类都可以重写父类的方法,这也是为实现多态的前提…
【重写的设计原则】
对于已经投入使用的类,尽量不要进行修改。最好的方式是:重新定义一个新的类,来重复利用其中共性的内容,并且添加或者改动新的内容。自身可以对投入的类里已有的方法进行重写,在保留原有的方法下,改写方法体…
例如:若干年前的手机,只能打电话,发短信,来电显示只能显示号码,而今天的手机在来电显示的时候,不仅仅可以显示号码,还可以显示头像,地区等。
在这个过程当中,我们不应该在原来老的类上进行修改,因为原来的类,可能还在有用户使用,
正确做法是:新建一个新手机的类,对来电显示这个方法重写就好了,这样就达到了我们当今的需求了同时之前的需求也不会消失。
①.重写和重载的区别
重写和重载看起来差不多,都是具有两个以上的具有相同的方法名,但其他地方有很多不同
区别点 | 重写 (override) | 重载 (override) |
---|---|---|
继承关系 | 必须要发生继承 要在子类和父类里 | 继承和非继承下都可以 |
是否依赖于对象 | 需要是成员方法且 非private 非static 非final 非构造 | 不依赖对象,只要有访问权限其他情况都可以 |
参数列表(类型,个数,顺序) | 必须一致 | 必须修改 三者至少要有一个不同 |
返回类型 | 不能修改【除非可以构成父子类关系】 | 可以修改 |
访问限定符 | 子类权限一定要大于等于父类权限 | 可以不同但是要有访问权限 |
重写要求比重载更严格,重载一般发生在一个类里(也可以在父子类) 重写一定要在父子类
即:方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现。
3.向上转型和向下转型
向上转型和向下转型都是发生在继承关系下
①.认识向上转型
向上转型:实际就是创建一个子类对象,将其当成父类对象来使用。
狗类当做动物来看待,那么狗只能做动物的行为,不能再做狗自己的行为
即 用父类引用 来接受子类对象地址
语法格式:父类类型 对象名 = new 子类类型();
Animal animal = new Dog("小白",3); // 动物类 引用 接收 实例化的狗类对象
animal是父类类型,但可以引用其子类对象,
子类对象看成是具有继承的父类部分和自身特有的部分,而父类对象只存在自身特有的部分或者继承其自己父类的部分 相当于父类对象部分是子类对象的子集
而发生向上转型就是子类对象抛弃掉子类自己特有的, 此时父类引用也就只能访问父类本身特有的部分或者父类继承以上父类的部分,不能再访问子类自身特有的部分
从类和对象角度来看, 父类的范围比子类更广, 狗类和猫类都是动物类,所以狗类和猫类都可以转型为动物类
【向上转型使用场景】
- 直接赋值
Animal animal1=new Dog("大白",4); //父类引用 接受子类对象地址 发生向上转型
- 方法传参
void func1(Animal animal){System.out.println(animal.name);//发生向上转型 只能访问子类继承的父类自己的部分 此时name在之前也被修改了 }//....Dog dog=new Dog("小白",3); //狗类对象func1(dog); //dog是子类引用存放子类对象地址 传参过去被父类引用接受 发生向上转型
- 方法返回
static Animal func2(){return new Dog("小黄",5); //返回的是子类对象 但是通过返回类型是Animal 在返回的时候会编译器会将其转换为Animal类型 也是向上转型}
向上转型的优点:让代码实现更简单灵活。当一个方法内可能会出现不同子类对象地址,但返回值类型只能有一个,而使用到向上转型即可以使不同子类对象转型为其父类对象返回
向上转型的缺陷:不能调用到子类特有的方法。
向上转型后,其只能访问当前子类对象里继承父类引用其自身成员以及其父类以上的父类成员,不能再访问子类对象自身的方法
如果向上转型后要想访问到子类特有的方法还需要用到向下转型…
②.认识向下转型
将一个子类对象经过向上转型之后当成父类方法使用,再无法调用子类的方法,但有时候可能需要调用子类特有的方法,此时:将父类引用再还原为子类对象即可,即向下转换。
向上转型由子类转为父类,而子类本身就是由父类派生进化而来,狗类对象转换为动物是安全的.
但是向下转型是由父类转换为子类,其前提必须得先发生向上转型,且向下转型的类型必须是对应的向上转型的子类,不能是其他子类,
哈士奇类转换为动物.其向下转型必须是动物转换为狗类或者哈士奇类,但是不能是动物转换为猫类… 所以向下转型也会有不安全的情况
public static void main(String[] args) {Animal animal=new Dog("小白",1); //发生向上转型Dog dog=(Dog) animal; //通过强转 发生向下转型 dog.eat();//可以访问狗类自己的行为
// 上面这种写法虽然可以,但是这样向下转型不安全↓!!Dog dog1=(Dog)new Animal("小狗",2);dog1.eat();
// 在未经过向上转型时 不能直接向下转型 !!!
//Animal animal1=new Bird("小飞",2);Dog dog3=(Dog)animal1;
// 在向上转型后 不能向下转型为其他不同子类类型!!!Animal animal2 =new Huskies("小哈",3);}
可以看到,当没有发生向上转型前 通过强转发生向下转型 和 发生向下转型 但是强转为另外的子类类型 都会抛出 ClassCastException --类型转换异常 …
所以 向下转型用的比较少,而且不安全,万一转换失败,运行时就会抛异常。Java中为了提高向下转型的安全性,引入了 instanceof 关键字,如果该表达式为true,则可以安全转换。
示例:
public static void main(String[] args) {
Animal animal2 =new Huskies("小哈",3);if(animal instanceof Dog){ // animal父类引用 指向的对象 是由Dog类向上转型//在向下转型之前 先进行判断 左边这个引用变量指向的对象是否是由右边类实例化的对象向上转型的 ,如果是则为true可以在里面进行强转 否则false跳过Dog dog1=(Dog) animal;dog.eat();}Animal animal1=new Bird("小飞",2);if(animal1 instanceof Dog){ //animal1 是由Bird类对象 向上转型 此处表达式为falseDog dog2=(Dog)animal1; //}if(animal2 instanceof Dog){// 虽然是哈士奇类转到动物类 但是期间经过了狗类也属于是狗类转型上去的 但是不能是转型的类的子类转型上去的Dog dog3=(Dog) animal2; // 当向下转型时如果转的类型是原来向上转型前的对象的父类又是当前父类引用的子类时也可以转,dog3.eat(); // 此时之前转型发生的重写 绑定关系不会变 dog类使用的eat方法是 哈士奇类的}
可以看到通过instanceof 关键字 来判断 父类引用内接受的是否是由对应子类向上转型而来的, 是则返回true 即可发生向下转型 不是则false跳过,避免了向下转型因为一些疏忽而造成不安全的行为抛出了异常
同时看到当huskies对象给 Animal 接受 虽然不是直接父类,但是其能转型为 huskies的父类->狗类 , 但调用其狗类的方法 发现执行的是huskies自身的方法,并不是狗类自身的,这里也就发生了下面要讲的 动态绑定!
4.静态绑定和动态绑定
①.认识静态绑定
静态绑定也称为前期绑定(早绑定),即在编译时,根据用户所传递实参类型就确定了具体调用那个方法。典型代表方法重载。
在此篇博客中讲到了方法重载->方法重载
学了方法重载后,可以知道即便有多个方法名相同的方法 编译器判定方法名实际上是根据方法签名,也就是最后的方法名,在编译期间就能确定所调用的是哪个重载方法,最后运行时即运行对应的方法体
②.认识动态绑定
动态绑定:也称为后期绑定(晚绑定),即在编译时,不能确定方法的行为,需要等到程序运行时,才能够确定具体调用那个类的方法。
动态绑定发生在继承关系,并且子类重写的父类的方法,而此时站在父类角度下调用父类的方法,最后并不会执行父类自身的方法,而会执行子类里重写的方法
class Animal{String name;int age;void eat(){System.out.println(this.age+"岁的"+this.name+"正在吃食物");}
}class Dog extends Animal{
void eat(){System.out.println(this.age+"岁的"+this.name+"正在吃狗粮");}
}//main...Animal animal1=new Dog("大白",4); //父类引用 接受子类对象地址 发生向上转型animal1.eat(); // 父类引用 调用父类的方法 当父类方法 在子类里被重写了 此时会发生动态绑定 调用的是被子类重写后的父类方法!
可以看到,当父类引用接受子类对象地址,调用父类方法时,本应该执行的是父类自身的方法xx岁在吃食物, 但是因为子类又重写的父类的此方法,而此时实际上运行的是子类重写的这个方法,执行到4岁大白正在吃狗粮 这也就是发生了动态绑定
而当在运行窗口 中通过反汇编指令 javap -c 字节码名 查看到 在编译阶段,实际上是执行的Animal 类的 eat方法 ,但是最后运行的是Dog类的eat方法
可以看到,动态绑定即在编译时并不确定要执行哪个方法,只有在运行程序的时候,才会知道具体是执行哪个方法,因为子类的方法重写了父类的方法,在运行时,虽然调用的是父类的方法,但是最后会执行子类里的方法体,这也是发生多态的最后一点…
5.多态的实现
实现多态需要满足上面的条件: 发生继承关系,发生子类重写父类方法 ,发生向上转型,发生动态绑定 ,通过一个父类引用接受不同子类对象的地址,使用父类引用调用父类被重写的方法,最后会运行不同子类所重写的父类的方法…
即多个对象执行同一个行为,呈现出不同的状态结果…
示例:
static void func(Animal animal){
//多态: 当一个父类引用 存放不同子类对象地址时,可以表现出不同的子类对象行为!!animal.eat();//调用父类自身的eat方法 执行的是不同子类自身的方法}
public static void main(String[] args) {Dog dog=new Dog("小狗",1);Bird bird=new Bird("小鸟",2);Huskies huskies=new Huskies("哈士奇",3);func(dog); //传狗类对象func(bird); //传鸟类对象func(huskies); //传哈士奇对象}
通过一个方法,一个形参,实现了不同对象的行为,
不同对象通过同一个行为,展现出了不同的状态,这便是多态!!!
三.多态的优缺点
假设有如下代码:
class Shape {
//属性....
public void draw() {
System.out.println("画图形!");}
}class Rect extends Shape{
@Override
public void draw() {
System.out.println("♦");}
}class Cycle extends Shape{
@Override
public void draw() {
System.out.println("●");}
}class Flower extends Shape{
@Override
public void draw() {
System.out.println("❀");}
}
当想输出对应的图像即实例化对应的对象然后调用其draw即可,
当我们想一次性输出多个不同的图形呢,我们可以写对应个数的实例化对象语句依次调用,但是这些重复性代码可以通过数组循环来优化↓
public static void drawShapes() {
Rect rect = new Rect(); //实例化 三个图形对象
Cycle cycle = new Cycle();
Flower flower = new Flower();
String[] shapes = {"cycle", "rect", "cycle", "rect", "flower"};//根据要输出的图形顺序规律对应创建一个字符串数组for (String shape : shapes) { //foreach 遍历字符串数组if (shape.equals("cycle")) { // 通过分支判断当前数组访问的是哪个字符串调用对应的对象方法输出对应的图形cycle.draw();
} else if (shape.equals("rect")) {
rect.draw();} else if (shape.equals("flower")) {
flower.draw();
}}}
上面代码 用到了数组 循环 分支 根据我们想要输出的图形顺序 调用不同对象的方法输出对应图形 , 后续还想输出已有的图形 只需在字符数组里增加字符串对象即可, 但是这种写法if–else较多,且要输出新图形时,还需要增加分支,从而使得代码可读性较差,扩展性也不高
当我们使用多态后↓
public static void drawShapes() {
// 我们创建了一个 Shape 对象的数组.
Shape[] shapes = {new Cycle(), new Rect(), new Cycle(),new Rect(), new Flower()};// Shape数组类型 根据顺序接受不同子类对象的地址// 即数组每个元素 父类引用变量都接受了子类对象地址发生向上转型
for (Shape shape : shapes) { //通过foreach遍历
shape.draw(); //每个父类引用 调用自身的draw方法 发生动态绑定执行子类的draw方法//实现了 调用同一个方法 执行不同对象的重写方法 展现出不同的结果
}
}
上面代码使用多态的思想也可以做到输出指定个数的图形,且想输出其它图形甚至新图形,新图型类需继承Shape类重写其draw方法后在数组位置新增子类对象地址即可,
最后通过父类引用调用同一个方法会执行其对应子类重写的方法, 简写了代码去除了大量的if else 代码可读性高, 扩展性强
【使用多态的好处】
- 能够降低代码的 “圈复杂度”, 避免使用大量的 if - else
什么叫 “圈复杂度” ?
圈复杂度是一种描述一段代码复杂程度的方式. 一段代码如果平铺直叙, 那么就比较简单容易理解. 而如 果有很多的条件分支或者循环语句,
就认为理解起来更复杂. 因此我们可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数, 这个个数就称为 “圈复杂度”.
如果一个方法的圈复杂度太高, 就需要考虑重构. 不同公司对于代码的圈复杂度的规范不一样. 一般不会超过 10
- 可扩展能力更强
如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低
对于类的调用者来说(drawShapes方法), 只要创建一个新类的实例就可以了, 改动成本很低. 而对于不用多态的情况, 就要把
drawShapes 中的 if - else 进行一定的修改, 改动成本更高.
多态缺陷:代码的运行效率降低。
- 属性没有多态性 当父类和子类都有同名属性的时候,通过父类引用,只能引用父类自己的成员属性
- 构造方法没有多态性
四.避免在构造方法内调用被重写的方法
我们创建两个类, B 是父类, D 是子类. D 中重写 func 方法. 并且在 B 的构造方法中调用 func 如下代码所示:
最后运行结果会是什么?
class B {
public B() {
// do nothing
func(); //构造方法内执行 子类和父类同名的方法
}
public void func() {
System.out.println("B.func()");
}}class D extends B {
private int num = 1;
@Override
public void func() {
System.out.println("D.func() " + num);
}}
public class Test {
public static void main(String[] args) {
D d = new D(); // 实例化D对象
}}
可以看到 最后输出了D.func() 0
在实例化子类对象D时,此时子类对象和父类对象有同名的方法此时已经发生了重写,
而在给子类构造前会先给父类构造,给父类构造调用父类构造方法 在里面又调用了func()方法
而此时站在父类的角度下调用子类和父类同名的方法(被子类重写的方法)实际上执行的是子类的方法,
而此时子类还没有构造, num虽然申请了空间 还没有就地初始化,所以里面是默认值0
最后输出了D.func()0
构造 D 对象的同时, 会调用 B 的构造方法. B 的构造方法中调用了 func 方法, 此时会触发动态绑定, 会调用到 D 中的func 此时 D 对象自身还没有构造, 此时 num 处在未初始化的状态, 值为 0. 如果构造方法具备多态性会先执行子类构造方法给num初始化,num的值应该是1.但是构造方法不具备多态性,
所以在构造函数内,尽量避免使用实例方法,除了final和private方法(使子类不能重写父类方法)。因为其在调用前可能被子类重写了,而导致动态绑定执行子类的方法
结论: “用尽量简单的方式使对象进入可工作状态”, 尽量不要在构造器中调用方法(如果这个方法被子类重写, 站在父类角度调用被重写的方法就会触发动态绑定执行子类的方法, 但是此时子类对象还没构造完成), 可能会出现一些隐藏的但是又极难发现的问题