【JAVA】java多态
多态和继承 封装 并成为面向对象的三大特性
一:多态的概念
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同 的状态。
总的来说:同一件事情,发生在不同对象身上,就会产生不同的结果。
1.1: 多态实现条件
在java中要实现多态,必须要满足如下几个条件,缺一不可:
1. 必须在继承体系下(必须要有子父类,java依赖、python的多态不依赖继承,c++中的静态多态不依赖继承,动态多态依赖继承)
2. 子类必须要对父类中方法进行重写
(重写:子类和父类的关系,父类中有一个方法,但子类中也提供同样的方法(参数列表,方法名字,返回值的类型要相容 都是一样的))1:类型完全相同 2:子类重写的方法的返回值类 是父类对应方法的返回值类型的子类。
3. 通过父类的引用调用重写的方法
public class Son { public void eat(){ System.out.println("Son 吃东西"); } }public class XiaoTang extends Son{ public void eat(){ System.out.println("xiaotang 爱吃米饭"); } } public class Yuanyuan extends Son{ public void eat(){ System.out.println("yuanyuan 在吃米饭"); } }public class Test { public static void main(String[] args) { Son son = new Son(); son.eat(); //使用父类的引用,指向一个字子类的实例 这样的语法是完全可以的 //XiaoTang xiaotang= new XiaoTang();意思相同 son=new XiaoTang(); son.eat(); son = new Yuanyuan(); son.eat(); //表面上看 都是son.eat但要看执行过程中 执行了完全不同的逻辑 //具体son.eat()执行那个逻辑 要结合上下文 明确son到底是谁。 } } 运行结果: //Son 吃东西 //xiaotang 爱吃米饭 //yuanyuan 在吃米饭
如果写成这样,这个就不是多态,这个就是引用
分析一下son.eat:
代码执行到这个地方,jvm就会在运行的过程中,分析son真实指向的对象是什么类型,进一步的调用匹配的类型方法(eat方法执行那个版本,是程序运行来确定,不是编译过程确定的。)
编译过程:就是生成字节码的过程
运行时:字节码生成好之后,运行来确定执行那个调用,那个方法。
java中的多态,就是基于动态绑定,这样的机制来实现,
在上述代码中, 分割线上方的代码是 类的实现者 编写的, 分割线下方的代码是 类的调用者 编写的.
当类的调用者在编写 eat 这个方法的时候, 参数类型为 Animal (父类), 此时在该方法内部并不知道, 也不关注当前的a 引用指向的是哪个类型(哪个子类)的实例. 此时 a这个引用调用 eat方法可能会有多种不同的表现(和 a 引用的实例相关), 这种行为就称为 多态.另外一种写法:
1.2:重写
重写(override):父类子类,方法名返回值参数都一样
也称为覆盖。重写是子类对父类非静态、非private修饰,非final修饰,非构造方法等的实现过程
进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写!重写的好处在于子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法。
2overrides,该方法被两次重写
【方法重写的规则】
1子类在重写父类的方法时,一般必须与父类方法原型一致: 返回值类型 方法名 (参数列表) 要完全一致2被重写的方法返回值类型可以不同,但是必须是具有父子关系的
3访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类方法被public修饰,则子类中重写该方法就不能声明为 protected
4父类被static、private修饰的方法、构造方法都不能被重写。
5重写的方法, 可以使用 @Override 注解来显式指定. 有了这个注解能帮我们进行一些合法性校验. 例如不小心将方法名字拼写错了 (比如写成 aet), 那么此时编译器就会发现父类中没有 aet 方法, 就会编译报错, 提示无法构成重写.
【重写的设计原则】
对于已经投入使用的类,尽量不要进行修改。最好的方式是:重新定义一个新的类,来重复利用其中共性的内容,并且添加或者改动新的内容静态绑定:也称为前期绑定(早绑定),即在编译时,根据用户所传递实参类型就确定了具体调用那个方法。典型代表函数重载。
动态绑定:也称为后期绑定(晚绑定),即在编译时,不能确定方法的行为,需要等到程序运行时,才能够确定具体调用那个类的方法。
1.3: 向上转移和向下转型
1:向上转型
向上转型:实际就是创建一个子类对象,将其当成父类对象来使用。
语法格式:父类类型 对象名 = new 子类类型()
animal是父类类型,但可以引用一个子类对象,因为是从小范围向大范围的转换。
父类引用指向子类实例
左边父类 右边子类,
package object3; public class Test { public static void func(Animal animal){//(2) animal.eat(); } public static Animal creatAnimal(){//(3) return new Cat(); } public static void main(String[] args) { Animal animal = new Cat();//向上转型 animal.eat(); func(new Cat());//第二种情况 //第三种(3) //Animal animal= creatAnimal(); } }
2:向下转型
将一个子类对象经过向上转型之后当成父类方法使用,再无法调用子类的方法,但有时候可能需要调用子类特有的方法,此时:将父类引用再还原为子类对象即可,即向下转换。
子类有这个方法,但是父类没有,虽然animal确实时指向子类的实例,但是类型却是父类的类,编译过程中,无法找到meao的方法。
编译器编译的时候,看到的animal引用就是父类类型的引用,尝试在父类中寻找sleep,但是没有,在编译阶段,不知道animal指向的真实类型。
Animal animal = new Cat();//向上转型 Cat cat = (Cat) animal;//在向下转强转 cat.mewo();//然后调用
这样就解决了。
还有一个例子图上;
向下转型的时候,需要确保,当前这里的父类引用确实是指向子类实例的,如果不是就算强转,也无法调用里面的方法。
虽然没有报错但出现异常了
解决办法:
Animal animal = new Animal(); // Cat cat = (Cat) animal;//在向下转 // cat.mewo();//然后调用 //ClassCastException:为了避免出现这个异常,我们可以显式使用instanceof关键字进行类型判断 if(animal instanceof Cat){ Cat cat = (Cat) animal;//在向下转 cat.mewo(); }
1.4:多态的优缺点
优点:降低代码的圈复杂度、
圈复杂度:衡量一个代码/方法复杂程度的一种指标
如果中国代码从上到下直接执行完成,计算简单代码
随着代码中 条件越来越多 循环越来越多 代码理解起来更费劲
把一段代码中出现分支的个数=“称为圈复杂度”如果使用使用多态, 则不必写这么多的 if - else 分支语句, 代码更简单
Test类()测试类package object3.package2; public class Test { public static void main(String[] args) { //给定一个数组,表示要打印的性状序列 // String[] shapes={"circle","rect","rect","flower"}; // for(String shapeName:shapes){//for-each // if(shapeName.equals("circle")){//比较两个字符串相等,需要使用equals,不能使用== // Shape shape=new Cycle(); // shape.draw(); // } else if (shapeName.equals("rect")) { // Shape shape =new Rect(); // shape.draw(); // } else if (shapeName.equals("flower")) { // Shape shape=new Flower(); // shape.draw(); // } //} //基于多态,优化上述代码 Shape[] shapes={new Cycle(),new Rect(),new Rect(),new Flower(),new Triangle()}; //由于此处的Cycle rect flower类类型是Shape的子类,子类的引用可以放到父类的数组当中 for(Shape shape :shapes){ //此时不用写太多if else 直接通过动态绑定,调用对应的draw从而绘制 shape.draw(); } } } public class Triangle extends Shape{ public void draw(){ System.out.println("▲"); } } public class Flower extends Shape { public void draw(){ System.out.println("❀"); } } public class Cycle extends Shape{ public void draw(){ System.out.println("⚪"); } } public class Rect extends Shape { public void draw(){ System.out.println("□"); } } 父类 public class Shape { public void draw(){ System.out.println("绘制形状"); } }
2:可扩展能力更强(最后的三角形类是手动添加)
如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低.
3:多态是使用者不光不需要知道类的内部空间,甚至都不需要知道这个类的真实类型,就可以使用。缺点:程序运行过程中,消耗更多的时间,(运行效率低)和空间(消耗的内存更多)
多态绑定,需要一定开销。
但是利大于弊。
1.5:避免在构造方法中调用重写的方法
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 Test {public static void main(String[] args) {D d = new D();}}// 执行结果D.func() 0
构造 D 对象的同时, 会调用 B 的构造方法.
B 的构造方法中调用了 func 方法, 此时会触发动态绑定, 会调用到 D 中的 func
此时 D 对象自身还没有构造, 此时 num 处在未初始化的状态, 值为 0. 如果具备多态性,num的值应该是1.
所以在构造函数内,尽量避免使用实例方法,除了final和private方法。
结论: "用尽量简单的方式使对象进入可工作状态", 尽量不要在构造器中调用方法(如果这个方法被子类重写, 就会触发动态绑定, 但是此时子类对象还没构造完成), 可能会出现一些隐藏的但是又极难发现的问题
二:总结
本文主要介绍了面向对象编程中的多态特性。多态指同一行为在不同对象上产生不同结果,实现条件包括继承体系、方法重写和父类引用调用。文章详细讲解了方法重写规则、向上转型与向下转型的应用场景,并通过实例代码展示了多态的实现方式。同时分析了多态的优点(降低复杂度、提高扩展性)和缺点(运行效率稍低),特别强调了在构造函数中避免调用重写方法的注意事项。最后总结了多态是面向对象编程的重要特性,能有效提升代码的灵活性和可维护性。