找往期文章包括但不限于本期文章中不懂的知识点:
个人主页:我要学编程(ಥ_ಥ)-CSDN博客
所属专栏:JavaSE
多态篇
目录
多态的概念
实现多态的条件
必须在继承体系下实现向上转型:
子类必须对父类中的方法进行重写:
通过父类引用调用重写方法:
多态的优缺点:
避免在父类的构造方法中调用重写的方法
多态的概念
通俗来说,就是多种形态,具体点就是去完成某个行为时,当不同的对象去完成时会产生出不同 的状态。
例如:同样是吃早餐,这个人可能是吃包子,饺子;另外一个人却是吃面条。这就是不同的对象去完成同一件事情时所表现出来的状态不同。
实现多态的条件
既然了解了什么是多态,接下来要知道什么情况下可以实现多态。
要想实现多态得满足以下三个条件:
1. 必须在继承体系下实现向上转型。
2. 子类必须对父类中的方法进行重写。
3. 通过父类引用调用重写方法。
下面就来解释这三个条件。
必须在继承体系下实现向上转型:
就是指一个是子类,一个是父类,然后把子类对象给到父类的引用。 注意:这里的体系,说明不一定是要在直系继承关系下,可以是通过中间类间接继承。
例如:
向上转型:把子类对象给到父类的引用。
向下转型:将一个子类对象经过向上转型之后当成父类方法使用,再无法调用子类的方法,但有时候可能需要调用子类特有的 方法,此时:将父类引用再还原为子类对象(强制类型转换)即可,即向下转换。简单理解就是把父类对象给到子类的引用。
结合该图理解
向上转型的语法格式:父类类型 名称 = new 子类类型(); 向下转型的语法格式与其差不多。
向上(向下)转型的应用场景:直接赋值、作为方法的参数、作为方法的返回值。、
例如:
//向上转型public class Test { public static void func1(Animal animal) { System.out.println("向上转型的场景之一:方法参数"); } public static Animal func2() { System.out.println("向上转型的应用场景之一:方法的返回值"); Dog dog = new Dog("大黄", 5); return dog; //return new Dog("大黄", 5); //上面两种写法都可以 } public static void main(String[] args) { Animal animal = new Dog("大黄", 8);//直接赋值 animal.eat(); func1(new Dog("大黄", 5));//也可以先实例化一个对象,再传参 func2(); }}
向上转型的优点:让代码实现更简单灵活。
向上转型的缺陷:不能调用到子类特有的方法。
向下转型相较于向上转型而言很不安全,因为向下转型的基础是向上转型,再把父类对象给到子类引用,而子类可能不是只有一个,但是父类只有一个。
例如:狗类对象经过向上转型给到父类引用,再把这个父类引用当成对象给到猫类引用,这时就会报错。因为不安全,这也就是为什么向下转型不安全?因为不确定是原来的子类。
public class Test { public static void main(String[] args) { //向上转型 Animal animal = new Dog("大黄", 5); //再把父类引用给到子类引用 Cat cat = animal; }}
这时就只能通过强制类型转换达到我们的目的。
很遗憾的是:强制类型转换也是做不到的。
那怎么样才能实现向下转型呢?很简单,通过同类型子类来转换。
例如:先把Dog类型进行向上转型给到animal引用,再把animal引用强制转换为Dog类型再向下转型给Dog引用。
子类必须对父类中的方法进行重写:
首先,得了解什么是重写?
重写(override):也称为覆盖,是子类对父类中非静态、非private修饰、非final修饰、非构造的方法进行重新编写。重写之后的方法与原来的方法相比:在参数列表、方法名、返回值都要一致。即外壳不变,核心重写。
例如:
注意:
1. 当重写的方法返回值与原来的方法返回值构成父子(继承)关系时,这时返回值就可以不一样。这里的父子(继承)关系同样可以不是直系关系。
例如:
上面特殊情况这种在Java叫:协变类型。
2. 子类中的重写方法的访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类方法被public修饰,则子类中重写该方法就不能声明为 protected。
3. 重写的方法, 可以使用 @Override 注解来显式指定。有了这个注解能帮我们进行一些合法性校验。例如不小心将方法名字拼写错了 (比如写成 aet), 那么此时编译器就会发现父类中没有 aet 方法,就会编译报错,提示无法构成重写。
这个重写和我们前面在学习方法的重载时有点类似。下面就来比较两者之间的区别。
区别处 | 方法重写(override) | 方法重载(overload) |
参数列表 | 必须一样 | 必须不一样 |
返回值 | 除继承关系外,其余时都得一致 | 未规定(可以一样,可以不一样) |
适用范围 | 存在继承关系的类中 | 所有类 |
目的性 | 实现多态性,子类可以根据自身需求对父类方法进行特定实现 | 提供多个功能相似但参数不同的方法,方便调用者根据不同情况选择合适的方法 |
简单理解:方法重载是一个类的不同表现,而方法重写是子类与父类的一种不同表现。
【重写的设计原则】 对于已经投入使用的类,尽量不要进行修改。最好的方式是:重新定义一个新的类,来重复利用其中共性的内容, 并且添加或者改动新的内容。
例如:若干年前的手机,只能打电话,发短信,来电显示只能显示号码,而今天的手机在来电显示的时候,不仅仅 可以显示号码,还可以显示头像,地区等。在这个过程当中,我们不应该在原来老的类上进行修改,因为原来的类,可能还在有用户使用,正确做法是:新建一个新手机的类,对来电显示这个方法重写就好了,这样就达到了我们当今的需求了。
第二点,我们刚刚也在代码中体会到了:何为子类必须对父类的方法进行重写。
通过父类引用调用重写方法:
class Animal { public String name; public int age; public void eat() { System.out.println(this.name+" 正在吃放!"); }}class Dog extends Animal{ @Override public void eat() { System.out.println(this.name+" 正在吃狗粮!"); } public Dog(String name, int age) { super(); this.name = name; this.age = age; }}public class Test { public static void main(String[] args) { Animal animal = new Dog("大黄", 8); animal.eat(); }}
上面三步完成之后,就会发生动态绑定。 而动态绑定是多态的基础。
静态绑定:也称为前期绑定(早绑定),即在编译时,根据用户所传递实参类型就确定了具体调用那个方法。典型代表方法重载。
动态绑定:也称为后期绑定(晚绑定、运行时绑定),即在编译时,不能确定方法的行为,需要等到程序运行时,才能够确定具体调用那个类的方法。典型代表多态。
多态的优缺点:
1. 能够降低代码的 "圈复杂度", 避免使用大量的 if - else 。
什么叫 "圈复杂度" ? 圈复杂度是一种描述一段代码复杂程度的方式。一段代码如果平铺直叙, 那么就比较简单容易理解。而如果有很多的条件分支或者循环语句, 就认为理解起来更复杂. 因此我们可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数, 这个个数就称为 "圈复杂度"。如果一个方法的圈复杂度太高, 就需要考虑重构。不同公司对于代码的圈复杂度的规范不一样,一般不会超过 10 。
例如:我们现在需要打印的不是一个形状了,而是多个形状。如果不基于多态,我们就只能通过if - else语句来进行打印。
//画一个圆class Round { public void draw() { System.out.println("○"); }}//画一个正方形class Square { public void draw() { System.out.println("□"); }}//画一个三角形class Triangular { public void draw() { System.out.println("△"); }}public class Test { public static void main(String[] args) { Round round = new Round(); Square square = new Square(); Triangular triangular = new Triangular(); String[] shapes = {"round", "square", "triangular"}; //开始判断需要画什么样的图形 for (String shape:shapes) { if (shape.equals("round")) { round.draw(); }else if (shape.equals("square")) { square.draw();; }else if (shape.equals("triangular")) { triangular.draw(); } }}
在Java中,判断两个字符串是否相等,应该使用 equals()
方法。这个方法比较的是字符串的内容,即两个字符串所包含的字符序列是否完全相同。这里有一个简单的示例:
//语法格式:字符串1.equals(字符串2); —— 得到的结果是布尔类型public class Test { public static void main(String[] args) { String str1 = "hello"; String str2 = "hello"; String str3 = "world"; boolean areEqual = str1.equals(str2); //使用equals()方法比较字符串内容 System.out.println("str1 和 str2 是否相等? " + areEqual); areEqual = str1.equals(str3); //比较str1和str3 System.out.println("str1 和 str3 是否相等? " + areEqual); }}
注意:使用 ==
操作符是比较两个字符串对象的引用是否相同,而不是比较它们的值是否相等。因此,通常情况下不推荐使用 ==
来比较字符串内容。只有在确保两个字符串引用指向同一对象时(例如,它们都是同一个字符串字面量的引用),使用 ==
才能得出正确的结果。
基于多态来打印图形。
//画一个图形的父类class Shape { public void draw() { System.out.println("画一个图形!"); }}//画一个圆class Round extends Shape{ //重写父类方法 @Override public void draw() { System.out.println("○"); }}//画一个正方形class Square extends Shape{ //重写父类方法 @Override public void draw() { System.out.println("□"); }}//画一个三角形class Triangular extends Shape{ //重写父类方法 @Override public void draw() { System.out.println("△"); }}public class Test { public static void main(String[] args) { //向上转型 Shape[] shapes = {new Round(), new Square(), new Triangular()}; for (Shape shape : shapes) { shape.draw();//通过父类引用调用重写方法 } }}
这里的shape.draw();就相当于shapes[i].draw。也就是通过父类引用调用重写方法。
2. 可扩展能力更强 如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低。
只需要新增一个类就可以了,而不使用多态的话,就得新增if-else语句来判断。
多态缺陷:代码的运行效率降低。
1. 属性没有多态性 当父类和子类都有同名属性的时候,通过父类引用,只能引用父类自己的成员属性。
2. 构造方法没有多态性。
避免在父类的构造方法中调用重写的方法
class A { public A() { //在父类的构造方法中调用被重写的方法会发生动态绑定 func(); } public void func() { System.out.println("这是父类A的方法"); }}class B extends A { @Override public void func() { System.out.println("这是子类B的方法"); }}public class Test { public static void main(String[] args) { B b = new B(); }}
通过前面的学习,我们已经知道了:在实例化子类对象时,先帮助父类执行构造方法。
而在父类中的构造方法中,如果调用了被重写的方法,此时就会发生动态绑定。
注意:如果此时子类重写的方法中存在成员变量,这个成员变量不会被初始化成我们想要的值,但是会有默认值。因为在调用子类的重写方法时,父类的构造方法都没有完成,自然其他的都还没有进行的。
可能有小伙伴有疑惑:哪里有通过父类引用调用被重写的方法?哪里有发生向上转型?
至于提到的“哪里有通过父类引用调用被重写的方法”,这通常不是直接在构造方法上下文中讨论的,而是更普遍地出现在多态的应用场景中。但构造方法内部调用非静态、非私有、非final的方法时,实际上也间接体现了这一点,因为尽管直接调用看似是父类方法,但由于动态绑定,最终执行的是子类的方法体。这里的关键是理解,即使调用点在父类构造器内,实际的对象身份(即内存中的对象)已经是子类的实例。
至于“哪里有发生向上转型”,向上转型是当你用父类的引用指向子类的对象时自然发生的现象。在构造方法调用的场景中,虽然没有直接的显式向上转型(比如 Parent p = new Child();
),但实际上,当子类构造方法调用父类构造方法时,可以视为一种隐式的向上转型,因为父类构造器中的this
引用在构造子类对象的上下文中代表了一个子类实例。在这个过程中,父类构造器内的代码通过这个this
引用间接地操作了子类对象,而当它调用一个可被子类重写的方法时,就涉及到了动态绑定,这也是向上转型和动态绑定交互作用的一个体现。
其实暂时就只要知道不要在父类的构造方法中调用被重写的方法就可以了。随着我们学习的深入就会理解了。
好啦!本期初始Java篇(JavaSE基础语法)(6)(继承和多态)(下)的学习之旅就到此结束了!下一期我们再一起学习吧!