宝剑锋从磨砺出
梅花香自苦寒来
目录
1.多态的概念
1.1 多态的基本概念
多态:见名知意,就可以知道是多种状态、多种形态。也就是去完成某个行为的时候,当不同的对象去完成就会产生不同的状态
比如:打印机可以分为彩色打印机和黑白打印机,我们知道 Java 是面向对象思想的语言,面向对象的程序设计中一切皆为对象。那么彩色打印机是一个对象,黑白打印机又是一个对象,它们都属于打印机,所以打印机是它们的父类,它们是打印机的子类。它们同样是打印但是当用彩色打印机打印出来的一定是彩色的,当用黑白打印机打印出来的就是黑白色的,所以这就体现了当用不同的对象去完成同一个行为时会产生不同的状态
结论:同一件事情发生在不同的对象身上,就会产生不同的结果
1.2 多态实现的条件
如果想要实现多态必须满足以下条件:
- 必须在继承体系下
- 子类必须对父类的方法进行重写
- 通过父类的引用调用子类重写的方法
多态的体现:在代码运行时,当传递不同的类对象时,会调用对应类中的重写方法
class Printer {
public String name;
Printer(String name) {
this.name = name;
}
public void print() {
System.out.println(name+"打印");
}
}
class ColorPrinter extends Printer {
ColorPrinter(String name) {
super(name);
}
public void print() {
System.out.println(name+"打印彩色的");
}
}
class BWPrinter extends Printer {
BWPrinter(String name) {
super(name);
}
public void print() {
System.out.println(name+"打印黑白的");
}
}
public class Test {
public static void main(String[] args) {
ColorPrinter colorPrinter = new ColorPrinter("彩色");
Printer printer = colorPrinter;
printer.print();
}
}
Printer:打印机 ColorPrinter:彩色打印机 BWPrinter:黑白打印机
要实现多态第一步就是必须在继承体系下,彩色打印机和黑白打印机都是打印机的子类,也就说明它们是继承关系。第二步就是子类必须对父类的方法进行重写,父类里面写了一个print方法,两个子类里面也写了print方法,这些print方法结构都一样只是方法体的里面的实现不一样,说明它们的确是重写了父类的方法。第三步通过父类的引用调用子类重写的方法,我们实例化一个子类对象,然后将子类对象赋值给一个父类类型,然后通过父类的引用调用该子类的方法
2.重写
上述我们说了想要实现多态,子类必须对父类的方法进行重写。那我们接下来就一起学习重写
2.1 重写的概念
重写:也称为覆盖。重写是子类对父类非静态、非private修饰,非final修饰,非构造方法等的实现过程进行重新编写, 返回值和形参还有方法名都不能改变(即外壳不变,核心重写)。
重写的好处: 子类可以根据需要,定义特定于自己的行为。也就是说子类能够根据需要实现父类的方法。
2.2 方法重写的规则
- 子类在重写父类的方法时,一般必须与父类方法原型一致: 返回值类型 方法名 (参数列表) 要完全一致
- 被重写的方法返回值类型可以不同,但是必须是具有父子关系的
- 访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类方法被public修饰,则子类中重写该方法就不能声明为 protected
- 父类被static、private修饰的方法、构造方法都不能被重写
- 重写的方法,可以使用 @Override 注解来显式指定。有了这个注解能帮我们进行一些合法性校验。例如不小心将方法名字拼写错了 (比如写成 aet),那么此时编译器就会发现父类中没有 aet 方法,就会编译报错,提示无法成重写
2.3 重写和重载的区别
①重写
class Printer {
public String name;
Printer(String name) {
this.name = name;
}
public void print() {
System.out.println(name+"打印");
}
}
class ColorPrinter extends Printer {
ColorPrinter(String name) {
super(name);
}
@Override
public void print() {
System.out.println(name+"打印彩色的");
}
}
上述代码 子类ColorPrinter 中的 print方法 就是对 父类Printer 里面的 print方法 进行了重写
重写:返回类型相同,方法名相同,形参列表一致
注:返回值还可以是父子关系
②重载
class Printer {
public String name;
Printer(String name) {
this.name = name;
}
public void print() {
System.out.println(name+"打印");
}
public void print(int year) {
System.out.println(name+year+"打印彩色的");
}
}
此时 Printer类 里面的两个 print方法 进行了重载
重载:返回类型随便,方法名相同,参数列表一定不同
注:参数列表不同可以是形参顺序不同,形参类型不同...
方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现
- 静态绑定:也称为前期绑定(早绑定),即在编译时,根据用户所传递实参类型就确定了具体调用那个方法。典型代表函数重载。
- 动态绑定:也称为后期绑定(晚绑定),即在编译时,不能确定方法的行为,需要等到程序运行时,才能够确定具体调用那个类的方法。
3.向上转型和向下转型
向上转型和向下转型是我们学习多态的一个重要部分,在多态实现条件中需要满足通过父类的引用调用子类重写的方法,这时我们就需要用到向上转型。
3.1 向上转型
在上述代码中有一个打印机的多态代码。在测试类的主函数中这样的代码:
ColorPrinter colorPrinter = new ColorPrinter("彩色");
Printer printer = colorPrinter;
我们知道 ColorPrinter 是子类,Printer 是父类,实例化子类,把子类实例化的对象赋值给了父类类型对象,这就是向上转型
向上转型:实际就是创建一个子类对象,将其当成父类对象来使用。
语法格式:父类类型 对象名 = new 子类类型()
Printer printer = new ColorPrinter("彩色");
还可以将其语法格式分开写,就是先实例化一个子类,然后将子类对象赋值给父类类型的变量
ColorPrinter colorPrinter = new ColorPrinter("彩色");
Printer printer = colorPrinter;
Printer 是父类类型,但是可以引用子类对象,因为是小范围向大范围的转换
向上转型:
- 只能访问父类中的成员属性
- 访问方法时如果子类中没有,则访问父类的,如果父类中也没有,则直接报错
- 访问方法时如果父类中没有 子类中有,则直接报错
- 访问方法时如果父类与子类中的方法重写,则访问子类的
- 访问方法时如果父类与子类中的方法重载,访问子类的重载方法,则直接报错
使用场景:
- 直接赋值
- 方法传参
- 方法返回
class Printer {
public String name;
Printer(String name) {
this.name = name;
}
public void print() {
System.out.println(name+"打印");
}
}
class ColorPrinter extends Printer {
ColorPrinter(String name) {
super(name);
}
@Override
public void print() {
System.out.println(name+"打印彩色的");
}
}
class BWPrinter extends Printer {
BWPrinter(String name) {
super(name);
}
@Override
public void print() {
System.out.println(name+"打印黑白的");
}
}
还是拿这些继承体系下的类来举例
①直接赋值
public class Test {
public static void main(String[] args) {
Printer printer = new ColorPrinter("彩色打印机");
printer.print();
}
}
通过 Test类 的主方法中 Printer printer = new ColorPrinter("彩色打印机") 这句代码直接赋值,来完成了向上转型。
②方法传参
public class Test {
public static void fun(Printer printer) {
printer.print();
}
public static void main(String[] args) {
ColorPrinter colorPrinter = new ColorPrinter("彩色打印机");
fun(colorPrinter);
}
}
将实例化的子类对象通过方法传参的形式,传给父类对象,来完成向上转型。
③方法返回
public class Test {
public static Printer buyPrinter(String name) {
if ("彩色打印机".equals(name)) {
return new ColorPrinter("彩色打印机");
} else if ("黑白打印机".equals(name)) {
return new BWPrinter("黑白打印机");
} else {
return null;
}
}
public static void main(String[] args) {
Printer printer = buyPrinter("彩色打印机");
printer.print();
}
}
通过方法返回实例化对象的方式,进行向上转型。根据方法的形参来判断需要实例化那个子类对象
注:父类引用哪个子类,当通过父类引用去访问重写的方法时,则会访问父类引用的子类中的重写方法
3.2 向下转型
向下转型:将一个子类对象经过向上转型之后当成父类方法使用,再无法调用子类的方法,但有时候可能需要调用子类特有的方法,此时:将父类引用再还原为子类对象即可,即向下转换
向下转型用的比较少,而且不安全,万一转换失败,运行时就会抛异常。Java中为了提高向下转型的安全性,引入 了 instanceof ,如果该表达式为true,则可以安全转换
class Printer {
public String name;
Printer(String name) {
this.name = name;
}
public void print() {
System.out.println(name+"打印");
}
}
class ColorPrinter extends Printer {
ColorPrinter(String name) {
super(name);
}
@Override
public void print() {
System.out.println(name+"打印彩色的");
}
public void print2() {
System.out.println(name+"激光打印");
}
}
public class Test {
public static void main(String[] args) {
Printer printer = new ColorPrinter("彩色打印机");
printer.print();
//printer.print2();直接报错
//此时可以采用向下转型
//如果直接向下转型就很不安全,我们就需要instanceof来判断
if (printer instanceof ColorPrinter) {
ColorPrinter colorPrinter = (ColorPrinter) printer;
colorPrinter.print2();
}
}
}
4.多态的优缺点
4.1 多态的优点
假设有如下代码:
class Draw {
public void fun() {
System.out.println("画画");
}
}
class Square extends Draw {
public void fun() {
System.out.println("正方形");
}
}
class Circular extends Draw {
public void fun() {
System.out.println("圆");
}
}
多态的好处:
①能够降低代码的 "圈复杂度", 避免使用大量的 if - else
那么什么是圈复杂度呢?
圈复杂度是一种描述一段代码复杂程度的方式。一段代码如果平铺直叙,那么就比较简单容易理解。而如果有很多的条件分支或者循环语句, 就认为理解起来更复杂。因此我们可以计算一段代码中条件语句和循环语句出现的个数, 这个个数就称为 "圈复杂度"。如果一个方法的圈复杂度太高, 就需要考虑重构。
如果我们现在需要打印多个图形,如果不用多态,就需要用到多个 if - else :
public class Test {
public static void main(String[] args) {
Square square = new Square();
Circular circular = new Circular();
String[] arr = {"square","circular"};
for (String tmp : arr){
if (tmp.equals("square")) {
square.fun();
} else if(tmp.equals("circular")) {
circular.fun();
}
}
}
}
如果使用使用多态, 则不必写这么多的 if - else 分支语句:
public class Test {
public static void func(Draw draw) {
draw.fun();
}
public static void main(String[] args) {
func(new Square());
func(new Circular());
}
}
②可扩展能力更强
如果要新增一种新的形状,使用多态的方式代码改动成本也比较低
对于类的调用者来说,只要创建一个新类的实例就可以了,改动成本很低
4.2 多态的缺点
多态缺陷:代码的运行效率降低。 1. 属性没有多态性 当父类和子类都有同名属性的时候,通过父类引用,只能引用父类自己的成员属性 2. 构造方法没有多态性
5.避免在构造方法中调用重写的方法
class Draw {
public Draw() {
fun();
}
public void fun() {
System.out.println("画画");
}
}
class Square extends Draw {
public Square() {
super();
}
int num = 1;
public void fun() {
System.out.println("正方形"+num);
}
}
public class Test {
public static void main(String[] args) {
Draw draw = new Square();
}
}
运行结果:
- 构造 Square 对象的同时, 会调用 Draw 的构造方法.
- Square 的构造方法中调用了 fun 方法, 此时会触发动态绑定, 会调用到 Draw 中的 fun
- 此时 Square 对象自身还没有构造, 此时 num 处在未初始化的状态, 值为 0。如果具备多态性,num的值应该是1.
- 所以在构造函数内,尽量避免使用实例方法,除了final和private方法。