文章目录
- 1. 多态
- 1.1 多态的概念
- 1.2 多态的实现条件
- 1.3 重写
- 1.4 向上转型和向下转型
- 1.4.1 向上转型
- 1.4.2 向下转型
- 1.5 多态的优缺点
- 1.6 避免在构造方法中调用重写的方法
1. 多态
1.1 多态的概念
多态就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同 的状态。
例如不同的动物吃的食物不一样。
不同的交通工具速度不一样
1.2 多态的实现条件
要实现多态,必须要满足如下几个条件:
- 必须在继承体系下
- 子类必须要对父类中方法进行重写
- 通过父类的引用调用重写的方法
多态体现:在代码运行时,当传递不同类对象时,会调用对应类中的方法。
class Animals {public String name;public int age;public Animals(String name, int age ) {this.name = name;this.age = age;}public void eat() {System.out.println(name + "吃饭");}}class Cat extends Animals{public Cat(String name, int age) {super(name, age);}@Overridepublic void eat() {System.out.println(name + "吃饭");}}class Dog extends Animals{public Dog(String name, int age) {super(name, age);}@Overridepublic void eat() {//与父类的eat方法构成重写System.out.println(name + "吃饭");}}public class Test {public static void eat(Animals animals) {animals.eat();}public static void main(String[] args) {Dog dog = new Dog("大黄", 6);Cat cat = new Cat("小黑", 6);eat(dog);eat(cat);}}
在编写 eat 这个方法的时候, 参数类型为 Animal (父类), 此时在该方法内部并不知道, 也不关注当前的 animals 引用指向的是哪个类型(哪个子类)的实例。此时 animals 这个引用调用 eat方法可能会有多种不同的表现(和 animals 引用的实例相关), 这种行为就称为多态。
1.3 重写
重写(override):也称为覆盖。重写是子类对父类非静态、非private修饰,非final修饰,非构造方法等的实现过程进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写!
【方法重写的规则】
- 子类在重写父类的方法时,一般必须与父类方法原型一致: 返回值类型、方法名 (参数列表) 要完全一致。
- 被重写的方法返回值类型可以不同,但是必须是具有父子关系的。
- 访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类方法被public修饰,则子类中重写该方法就不能声明为 protected。
- 父类被static、private修饰的方法、构造方法都不能被重写。
- 重写的方法, 可以使用 @Override 注解来显式指定.,有了这个注解能帮我们进行一些合法性校验.。例如不小心将方法名字拼写错了 (比如写成 aet),那么此时编译器就会发现父类中没有 aet 方法, 就会编译报错, 提示无法构成重写。
【重写与重载的区别】
重写方法名和参数都一样。
class Animals {public String name;public int age;public void eat() {System.out.println(name + "吃饭");}public void sleep() {System.out.println(name + "睡觉");}public void play() {System.out.println(name + "做游戏");}}class Cat extends Animals{public Boolean LoveClean;//爱干净public void catchMouse() {System.out.println(name + "抓老鼠");}public void eat() {System.out.println(name + "吃饭");//与父类的eat方法构成重写}}
重载方法名相同,参数不同。
class Cat{public String name = "小黑";public void catchMouse() {System.out.println("抓老鼠");}public void catchMouse(String name) {//构成重载}}
静态绑定: 也称为前期绑定(早绑定),即在编译时,根据用户所传递实参类型就确定了具体调用那个方法。典型代表函数重载。
动态绑定: 也称为后期绑定(晚绑定),即在编译时,不能确定方法的行为,需要等到程序运行时,才能够确定具体调用那个类的方法。
1.4 向上转型和向下转型
1.4.1 向上转型
向上转型:实际就是创建一个子类对象,将其当成父类对象来使用。
语法格式:父类类型 对象名 = new 子类类型()
Aniamls animals = new Dog("大黄", 7);
向上转型的使用场景:
- 直接赋值
- 方法传参
- 方法返回
class Animal {public String name;public int age;public Animal(String name, int age ) {this.name = name;this.age = age;}public void eat() {System.out.println(name + "吃食物");}}class Cat extends Animals {public Cat(String name, int age) {super(name, age);}@Overridepublic void eat() {System.out.println(name + "吃食物");}}class Dog extends Animals { public Dog(String name, int age) {super(name, age);}@Overridepublic void eat() {//与父类的eat方法构成重写System.out.println(name + "吃食物");}}public class TestAniamls {// 方法传参:形参为父类型引用,可以接收任意子类的对象public static void eat(Animals animals) {animals.eat();}//作返回值:返回任意子类对象public static Animals byAnimals(String var) {if ("小狗狗".equals(var)) {return new Dog("小狗狗", 1);} else if ("小猫猫".equals(var)) {return new Cat("小猫猫", 1);}else {return null;}}public static void main(String[] args) {//直接赋值:子类对象赋值给父类对象Animals cat = new Cat("小黑", 7);Dog dog = new Dog("大黄", 6);eat(cat);eat(dog);}}
向上转型的优点:让代码实现更简单灵活。
向上转型的缺陷:不能调用到子类特有的方法。
1.4.2 向下转型
将父类引用再还原为子类对象,即向下转型。
向下转型不安全。
因为猫和狗是动物,向上转型安全;但是动物不一定是猫或者狗,所以不安全。
class Animal {public String name;public int age;public Animal(String name, int age ) {this.name = name;this.age = age;}public void eat() {System.out.println(name + "吃食物");}}class Cat extends Animal {public Cat(String name, int age) {super(name, age);}public void catchMouse() {System.out.println(name + "抓老鼠");}@Overridepublic void eat() {System.out.println(name + "吃食物");}}class Dog extends Animal {public Dog(String name, int age) {super(name, age);}public void brak() {System.out.println(name + "汪汪叫");}@Overridepublic void eat() {//与父类的eat方法构成重写System.out.println(name + "吃食物");}}public class TestAnimals {public static void main(String[] args) {Animal animal = new Cat("小黑", 7);Cat cat = (Cat)animal;//向下转型cat.catchMouse();}}
Dog类当中没有抓老鼠这一方法,引用的话会报错。
//部分代码public class TestAnimals {public static void main(String[] args) {Animal animal = new Dog("大黄", 7);Cat cat = (Cat)animal;//向下转型cat.catchMouse();}}
如果想要避免运行后报错,可以使用 instanceof关键字。
public class TestAnimals {public static void main(String[] args) {Animal animal = new Dog("大黄", 7);if (animal instanceof Cat) {Cat cat = (Cat)animal;//向下转型cat.catchMouse();}}}
如果反生错误的话会返回一个 flase。
1.5 多态的优缺点
先来看一段代码:
一个用来画三种图形的代码
package demo1;class Graph {public void draw() {System.out.println("画图形");}}class Rectangle extends Graph{@Overridepublic void draw() {System.out.println("画矩形!");}}class Cycle extends Graph{@Overridepublic void draw() {System.out.println("画圆!");}}class Triangle extends Graph{@Overridepublic void draw() {System.out.println("画三角形!");}}public class Test {public static void main(String[] args) {Graph graph1 = newRectangle();graph1.draw();Graph graph2 = new Cycle();graph2.draw();Graph graph3 = new Triangle();graph3.draw();}}
利用多态来进行优化:
package demo1;class Graph {public void draw() {System.out.println("画图形");}}class Rectangle extends Graph{@Overridepublic void draw() {System.out.println("画矩形!");}}class Cycle extends Graph{@Overridepublic void draw() {System.out.println("画圆!");}}class Triangle extends Graph{@Overridepublic void draw() {System.out.println("画三角形!");}}public class Test {public static void drawMap(Graph graph) {graph.draw();}public static void main(String[] args) {drawMap(new Rectangle());drawMap(new Cycle());drawMap(new Triangle());}}
同样的drawMap方法,在做着不同的事情,说明这是一个多态。
多态使代码变得简洁了。
【使用多态的好处】
- 能够降低代码的 “圈复杂度”, 避免使用大量的 if – else。
- 如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低。
在上面的代码中如果先要实现新的类直接添加就可以了。
//部分代码class Apple extends Graph{public void draw() {System.out.println("画苹果!");}}public class Test {public static void drawMap(Graph graph) {graph.draw();}public static void main(String[] args) {drawMap(new Rectangle());drawMap(new Cycle());drawMap(new Triangle());drawMap(new Apple());}}
这里可以说明使用多态后,改动也比较方便。
1.6 避免在构造方法中调用重写的方法
先来看一段代码
package demo1;class B {public B() {// do nothingfunc();}public void func() {System.out.println("B.func()");}}class D extends B {private int num = 1;@Overridepublic void func() {System.out.println("D.func() " + num);}}public class TestDemo {public static void main(String[] args) {D d = new D();}}
这段代码也该是会输出 D.func() 1。
但是结果却是:
说明一开始num就没有被初始化为1。
- 构造 D 对象的同时, 会调用 B 的构造方法。
- B 的构造方法中调用了 func 方法, 此时会触发动态绑定, 会调用到 D 中的func。
- 此时 D 对象自身还没有构造, 此时 num 处在未初始化的状态, 值为 0. 如果具备多态性,num的值应该是1。
- 所以在构造函数内,尽量避免使用实例方法,除了final和private方法。
结论:
“用尽量简单的方式使对象进入可工作状态”, 尽量不要在构造器中调用方法(如果这个方法被子类重写, 就会触发动态绑定, 但是此时子类对象还没构造完成), 可能会出现一些隐藏的但是又极难发现的问题