本篇主要介绍Java中的多态
其实不是很难,就是一些语法知识,大家掌握了就会觉得其实不难。
👩🏻🏫作者: 初入编程的菜鸟哒哒
📚系列文章目录:一、TvT——JavaSE(1)–Java的数据类型与变量
二、TvT——JavaSE(2)–Java的运算符
三、TvT——JavaSE(3)–Java的逻辑控制
四、TvT——JavaSE(4)–Java方法的使用
五、你真的学会了JavaSE中数组的定义与使用吗?
六、Java中的类与对象来袭
七、类与对象有关static成员的知识你真的了解吗?
八、有关代码块的知识你真的了解吗?
九、初步学会JavaSE内部类知识
十、java继承真的一点也不难
📜刷题笔记:
📖TvT——C语言进阶の刷题【1】:指针进阶
📖面试高频题の环形链表 环形链表Ⅱ 全解析
文章目录
- 多态
- 🎠1. 多态的概念
- 🎠2. 向上转型和向下转型
- 🥼2.1 向上转型
- 🥼2.2 向下转型
- 🎠3.重写
- 🎠4.动态绑定
- 🎠5.多态
- 🎠6. 多态的优缺点
- 总结
多态 🎠1. 多态的概念
通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
比如动物都需要吃东西,而狗对吃表现出来的是吃狗粮,而猫是吃猫粮。
直接来说就是:
同一件事情,发生在不同对象身上,就会产生不同的结果。
要学习多态,我们首先要了解向上转型。
🥼2.1 向上转型向上转型:
实际就是创建一个子类对象,将其当成父类对象来使用。 |
举个栗子🌰:
class Animal{
public String name;
public int age;
public void eat() {
System.out.println(name+" 正在吃(animal)");
}
}
class Cat extends Animal{
public void liquid() {
System.out.println("猫"+name+"是液体");
}
}
class Pig extends Animal{
public void eated() {
System.out.println("猪"+name+"会被宰了吃肉");
}
}
public class TestDemo {
public static void main(String[] args) {
Animal cat = new Cat();
Animal pig = new Pig();
}
}
Cat
和Pig
都是Animal
的子类,但是我在main
方法中创建(new
)了子类对象,却给它了一个父类的引用,将它当成父类对象来使用,Cat
和Pig
都发生了向上转型。
可以简单记为猫是动物所以用猫用动物接收也没问题!
❓ 为什么向上转型相对安全呢?
因为是小范围到大范围的转换,如果没有就可以直接报错。
有三种使用方法,上面是第一种直接赋值
第二种:方法传参
传过去一个子类用父类接收。
public class TestAnimal {
public static void eatFood(Animal a){
a.eat();
}
public static void main(String[] args) {
Animal cat = new Cat();
eatFood(cat);
}
第三种:作返回值
返回任意子类对象,用父类接收
public static Animal buyAnimal(String var){
if("猪" == var){
return new Pig();
}else if("猫" == var){
return new Cat();
}else{
return null;
}
}
public static void main(String[] args) {
animal = buyAnimal("猫");
animal.eat();
}
🥼2.2 向下转型向上转型的优点:让代码实现更简单灵活。
向上转型的缺陷:不能调用到子类特有的方法。
将一个子类对象经过向上转型之后当成父类方法使用,再无法调用子类的方法,但有时候可能需要调用子类特有的方法,此时:
将父类引用再还原为子类对象即可,即向下转换。 |
什么情况下会用到向下转型呢?
举个栗子🌰:
但是向下转型不安全。
第一种情况是编译器报错:
还有这样的情况,不会报错但却会抛异常:
为了解决这种问题,Java中为了提高向下转型的安全性,引入了 instanceof
,如果该表达式为true
,则可以安全转换。
比如我们上面的代码,Animal
中有eat
方法,但是猫和猪吃的东西又不同,我们怎么实现把这两个吃的东西区分开呢?
我们尝试在Cat
里面也写上eat
方法:
因为父类中有eat
方法,而我们在子类中又写了一个eat
方法,这就叫做重写。
重写是返回值和形参都不能改变。即外壳不变,核心重写!
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+" 正在吃(animal)");
}
}
class Cat extends Animal{
public Cat(String name, int age) {
super(name, age);
}
@Override
public void eat() {
System.out.println(name+"吃猫条");
}
public void liquid() {
System.out.println("猫"+name+"是液体");
}
}
满足方法的重写需要的条件:
- 子类在重写父类的方法时,一般必须与父类方法原型一致:
修饰符 返回值类型 方法名(参数列表)
要完全一致- 方法的参数列表相同【个数、类型、顺序】
- 方法的返回值相同(子类的返回值和父类返回值是父子类关系也可以【协变类型】 )
static
的方法不能被重写private
修饰的方法不能被重写。
重写的方法, 可以使用@Override
注解来显式指定。
有了这个注解能帮我们进行一些合法性校验。
例如不小心 将方法名字拼写错了 (比如写成aet
), 那么此时编译器就会发现父类中没有aet
方法, 就会编译报错, 提示无法构成重写。- 子类的访问修饰符需要大于等于父类的访问修饰符
例如:如果父类方法被public
修饰,则子类中重写该方法就不能声明为protected
重写和重载的区别:
区别点 | 重载(override) | 重写(override) |
---|---|---|
参数列表 | 必须修改 | 一定不能修改 |
返回类型 | 可以修改 | 一定不能修改 |
访问限定符 | 可以修改 | 一定不能做更严格的限制(可以降低限制) |
即:方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现
如果这个方法不想被重写只需要加上一个final
:
public final void eat() {
System.out.println(name+" 正在吃(animal)");
}
这样这个eat
方法就不能被重写了,这个eat
方法是一个密封方法。
这时我们用向上转化了的cat
调用eat
方法:
public static void main(String[] args) {
Animal cat = new Cat("猫猫",1);
cat.eat();
}
那实现的是Animal
的吗?还是Cat
的呢?
答案是Cat
的,为什么?
因为发生了动态绑定。
在Powershell
窗口来反编译一下字节码文件:
我们发现在编译的时候还是调用父类自己的方法,但是在运行的时候,程序发生了动态绑定,最后调用了自己的方法。
那动态绑定是怎么做到的(稍稍了解)?
调用方法得通过地址找到这个方法。
假设在内存中是这样的:
理论上运行的时候应该找Animal
的地址,但是在运行的时候悄悄发生了动态绑定,把Animal.eat()
的地址改变了:
这就大概完成了动态绑定。
动态绑定也称为后期绑定(晚绑定),即在编译时,不能确定方法的行为,需要等到程序运行时,才能够确定具体调用那个类的方法。
🎠5.多态讲到这里,其实大家已经知道多态是什么了,也知道多态实现出来的效果是怎么样的了。
多态其实就是:
在代码运行时,当传递不同类对象时,会调用对应类中的方法。 |
🌰那我们再举一个多态的实例:
我们可以直接通过数组来画图形:
class Shape {
//属性....
public void draw() {
System.out.println("画图形!");
}
}
class Pig extends Shape{
@Override
public void draw() {
System.out.println("🐖");
}
}
class Dog extends Shape{
@Override
public void draw() {
System.out.println("🐕");
}
}
class Bird extends Shape{
@Override
public void draw() {
System.out.println("🐦");
}
}
class Duck extends Shape{
@Override
public void draw() {
System.out.println("🦆");
}
}
public class Test {
public static void drawMap3( ) {
//猪 狗 鸟 鸭子 猪
Pig pig = new Pig();
Dog dog = new Dog();
Bird bird = new Bird();
Duck duck = new Duck();
Shape[] shapes = {pig, dog, bird, duck, pig};
for (Shape shape : shapes) {
//当前shape引用的对象重写draw方法就调用,没有就调用Shape自己的
shape.draw();
}
}
public static void main(String[] args) {
drawMap2();
}
public static void drawMap(Shape shape) {
shape.draw();
}
}
运行结果:
是不是很巧妙呢?这就是多态的巧妙之处!!
🎠6. 多态的优缺点🥪使用多态的好处:
- 能够降低代码的
"圈复杂度"
, 避免使用大量的if - else
什么叫 “圈复杂度” ?
圈复杂度是一种描述一段代码复杂程度的方式。一段代码如果平铺直叙, 那么就比较简单容易理解。
而如果有很多的条件分支或者循环语句, 就认为理解起来更复杂。
因此我们可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数, 这个个数就称为 “圈复杂度”。
如果一个方法的圈复杂度太高, 就需要考虑重构。
不同公司对于代码的圈复杂度的规范不一样,一般不会超过 10 。
例如我们现在需要打印多个形状,如果不基于多态, 实现代码如下:
可以看见有多个if-else
,圈复杂度较高。
public static void drawMap2( ) {
//猪 狗 鸟 鸭子 猪
Pig pig = new Pig();
Dog dog = new Dog();
Bird bird = new Bird();
Duck duck = new Duck();
String[] shapes = {"pig", "dog", "bird", "duck", "pig"};
for (String shape : shapes) {
if (shape.equals("pig")) {
pig.draw();
} else if (shape.equals("dog")) {
dog.draw();
} else if (shape.equals("bird")) {
bird.draw();
}else if(shape.equals("duck")) {
duck.draw();
}
}
}
而使用多态的思想,代码更加简单:
public static void drawMap3( ) {
//猪 狗 鸟 鸭子 猪
Pig pig = new Pig();
Dog dog = new Dog();
Bird bird = new Bird();
Duck duck = new Duck();
Shape[] shapes = {pig, dog, bird, duck, pig};
for (Shape shape : shapes) {
//当前shape引用的对象重写draw方法就调用,没有就调用Shape自己的
shape.draw();
}
}
- 可扩展能力更强
如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低
class Tiger extends Shape{
@Override
public void draw() {
System.out.println("🐅");
}
}
对于类的调用者来说,只要创建一个新类的实例就可以了, 改动成本很低。
而对于不用多态的情况, 就要把drawMap
中的 if - else
进行一定的修改, 改动成本更高。
多态缺陷:代码的运行效率降低。
总结欢迎分享,转载请注明来源:内存溢出
评论列表(0条)