作者:困了电视剧
专栏:《JavaSE语法与底层详解》
文章分布:这是一篇关于Java面向对象三大特性——多态的文章,在本篇文章中我会分享多态的一些基础语法以及类在继承时代码的底层逻辑和执行顺序。
目录
多态的定义及实现条件
多态的概念
多态的实现条件
方法重写
重写的规则和与重载的对比
静态绑定
动态绑定
向上转型和向下转型
向上转型
向下转型
多态的优点和注意事项
class Animal{
String name;
int age;
String sex;
public void eat(){
System.out.println("吃饭");
}
public void sleep(){
System.out.println("睡觉");
}
}
class Cat extends Animal{
public void eat(){
System.out.println("吃狗粮");
}
}
class Dog extends Animal{
public void eat(){
System.out.println("吃猫粮");
}
}
以这段代码作为栗子。
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同 的状态。总的来说:同一件事情,发生在不同对象身上,就会产生不同的结果。
多态在上述栗子的体现中就是Animal都有吃饭这一行为,但在猫和狗中就体现了不同的状态,即狗吃狗粮,猫吃猫粮。
1. 必须在继承体系下
2. 子类必须要对父类中方法进行重写
3. 通过父类的引用调用重写的方法
多态体现:在代码运行时,当传递不同类对象时,会调用对应类中的方法。
在上述栗子中,我的Cat类和Dog类都继承了Animal 类,同时在子类中我又对父类中的eat方法进行了重写,最后我只需要在main函数中用父类的引用调用子类重写的方法即可,这样一个Animal就可以根据我的需要既表现出Cat的eat行为,又表现出Dog的eat行为,从而实现了多态。
重写(override):也称为覆盖。重写是子类对父类非静态、非private修饰,非final修饰,非构造方法等的实现过程 进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写!重写的好处在于子类可以根据需要,定义特定 于自己的行为。 也就是说子类能够根据需要实现父类的方法。
1.子类在重写父类的方法时,一般必须与父类方法原型一致: 返回值类型 方法名 (参数列表) 要完全一致
2.被重写的方法返回值类型可以不同,但是必须是具有父子关系的
3.访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类方法被public修饰,则子类中重写该方 法就不能声明为 protected
4.父类被static、private修饰的方法、构造方法都不能被重写。
5.重写的方法, 可以使用 @Override 注解来显式指定. 有了这个注解能帮我们进行一些合法性校验. 例如不小心 将方法名字拼写错了 (比如写成 aet), 那么此时编译器就会发现父类中没有 aet 方法, 就会编译报错, 提示无法 构成重写.
静态绑定:也称为前期绑定(早绑定),即在编译时,根据用户所传递实参类型就确定了具体调用那个方法。典型代 表函数重载。
动态绑定:也称为后期绑定(晚绑定),即在编译时,不能确定方法的行为,需要等到程序运行时,才能够确定具体 调用那个类的方法。
public class Javabit_Code {
public static void main(String[] args) {
Animal an1=new Cat();
an1.eat();
System.out.println("==============");
Animal an2=new Dog();
an2.eat();
}
}
class Animal{
String name;
int age;
String sex;
public void eat(){
System.out.println("吃饭");
}
public void sleep(){
System.out.println("睡觉");
}
}
class Cat extends Animal{
public void eat(){
System.out.println("吃猫粮");
}
}
class Dog extends Animal{
public void eat(){
System.out.println("吃狗粮");
}
}
以这段代码为例,当程序在进行编译的时候,程序无法确定我们回去调用哪一个eat,只有当程序运行起来的时候程序才会知道,此时程序底层就发生了动态绑定,这里的动态其实也就是运行的意思。我们可以看一下程序的底层逻辑。
我们可以清楚地看到程序在进行编译的时候,由于不知道会调用哪一个子类的重写方法,于是直接调用父类的方法,当程序运行的时候,这个方法再通过动态绑定从而执行指定的子类的方法。
对于动态绑定的过程可以简单举例理解为,父类的方法的地址为0x88,编译时将父类方法和地址存储在一个地方A,父类方法地址对应的就是0x88,当运行的时候,程序知道了我此时要调用的是Cat的eat方法,于是就将Cat重写的eat方法的地址拿出来并将 A这个地方中父类方法对应的地址改为Cat方法的地址,然后得以运行。
向上转型:实际就是创建一个子类对象,将其当成父类对象来使用。
比如:
Animal animal=new Cat();
animal是父类类型,但可以引用一个子类对象,因为是从小范围向大范围的转换。
向上转型的优点:让代码实现更简单灵活,当我们在做一些项目的时候,在方法传参等方面会有巨大的便利。
向上转型的缺陷:不能调用到子类特有的方法。
这个不能调用不是代表子类对象中没有,而是由于存储在栈区中的是父类的引用类型,这个引用类型只能指向自己类中拥有的成员,对子类中独有的成员无法进行指向。
将一个子类对象经过向上转型之后当成父类方法使用,再无法调用子类的方法,但有时候可能需要调用子类特有的 方法,此时:将父类引用再还原为子类对象即可,即向下转换。
对于向下转型,由于是大的转小的,所以会非常的不安全,举个栗子:
如果animal对象此时不是向下转型的父类就会报错,为了避免这一问题,Java的研发团队引入了instanceof关键字,他可以帮我们判断类和对象之间的关系,帮助我们减少错误。
1. 能够降低代码的 "圈复杂度", 避免使用大量的 if - else
什么叫 "圈复杂度" ? 圈复杂度是一种描述一段代码复杂程度的方式. 一段代码如果平铺直叙, 那么就比较简单容易理解. 而如 果有很多的条件分支或者循环语句, 就认为理解起来更复杂. 因此我们可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数, 这个个数就称为 "圈复杂度". 如果一个方法的圈复杂度太高, 就需要考虑重构. 不同公司对于代码的圈复杂度的规范不一样. 一般不会超过 10 .
2. 可扩展能力更强 如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低.
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.func() 0。
这段代码可以作为之前知识的检测,他的执行顺序为:实例父类的成员变量——》执行父类的构造方法——》父类的构造方法中有func方法,于是调用func方法——》子类D中重写了func方法所以此时发生动态绑定即调用子类的func方法——》父类B成员尚未完成实例化,所以此时的D类中成员并没有实例化——》此时的num取默认值即0——》输出D.func() 0。
结论: "用尽量简单的方式使对象进入可工作状态", 尽量不要在构造器中调用方法(如果这个方法被子类重写, 就会触 发动态绑定, 但是此时子类对象还没构造完成), 可能会出现一些隐藏的但是又极难发现的问题.
以上就是本篇博客的全部内容,如有疏漏还请指正!如有帮助,还请三连!