说来也惭愧,本博主继前一个博客到现在已经搁一个星期没写新的博客了,JavaSE语法部分还差一部分。
不能再只因为学校一些事情耽搁必须坚持下来的事情,接下来我会好好的更新博客,给各位看官提供更优质的文章,每周无论如何起码会发布3篇以上的博客。这也是对自己的一种鞭策吧,希望能和各位一起加油,一起努力。🐱🚀
在前几个章节,已经介绍完Java三大特性中的封装和继承,本章节将详细介绍三大特性中的最后一个---多态!
继承传送门:
我将把多态分为以下几点进行解剖和理解:
目录
一、多态
1、多态的概念
说到多态,按照文字的表面理解,我们通常会认为是”多种不同形态“。
具体点多态是当不同的对象去执行完成同一件事情时,会产生不同的状态。
比如现实生活中,猫和狗都是动物,它们会有一些共有的特性(饮食习惯,身体构造之类),虽说猫和狗一定会进食,但作为物种的不同,狗会吃狗粮,猫会吃猫粮;狗和猫都会叫,但是狗是”汪汪汪“地叫,而猫是”喵喵喵“地叫。
比如我们平时使用的打印机,打印机其中会有黑白打印机和彩色打印机,同为打印机的它们都能打印东西,但是同样的打印行为,打印出来的纸张效果是不一样。
总结以上的特点,同一件事情发生在不同的对象身上,会产生不同的行为结果,这就是多态。
要具体使用多态的这种特性,需要在特定条件下才能使用,接下来我将逐一介绍实现的条件和过程。
2、多态的前提条件
在java中要实现多态,必须要满足如下几个条件,缺一不可:
- 必须在继承体系下
- 子类必须要对父类中方法进行重写(父类得引用对应的子类对象)
- 通过父类的引用调用重写的方法
多态的实现:
运行代码时,传递不同的子类对象给父类对象,之后通过调用父类和子类中构成重写的方法,就会执行对应类的方法。期间父类引用子类对象会发生向上转型。
听到这里,你可以会有很多的疑问,重写是什么?向上转型又是什么?
不用担心,接下来我会以一个代码示例,带你感受一下多态的代码实现!
注意:
编译器在编译代码的时候,并不知道父类调用重写方法时,具体调用的是哪个子类对象,在程序运行起来的时候,会先判断其父类的引用对象类型,再根据实际的类型调用其相应的方法。运行过程中即会发生“动态绑定”。(下面将会介绍)
此处你们可能会发现代码中的父类Animal和子类Dog和Cat内,都存在eat()方法,而且除了内部语法不一样之外,方法名,返回类型,甚至连参数列表都一模一样。这些方法之间构成了方法的重写。
之前的方法重载是参数列表不一样,返回类型和变量名一样。
二、方法的重写
1、什么是重写?
重写(override):
重写也称为覆盖。重写是子类对父类非静态、非private修饰,非final修饰,非构造方法等的实现过程进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写!
重写的好处在于子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法。
2、重写的规则
在使用重写的时候需要注意其规则,不是任何场合下都能使用的!
1、子类在重写父类的方法时,一般必须与父类方法原型一致:修饰符 返回值类型 方法名(参数列表) 要完全一致。
2、其实被重写的方法返回值类型可以不同,但是必须是具有父子关系的。这种类型关系称为:方法重写的协变。(协变类型,如果重写的时候,返回值之间构成继承关系,则称为协变)
3、子类的访问权限等级不能比父类的访问权限更低,例如:如果父类的一个方法被声明public,那么在子类中重写该方法就不能声明为 protected,只能为public了。或者父类一个方法被声明为protected,那么子类的重写方法可以声明为pubilc或protected。(最起码与父类的访问权限等级相等或更高)。
访问权限大小关系:
public>protected>default>private
【注意】private不能修饰重写方法
4、父类的成员方法只能被它的子类重写。
5、子类和父类不在同一个包中,那么子类只能够重写父类的声明为 public 和 protected 的非 final 方法。
6、子类和父类在同一个包中,那么子类可以重写父类所有方法,除了声明为 private 和 final 的方法。
7、父类被static、private修饰的方法和构造方法都不能被重写。
8、声明为 final 的方法不能被重写。
9、重写的方法, 可以使用 @Override 注解来显式指定. 有了这个注解能帮我们进行一些合法性校验. 例如不小心 将方法名字拼写错了 (比如写成 aet), 那么此时编译器就会发现父类中没有 aet 方法, 就会编译报错, 提示无法构成重写.
在IDEA中,我们可以通过快捷键,方便地直接打印出重写方法-------ctrl+o
创建后,该重写方法会默认为以下语句
【注意】:如果类和类之间没有继承关系,是不会出现重写的!
3、重写和重载的区别
区别点 | 重载(overrode) | 重写(override) |
参数列表 | 必须修改 | 一定不能修改 |
返回类型 | 可以修改 | 一定不能修改 |
访问限定符 | 可以修改 | 一定不能做更严格的限制(可以降低限制) |
方法的重载:一个类的多态性表现
方法之间构成的重载,其方法的返回类型不作要求,但变量名需一样,而参数列表一定要有所不同(参数数量、参数类型、参数顺序上的不同)。
方法的重写:子类与父类的一种多态性表现。
=========================================================================
我们在编译时调用父类的重写方法,编译器并不会知道父类对象会具体调用引用的哪个子类重写方法,但是在运行后,却能准确地调用相应的子类重写方法。这是为什么呢?
这时就引出了一个名词---“动态绑定”
动态绑定 :
动态绑定是指在执行期间(非编译期)判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。程序运行过程中,把方法(或过程)调用与响应调用所需要的代码相结合的过程称为动态绑定。
也称为后期绑定(晚绑定),即在编译时,不能确定方法的行为,需要等到程序运行时,才能够确定具体调用那个类的方法,因此也可以称其为运行时绑定。(动态绑定是多态的基础)
以此代码的字节码文件为例:
class Animal { public String name; public Animal(String name) { this.name = name; } public void eat() { System.out.println(this.name + "吃饭"); } } class Cat extends Animal { public Cat(String name) { super(name); } public void eat() { System.out.println(this.name + "正在吃猫粮"); } } class Dog extends Animal { public Dog(String name) { super(name); } public void eat() { System.out.println(this.name + "正在吃狗粮"); } } public class TestDemo6 { public static void main(String[] args) { Animal animal = new Animal("小黑"); Animal animal1 = new Cat("咪咪"); Animal animal2 = new Dog("小黄"); animal.eat(); animal1.eat(); animal2.eat(); } }
在运行的时候,Animal.eat()才变成子类自己的eat()方法,也就是说发生动态绑定后才知道其父类具体调用了谁。
静态绑定:
有动态绑定自然就有静态的绑定!
静态绑定也称为前期绑定(早绑定),即在编译时,根据用户所传递实参类型就确定了具体调用那个方法。典型代表方法重载。
4、避免在构造方法中调用重写的方法
下面以一个代码为例:
此代码中:
- 构造Cat的对象时,会调用Animal的构造方法
- Animal的构造方法中调用了eat方法, 此时会触发动态绑定, 会调用到 Cat中的eat
【 因为 Java 中的动态绑定机制。在 Java 中,动态绑定是通过虚拟方法表(VMT)实现的。当一个对象调用一个方法时,Java 会在该对象的 VMT 中查找该方法的具体实现。如果在该对象的类中找到了该方法的实现,则直接调用该方法;否则,Java 会在该对象的父类中查找该方法的实现,直到找到为止。】
此处在父类的构造方法中调用子类的重写方法时,由于子类已经重写了该方法,因此 Java 会在子类中查找该方法的具体实现,并调用子类的方法。
以后在我们写代码的时候,尽量不要出现类似的代码,可能会出现一些隐藏的但是又极难发现的问题。我们要避免在构造方法中调用重写的方法了!
三、向上转型和向下转型
1、向上转型
向上转型:实际就是创建一个子类对象,将其当成父类对象来使用。
语法格式:父类类型 对象名 = new 子类类型()
以animal为例,animal是父类类型,但可以引用一个子类对象,因为是从小范围向大范围的转换。 (注意:继承关系,一次只能继承一个类)
比如: Aniaml animal=new Cat();
此处代表了,猫是动物的这一关系。
我们知道平时new一个对象后,可能调用对应类里的成员方法。
还需注意的是不能调用其他类中的方法,即使两个类之间构成了继承关系!
2、向上转型的三种方式
2.1、直接赋值
通过父类直接引用子类对象
public class TestDemo6 {
public static void main(String[] args) {
Animal animal= new Cat();
animal.eat();
}
}
2.2、通过方法的传参
通过传一个子类对象,让父类接受,达到向上转型的效果。
public static void eat(Animal animal){
animal.eat();
}
public static void main(String[] args) {
eat(new Cat());
}
2.3、方法返回值
以一个方法的返回值类型为父类类型,而调用方法中返回一个子类对象,从而进行向上转型。
public static Animal eat(){
return new Cat("小黄");
}
public static void main(String[] args) {
eat();
}
public class TestAnimal {
// 2. 方法传参:形参为父类型引用,可以接收任意子类的对象
public static void eatFood(Animal a){
a.eat();
}
// 3. 作返回值:返回任意子类对象
public static Animal buyAnimal(String var){
if("狗" == var){
return new Dog("狗狗",1);
}else if("猫" == var){
return new Cat("猫猫", 1);
}else{
return null;
}
}
public static void main(String[] args) {
// 1. 直接赋值:子类对象赋值给父类对象
Animal cat = new Cat("元宝",2);
Dog dog = new Dog("小七", 1);
eatFood(cat);
eatFood(dog);
Animal animal = buyAnimal("狗");
animal.eat();
animal = buyAnimal("猫");
animal.eat();
}
3、向下转型
将一个子类对象经过向上转型之后当成父类方法使用,再无法调用子类的方法,但有时候可能需要调用子类特有的方法,此时:将父类引用再还原为子类对象即可,即向下转型。
向上转型是把子类给父类,而向下转型是把父类给子类。
即Animal animal=new Cat();(向上转型),但此处只能调用Animal中的方法,无法调用子类中的方法。
向下转型语法格式:
Animal animal=new Cat(); Cat cat=(Cat) animal;
向下转型和向上转型的转向:
其中我们该注意的是,在发生向上转型之后,若想进行向下转型,一定得确定其还原的对象是否是刚刚向上转型的对象。
而且在向下转型时,需对父类对象进行类型强制转换,因为animal的类型时Animal,而cat的类型时Cat,需将animal强转为Cat!
向下转型其实是很不安全的。万一在使用的时候转换失败,运行时就会抛异常。
Java中为了提高向下转型的安全性,引入 了 instanceof ,如果该表达式为true,则可以安全转换。
public class TestAnimal { public static void main(String[] args) { Cat cat = new Cat("元宝",2); Dog dog = new Dog("小七", 1); // 向上转型 Animal animal = cat; animal.eat(); animal = dog; animal.eat(); if(animal instanceof Cat){ cat = (Cat)animal; cat.mew(); } if(animal instanceof Dog){ dog = (Dog)animal; dog.bark(); } } }
此处的instanceof,是用来判断animal这个引用是否引用过相对应的子类对象(发生向上转型),如果不是则为该语句结果为false,如果是则为true,执行if中的代码!
四、多态的优缺点
1、多态的优点
1.1、降低代码圈复杂度
使用多态,能够降低代码的 "圈复杂度", 避免使用大量的 if - else
什么叫 "圈复杂度" ? 圈复杂度是一种描述一段代码复杂程度的方式. 一段代码如果平铺直叙, 那么就比较简单容易理解. 而如 果有很多的条件分支或者循环语句, 就认为理解起来更复杂. 因此我们可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数, 这个个数就称为 "圈复杂度". 如果一个方法的圈复杂度太高, 就需要考虑重构. 不同公司对于代码的圈复杂度的规范不一样. 一般不会超过 10
例如我们现在需要打印的不是一个形状了, 而是多个形状. 如果不基于多态, 实现代码如下:
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) {
if (shape.equals("cycle")) {
cycle.draw();
} else if (shape.equals("rect")) {
rect.draw();
} else if (shape.equals("flower")) {
flower.draw();
}
}
}
如果使用使用多态, 再通过一个for-each循环则不必写这么多的 if - else 分支语句, 代码更简单.
public static void drawShapes() {
// 创建了一个 Shape 对象的数组.
Shape[] shapes = {new Cycle(), new Rect(), new Cycle(),
new Rect(), new Flower()};
for (Shape shape : shapes) {
shape.draw();
}
}
//Shape为自定义类类型,定义一个自定义类数组,每一个元素都是一个实例化的对象,
//发生了向上转型
1.2、可扩展能力更强
通过多态,我们可以很方便地对其功能的扩展,不用过于麻烦地改动。
以上面打印图形形状为例,如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低.
新增打印三角形
class Triangle extends Shape {
@Override
public void draw() {
System.out.println("△");
}
}
对于类的调用者来说(drawShapes方法), 只要创建一个新类的实例就可以了, 改动成本很低. 而对于不用多态的情况, 就要把 drawShapes 中的 if - else 进行一定的修改, 改动成本更高.
2、 多态的缺点
再好用的东西,有优点就有缺点。
多态的缺点是:代码的运行效率降低。
五、总结(多态的注意事项)
- 必须在继承体系下
- 子类必须要对父类中方法进行重写(向上转型)
- 通过父类的引用调用重写的方法(动态绑定)
- 【注意】:向上转型和重写并没有什么关系,不要弄混。重写是重写,向上转型是向上转型
- 向上转型是父类引用子类对象
- 向下转型不安全,用的时候得加instanceof判断是否符合安全还原
- 重写也称为覆盖。重写是子类对父类非静态、非private修饰,非final修饰,非构造方法等的实现过程 进行重新编写。
有关Java中的三大特性介绍,到这里就已经完结了。
如果文章对各位看官有所帮助,希望能点一个免费赞支持支持!
要是有哪个地方写的不好,或哪个地方出现错误,也希望各位大佬能帮忙纠正错误,我会多加改进
让我们一起努力,一起加油吧