多态
1.多态的概念:
通俗来说,就是多种形态,具体点就是去完成某个⾏为,当不同的对象去完成时会产⽣出不同的状态。
2.Animal 中的 eat 方法,Cat 子类和 Dog 子类都能调用 eat 方法。
3.例:
publicclassAnimal{publicStringname;publicintage;publicAnimal(Stringname,intage){this.name=name;this.age=age;}publicvoideat(){System.out.println(this.name+"正在吃饭...");}}父类和子类都有eat方法
publicclassDogextendsAnimal{publicDog(Stringname,intage){super(name,age);}publicvoidbark(){System.out.println(this.name+"正在汪汪叫......");}publicvoideat(){System.out.println(this.name+"正在吃狗粮......");}}4.如上面的例子:
- 方法名一样
- 参数一样【数据类型,个数,顺序】
- 返回值一样
就说这两个方法构成了重写
5.面试题:
重写和重载的区别
6.以在重写的方法前加上 @Override 表示该方法是重写的
也可以通过编译器生成,ctrl+o
一、多态实现条件
1.在Java中要实现多态,必须要满⾜如下⼏个条件,缺⼀不可:
- 必须在继承体系下
- ⼦类必须要对⽗类中⽅法进⾏重写
- 通过⽗类的引⽤调⽤重写的⽅法
2.要了解三个知识点,才能学习多态:
- 了解重写这个知识点
- 了解向上转型
- 了解动态绑定
二、重写
⼦类能够根据需要重新实现⽗类的⽅法。
1.【⽅法重写的规则】
• ⼦类在重写⽗类的⽅法时,⼀般必须与⽗类⽅法原型⼀致: 返回值类型 ⽅法名 (参数列表) 要完全⼀致
• 被重写的⽅法返回值类型可以不同,但是必须是具有⽗⼦关系的
特例:
publicAnimaleat(){System.out.println(this.name+"正在吃饭......");returnnull;}@OverridepublicDogeat(){System.out.println(this.name+"正在吃狗粮......");returnnull;}这里的返回值类型一个是 Animal,一个是 Dog,这两个类类型之间有继承关系,所以可以
• 子类方法的访问修饰限定符权限要大于等于父类
• ⽗类被static、private、final修饰的成员⽅法以及构造⽅法都不能被重写。
• 重写的⽅法, 可以使⽤ @Override 注解来显式指定. 有了这个注解能帮我们进⾏⼀些合法性校验.
例如不⼩⼼将⽅法名字拼写错了 (⽐如写成 aet), 那么此时编译器就会发现⽗类中没有 aet ⽅法, 就会编译报错, 提⽰⽆法构成重写。
2.【重写的设计原则】
对于已经投⼊使⽤的类,尽量不要进⾏修改。最好的⽅式是:重新定义⼀个新的类,来重复利⽤其中共性的内容,并且添加或者改动新的内容。
例如:若⼲年前的⼿机,只能打电话,发短信,来电显⽰只能显⽰号码,⽽今天的⼿机在来电显⽰的时候,不仅仅可以显⽰号码,还可以显⽰头像,地区等。在这个过程当中,我们不应该在原来⽼的类上进⾏修改,因为原来的类,可能还在有⽤户使⽤,正确做法是:新建⼀个新⼿机的类,对来电显⽰这个⽅法重写就好了,这样就达到了我们当今的需求了。
三、向上转型
1.实际就是创建⼀个⼦类对象,将其当成⽗类对象来使⽤。
2.语法格式:
⽗类类型 对象名 = new ⼦类类型()
例:
Animalanimal=newCat("元宝",2);例:
Dogdog=newDog(name:"旺财",age:2);Animalanimal=dog;Animalanimal=newDog(name:"旺财",age:2);//父类引用 引用了 子类对象父类引用 animal存的是子类对象的地址
3.animal是⽗类类型,但可以引⽤⼀个⼦类对象,因为是从⼩范围向⼤范围的转换(理论上等号两边的类型必须一致,但因为 Dog 继承了 Animal,所以赋值成功)
4.【使⽤场景】
- 直接赋值(上面的向上转型例子就是以赋值的形式进行的)
- ⽅法传参
- ⽅法返回
例:
// 2. ⽅法传参:形参为⽗类型引⽤,可以接收任意⼦类的对象publicstaticvoideatFood(Animala){//Animal a这个引用就间接指向子类对象a.eat();}publicstaticvoidmain(String[]args){Animalcat=newCat("元宝",2);// 1. 直接赋值:⼦类对象赋值给⽗类对象Dogdog=newDog("⼩七",1);eatFood(cat);eatFood(dog);}//3.返回值publicstaticAnimalfunc2(){returnnewDog(name:"旺财",age:2);}5.让父类引用 引用了子类对象 → 之所以可以,是因为 Dog 是 Animal 的子类,相当于是 大的类型 = 小的类型
6.例:
publicclassAnimal{publicStringname;publicintage;publicAnimal(Stringname,intage){this.name=name;this.age=age;}publicvoideat(){System.out.println(this.name+" 正在吃饭...");}}publicstaticvoidmain(String[]args){Animalanimal=newDog(name:"旺财",age:2);animal.bark();//当前 Animal 这个类当中没有 bark 方法animal.eat();}编译失败,编译时编译器将animal当成Animal对象处理
⽽Animal类中没有bark⽅法,因此animal.bark();编译失败
(bark方法是子类Dog的)
7.虽然父类引用 引用了子类对象,但是通过父类引用 只能访问父类自己的属性和方法
四、动态绑定
1.例:
publicclassDogextendsAnimal{@Overridepublicvoideat(){System.out.println(this.name+"正在吃狗粮……");}}publicstaticvoidmain(String[]args){Animalanimal=newDog("旺财",2);animal.eat();}
- 在刚才的代码当中,通过父类引用 只能调用父类自己的方法
- 此时子类继承父类之后 重写了父类的 eat 方法
- 现在父类引用 引用子类对象,通过父类引用会调用这个重写的方法
- 把此时这种调用 发生的行为:动态绑定 或者 运行时绑定
2.动态绑定的条件:
- 父类引用引用了子类对象
- 子类中重写了父类方法
- 通过父类引用去调用这个方法(此时会调用子类中重写的方法)
动态绑定只会针对方法,不会针对成员变量
1.中的例子中,@Override是注解,就算没写注解,通过父类引用,也会调用该方法,该方法仍然是重写的,注解只是说明
3.什么是多态:
例:
publicclassAnimal{Stringname;intage;publicAnimal(Stringname,intage){this.name=name;this.age=age;}publicvoideat(){System.out.println(name+"吃饭");}}publicclassCatextendsAnimal{publicCat(Stringname,intage){super(name,age);}@Overridepublicvoideat(){System.out.println(name+"吃鱼~~~");}}publicclassDogextendsAnimal{publicDog(Stringname,intage){super(name,age);}@Overridepublicvoideat(){System.out.println(name+"吃骨头~~~");}}publicclassTestAnimal{// 编译器在编译代码时,并不知道要调用Dog 还是 Cat 中eat的方法// 等程序运行起来后,形参a引用的具体对象确定后,才知道调用那个方法// 注意:此处的形参类型必须时父类类型才可以publicstaticvoideat(Animala){//看下面4.a.eat();}publicstaticvoidmain(String[]args){Catcat=newCat("元宝",2);Dogdog=newDog("小七",1);eat(cat);eat(dog);}}运⾏结果:
元宝吃鱼~~~
小七吃骨头~~~
4.当animal这个引用引用的对象不一样的时候,调用同一个方法 所表现的行为不一样,这就叫多态。
5.同一个引用 同一个方法
引用的对象不一样 调用同一个方法
所表现的行为不一样,我们把这种思想 就称之为多态
6.
1.向上转型 2. 发生重写 3. 动态绑定
(所以说多态发生了这三种情况)
7.动态绑定:
编译的时候 认为还是调用 Animal 的 eat 方法
但是在运行的时候 运行的时候 才知道 我真正要调用的是哪个方法
8.例:
add(1,2);add(1,2,3);当 add (1,2,3) 时,编译时就知道调用的是有 3 个参数的 add 方法,这叫静态绑定
五、多态的优缺点
1.例:
classShape{//属性....publicvoiddraw(){System.out.println("画图形!");}}classRectextendsShape{@Overridepublicvoiddraw(){System.out.println("♦");}}classCycleextendsShape{@Overridepublicvoiddraw(){System.out.println("●");}}classFlowerextendsShape{@Overridepublicvoiddraw(){System.out.println("❀");}}2.【使⽤多态的好处】
(1)能够降低代码的 “圈复杂度”, 避免使⽤⼤量的 if - else
什么叫 “圈复杂度” ?
圈复杂度是⼀种描述⼀段代码复杂程度的⽅式. ⼀段代码如果平铺直叙, 那么就⽐较简单容易理
解. ⽽如果有很多的条件分⽀或者循环语句, 就认为理解起来更复杂.因此我们可以简单粗暴的计算⼀段代码中条件语句和循环语句出现的个数, 这个个数就称为 “圈复
杂度”. 如果⼀个⽅法的圈复杂度太⾼, 就需要考虑重构.
例如我们现在需要打印的不是⼀个形状了, ⽽是多个形状. 如果不基于多态, 实现代码如下:
publicstaticvoiddrawShapes(){Rectrect=newRect();Cyclecycle=newCycle();Flowerflower=newFlower();//就相当于 int [] array = {1,2,1,2,3}String[]shapes={"cycle","rect","cycle","rect","flower"};for(Stringshape:shapes){//44.相当于就是判断array中每个元素的值,是哪个值就执行对应语句if(shape.equals("cycle")){cycle.draw();}elseif(shape.equals("rect")){rect.draw();}elseif(shape.equals("flower")){flower.draw();}}}如果使⽤使⽤多态, 则不必写这么多的 if - else 分⽀语句, 代码更简单:
publicstaticvoiddrawShapes(){// 我们创建了一个 Shape 对象的数组.//发生了向上转型Shape[]shapes={newCycle(),newRect(),newCycle(),newRect(),newFlower()};for(Shapeshape:shapes){shape.draw();}}Shape[] shapes = {new Cycle(), new Rect(), new Cycle(),
new Rect(), new Flower()};
也可以这样写:
Shape [] shapes = new Shape [3];
shapes [0] = new Flower ();
shapes [1] = new Rect ();
shapes [2] = new Cycle ();
(2)可扩展能⼒更强
1.如果要新增⼀种新的形状, 使⽤多态的⽅式代码改动成本也⽐较低
例:
classTriangleextendsShape{@Overridepublicvoiddraw(){System.out.println("△");}}直接新增三角形子类即可
五、避免在构造⽅法中调⽤重写的⽅法
1.例:
classB{publicB(){// do nothingfunc();}publicvoidfunc(){System.out.println("B.func()");}}classDextendsB{privateintnum=1;@Overridepublicvoidfunc(){System.out.println("D.func() "+num);}}publicclassTest{publicstaticvoidmain(String[]args){Dd=newD();}}父类和子类都有 func 方法
此时在main方法中写D d = new D();此时会调用父类的构造方法
2.在父类的构造方法中若调用了子类和父类同名的方法,则会发生动态绑定,会调用子类的方法
3.总结:
• 构造 子类 对象的同时, 会调⽤ 父类的构造⽅法.
• 父类的构造⽅法中调⽤了 func ⽅法, 此时会触发动态绑定, 会调⽤到 子类 中的 func
• 但此时 子类 对象⾃⾝还没有构造, 所以此时 num 处在未初始化的状态, 值为 0.
因为必须先执行父类构造方法,再执行子类构造方法,现在执行父类构造方法时就要调用子类中的方法,但此时子类相关的代码还没开始执行,所以子类中 num 被赋值为 1 的操作还没执行
• 所以在构造方法内,尽量避免使⽤实例⽅法,除了final和private⽅法。
六、向下转型
1.将⼀个⼦类对象经过向上转型之后当成⽗类⽅法使⽤,此时就⽆法调⽤⼦类的⽅法,但有时候可能需要调⽤⼦类特有的⽅法,此时:将⽗类引⽤再还原为⼦类对象即可,即向下转换。(需要强转)
向上转型了之后,才能进行向下转型
2.例:
publicclassTestAnimal{publicstaticvoidmain(String[]args){Catcat=newCat("元宝",2);Dogdog=newDog("小七",1);// 向上转型Animalanimal=dog;//把父类类型给到了子类类型//现在要强制还原为猫,⽆法正常还原,运⾏时抛出:ClassCastExceptioncat=(Cat)animal;cat.mew();// animal本来指向的就是狗,因此将animal还原为狗也是安全的dog=(Dog)animal;dog.bark();}}因为不是所有动物都是猫,要看此时父类 animal 引用的是哪个类型的对象
3.向下转型⽤的⽐较少,⽽且不安全,万⼀转换失败,运⾏时就会抛异常。
4.Java中为了提⾼向下转型的安全性,引⼊了instanceof,如果该表达式为true,则可以安全转换。
例:
publicclassTestAnimal{publicstaticvoidmain(String[]args){Catcat=newCat("元宝",2);Dogdog=newDog("小七",1);// 向上转型Animalanimal=cat;if(animalinstanceofCat){cat=(Cat)animal;cat.mew();}if(animalinstanceofDog){dog=(Dog)animal;dog.bark();}}}animal instanceof Cat 的意思是:
animal 是否引用了 Cat 这个类型的对象
以后如果用到向下转型,就必须写这些