文章目录
- 一、什么是多态
- 二、重写
- 2.1、重写的规则
- 三、多态的实现条件
- 四、向上转型
- 五、向下转型
- 六、动态绑定
- 七、使用多态的优缺点
- 7.1、优点
- 7.2、缺点
- 八、避免在构造方法中调用重写的方法
一、什么是多态
Java多态是面向对象编程的一个重要特性,它允许不同的对象对同一消息做出不同的响应。具体点就是不同的对象去完成相同的一个任务完成后展现的结果不相同。比如:学生在学校里面学习,但是学生分为体育生和文化生,体育生在学校里面主要学习体能方面的训练技巧,文化生在学校里面主要学习文化知识,两者在学校里面都是进行学习这一件事,但是体育生和文化生学习得到的内容并不相同,体育生学习后的结果就是体能方面的提升,文化生学习后的结果就是将知识记忆在脑海中,两者都是进行学习,但是结果并不相同,这就是多态。
体育生是一个对象,文化生是一个对象,两个对象完成学习这一件事情得到的结果不相同,这是一个多态现象的体现,但想要在程序世界实现多态就需要其他的特性的辅助了
二、重写
重写,又称为“覆盖”,简单来说就是将原有的方法进行重新编写,但是方法名,返回类型,参数列表必须要相同,是子类对父类非静态,非private修饰,非final修饰,非构造方法等谁实现过程进行重新编写,即外壳不变,内部革新。
当子类继承父类,并且子类调用从父类继承下来的方法时,该方法不适应当前对象的行为,又或者是行为类似但不尽相同,子类需要重新定义该行为,此时就可以将父类继承下来的方法进行重写,定义属于自己的特定的行为。 比如说学生类,学生对象在学校的行为就是学习,当体育生类继承学生类,体育生对象调用行为学习,但是该行为对于体育生来说不是很完整,体育生对象应该是学习体能训练技巧,此时我们就可以对行为学习进行重写,将其重新定义为学习体能训练技巧。
对于已经投入使用的类,尽量不要轻易修改,避免出现不必要的麻烦。最好的方法就是重新定义一个类,来重复利用其中的共性内容,并且添加或者改动新的内容。
学生类
public class Student {protected String name;protected String id;protected int age;public Student(String name, String id, int age) {this.name = name;this.id = id;this.age = age;}public void study() {System.out.println("学生" + name + "在学习....");}
}
体育生类
public class CulStu extends Student{public CulStu(String name, String id, int age) {super(name, id, age);}}
子类体育生对父类学生类中的行为学习进行重写
public class CulStu extends Student{public CulStu(String name, String id, int age) {super(name, id, age);}@Overridepublic void study() {//对父类的行为进行重写System.out.println("体育生" + name + "在学习体能训练技巧...");}
}
2.1、重写的规则
1、子类在重写父类方法时,必须与父类的原型一致:返回类型,参数列表,方法名要完全一致
2、被重写的方法返回类型可以不相同,但是必须具有父子类关系
3、访问权限不能比被重写的方法的访问权限低,就是重写后的方法的访问权限要大于等于被重写的方法的访问权限。例如:如果父类方法被public修饰,子类中重写方法的访问权限就不能是protected;如果被重写方法是被protected修饰,那么重写方法只能说public修饰或者是protected修饰。访问权限(从小到大0):private < 默认权限 < protected < public
4、被重写方法和重写后的方法,两者的方法签名是相同的。
5、父类中的构造方法、被static修饰的方法、被final修饰的方法,被private修饰的方法不可以被重写
6、重写的方法可以使用“@Override”注解来显示指定,该注解可以帮我们校验重写中出现的错误,比如:将重写方法名编写错了,父类中没有对于的方法名,那么此时编译会爆红,来提示我们重写出现错误。
7、只能在子类中进行重写父类的方法,被重写方法和重写方法必须构成父子类关系
需要注意的是:被private修饰的方法不能够被重写的原因并非是访问权限的原因,重写后的方法的访问权限要大于等于被重写方法的访问权限,这也就意味着访问权限是可以相同的,这说明如果被重写方法的访问权限是private的话,重写后方法的访问权限也可以说private,但实际上这样编写代码还是会爆红,被private修饰的方法不能够被重写的原因实际上是因为private修饰符本身,被private修饰的方法是私有型方法,只允许在本类中调用,拥有父子类关系也不可以调用,而且重写后方法和被重写方法的方法签名是一样的,那说明这两者是同一个方法。因此被private修饰的方法是不可重写的
三、多态的实现条件
在Java中要实现多态,必须要满足以下几点:
1、必须要含有父子类关系
2、子类中要重写父类的方法
3、通过父类的引用调用重写的方法
多态的体现:在代码允许的时候,当传递不同的对象时,会调用对应类中的方法
体育生类
public class PEstu extends Student{public PEstu(String name, String id, int age) {super(name, id, age);}@Overridepublic void study() {System.out.println("体育生" + name + "在学习体能训练技巧...");}
}
文化生类
public class CulStu extends Student{public CulStu(String name, String id, int age) {super(name, id, age);}@Overridepublic void study() {//对父类的行为进行重写System.out.println("文化生" + name + "在学习文化知识...");}
}
学生类
public class Student {protected String name;protected String id;protected int age;public Student(String name, String id, int age) {this.name = name;this.id = id;this.age = age;}public void study() {System.out.println("学生" + name + "在学习....");}
}
public class Main {public static void func(Student student) {student.study();}public static void main(String[] args) {Student student1 = new CulStu("文化生","1111",18);func(student1);Student student2 = new PEstu("体育生","2222",18);func(student2);}
}
当编译器在编译的时候func()方法并不知道会传递什么对象,也不知道会调用体育生类还是文化生类中的方法,只有等程序运行起来才知道会传递什么对象给方法,才知道会调用哪个类中的方法,需要注意的是:此时必须要使用父类的引用,func()方法的形参必须是父类类型的才可以。
因为形参sdutens引用引用的对象不同,所以调用同一个study()方法时,所表现的形式不一样,我们把这个情况称之为多态。(不同的对象做同一件事情所表现的结果不同)
第一次调用func()方法时,传递的是文化生类这个对象,studennt这个引用就指向文化生对象,此时student调用study()方法,就会调用子类中重写的syudy()方法,从而打印出”文化生 + name + 在学习文化知识…"
第二次调用func()方法时,传递的是体育生类这个对象,studennt这个引用就指向体育生对象,此时student调用study()方法,就会调用子类中重写的syudy()方法,从而打印出”体育生 + name + 在学习文化知识…"
在上述代码中,前三个类是由类的实现者编写的,最后一个类是类的调用者编写的。当类的调用者在编写func()这个方法的时候,参数类型是Student(父类),方法内部本身并不知道,也不关注将来会传递什么对象给student引用进行指向,此时studengt这个引用调用study()方法可能会有多种不同的表现,这种行为就称为多态。
四、向上转型
在实例化对象时,我们一般是这样编写的:
Student studengt = new Student();
但是我们看上面的代码会发现,并不是这样编写的,而是使用父类类型的引用来指向子类类型的对象
Student student1 = new CulStu("文化生","1111",18);
这就是向上转型。
向上转型:创建一个子类对象,将其当做父类对象来使用
语法格式: 父类类型 对象名 = new 子类类型();
父类类型的引用指向了子类对象,Student是父类,CulStu是子类,父类类型是可以引用子类对象的,我们知道继承本质上是is-a的关系,体育生是学生也是is-a的关系。
Student student1 = new CulStu("文化生","1111",18);文化生是学生
Student student1 = new CulStu("体育生生","1111",18);体育生是学生
向上转型的使用场景有三个:
1、直接赋值
2、方法传递
3、方法返回
Student student1 = new CulStu("体育生生","1111",18);//1、直接传递
public static void func(Student student) {//2、方法传递//将方法的参数类型设置为父类类型}
public static Student func() {//3、方法返回return student;//将方法的返回值设置为父类类型}
向上转型的优点:让代码变得更加灵活
缺点:该引用无法调用子类的特有的方法(因为引用是父类类型的,引用本身就只能调用本类型的方法变量,无法调用子类的方法变量)
五、向下转型
我们实现向上转型后,只能通过父类的引用调用父类的方法或者访问父类的变量,当我们需要调用子类的方法或者访问子类的变量时又该如何?重新new一个子类对象吗?不现实,如果重新new一个子类,那么和之前的子类对象就是两个对象,这两个对象之间是没有联系的。此时我们可以通过向下转型,将父类引用还原为子类引用,再通过该引用调用子类方法或访问变量即可。
但需要注意的是,将父类引用还原为子类引用,是一个强制转换的过程,因为向上转型是符合is-a关系的,体育生类是学生类没有问题:父类引用 = 子类对象,但此时我们是将父类引用转换为子类对象,相当于是将父类引用赋予给子类对象,学生类是体育生类,这似乎不太符合常理吧,并非所有学生类都是体育生类,因此相当于是非is-a的关系而是a-is的关系,有点倒反天罡啊,因此此处需要强制类型转换
public static void main(String[] args) {Student student = new CulStu("文化生","1111",18);CulStu culStu = (CulStu) student;//将父类引用强转为子类引用culStu.a = "嘻嘻";culStu.fun();}
将父类引用强转为子类引用后,此时子类引用指向之前被父类引用指向的子类对象,也就是说现在是子类引用指向子类对象,即可通过子类引用访问子类变量,调用子类方法了。
但是还有特别需要注意的:向上转型时父类引用指向体育生子类对象,当我们需要向下转换时,该父类引用只能强转为体育生子类引用,不可以将父类引用强转为文化生子类引用,否则会编译报错。
此时父类引用指向文化生子类对象,但是却将父类引用强转为体育生子类引用,引用类型不对,结果编译就报错了,并没有语法上的错误。因此我们需要强转的时候就需要先判断此时的父类引用得到是指向哪个子类对象,我们需要用到instanceof关键字,该关键字可以判断引用是否指向该类的对象。
引用名 instanceof 类名;//判断引用是否指向该类对象,指向则返回true,否则放false
再配合上if语句即可判断并执行强转代码了。
public static void main(String[] args) {Student student = new CulStu("文化生","1111",18);if(student instanceof CulStu) {//判断student引用是否指向Culstu类对象CulStu culStu = (CulStu) student;culStu.study();}if(student instanceof PEstu) {//判断student引用是否指向PEstu类对象PEstu pEstu = (PEstu) student;pEstu.study();}}
一般向下转型使用比较少,因为涉及到强制类型转换,强转是不安全的,有可能会导致数据的丢失,这个是我们无法预知的,因此不到迫不得已不要出现强转的代码。
六、动态绑定
public class Main {public static void func(Student student) {student.study();}public static void main(String[] args) {Student student1 = new CulStu("文化生","1111",18);func(student1);Student student2 = new PEstu("体育生","2222",18);func(student2);}
}
动态绑定又称之为运行时绑定。
大家可能会好奇,在func()方法中,明明是通过父类的引用去调用study()方法,但是最终显示的结果却是子类中重写的study()方法的结果,也就是说程序并非调用父类的study()方法,而是调用子类的study()方法
通过反汇编查看汇编代码,确实是调用父类的study()方法,但是在运行的时候却调用了子类重写的study()方法,这个过程称之为运行时绑定,又称动态绑定。在代码运行的时候父类的study()方法被绑定到了子类中重写的study()方法中。
动态绑定:又称为运行时绑定,在程序编译的时候不能确定方法的行为(结果),只有当程序运行的时候,才能够确定具体调用哪个类的方法。典型代表是方法重写,又称后期绑定
静态绑定:在编译的时候编译器通过用户传递的实参类型就可以确定具体调用哪个方法。典型代表是方法重载,又称前期绑定
七、使用多态的优缺点
7.1、优点
使用多态的好处:
1、可以降低圈复杂度圈复杂度是一种描述一段代码复杂程度的方式,一段代码如果平铺直叙,那么就很容易理解,如果加上大量的if语句循环语句就会导致理解起来很复杂。因此我们可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数,这个个数就称之为圈复杂度。当代码中没有条件语句和循环语句时,代码直接从头到尾一条路径执行,那么圈复杂度为1;如果代码中有一个if语句,那么此时代码出现了两种路径,那么圈复杂度为2。如果一个方法的圈复杂度太高,那么就需要考虑重构了
而使用多态,通过运行时绑定能够避免使用if语句,还可以调用重写后有特殊要求的方法,从而降低代码的圈复杂度。
此时有以下代码:
class Shape {public void draw() {System.out.println("画图形");}
}
class Rect extends Shape {@Overridepublic void draw() {System.out.println("画菱形");}
}class Cycle extends Shape{@Overridepublic void draw() {System.out.println("画圆形");}
}
class Flower extends Shape {@Overridepublic void draw() {System.out.println("画一朵花");}
}
public class Main {}
现在我们需要打印两朵花,两个圆形,一个菱形:(我们可以用到数组)
1、不使用多态打印
public class Main {public static void main(String[] args) {Rect rect = new Rect();Cycle cycle = new Cycle();Flower flower = new Flower();String[] str = {"Flower","Flower","Cycle","Cycle","Rect"};for (String s:str) {if(s.equals("Flower")) {flower.draw();} else if (s.equals("Cycle")) {cycle.draw();} else {rect.draw();}}}
}
2、使用多态打印
public static void main(String[] args) {Shape rect = new Rect();Shape cycle = new Cycle();Shape flower = new Flower();Shape[] shapes = {flower,flower,cycle,cycle,rect};for (Shape shape : shapes) {shape.draw();}}
观察两个代码不难看出:不使用多态打印的代码大量使用条件语句和循环语句,那么伴随着圈复杂度提高,代码的可阅读性降低。
2、可扩展能力强
如果此时需要增加打印,比如增加打印一个正方形,我们需要新定义一个正方形类,在不使用多态的情况下,需要在条件语句和循环语句上进行更改,需要做大量的修改操作,改动成本高;如果是使用多态的方法打印,那么我们只需要在数组里面增加指向正方形类对象的父类引用即可,代码的改动较小,效率高。
public static void main(String[] args) {Shape rect = new Rect();Shape cycle = new Cycle();Shape flower = new Flower();Shape[] shapes = {flower,flower,cycle,cycle,rect,square};for (Shape shape : shapes) {shape.draw();}}
7.2、缺点
多态的缺点:代码的运行效率降低
1、属性没有多态性
当父类和子类同时有同名属性的时候,通过父类引用,只能调用父类的成员属性
2、构造方法没有多态性
八、避免在构造方法中调用重写的方法
class A {public A() {func();}public void func() {System.out.println("A.func()");}
}class B extends A{private int num = 1;@Overridepublic void func() {System.out.println("B.func()" + num);}
}
public class Test {public static void main(String[] args) {A a = new B();}
}
仔细观察这一段代码,会打印什么结果?
首先调用子类的构造方法,在子类中的构造方法会先通过super( )调用父类的构造方法,但是在执行构造方法之前需要先执行构造代码块,调用父类的构造方法过程中再调用func()方法,此时满足动态绑定的条件:在继承体系下,子类中重写父类的方法,调用重写的方法。发生动态绑定调用子类中重写的func()方法,因为此时父类的构造方法还没执行完成,子类的构造代码块和构造方法就没有得以执行,因此子类中的成语变量处于未初始化状态,因此动态绑定调用子类重写的func()方法,方法里面打印子类未初始化的成员变量num,重写的func()方法执行完后父类的构造方法也执行完毕,接下来执行子类的构造代码块将num赋值为1,再执行子类的构造方法。因此程序最终打印结果为0。
用尽量简单的方式使对象进入可工作状态 用尽量简单的方式使对象进入可工作状态 用尽量简单的方式使对象进入可工作状态
尽量不要在构造器(构造方法、构造代码块)中调用方法,如果这个方法被子类重写,构造器内就会触发动态绑定,从而调用子类中重写的构造方法,但是此时子类对象还没有构造完成,因此该过程就有可能会触发一些引用又极难发现的问题。因此在构造器中最好不调用方法,只对成员变量进行初始化即可。