java 面向对象三大特性之多态 万字详解(超详细)
目录
前言 :
Hi,guys.这篇博文是Java面向对象三大特性篇的第三篇———多态篇。之前我们就一直强调,面向对象三大特性中,封装是继承的前提,继承是多态的前提。多态也是实际开发中运用最多,最广泛的一个特性!因此,多态的重要性不言而喻。本篇博文,内容包括但不限于多态的介绍,多态的使用详解,多态中的类型转化机制,动态绑定机制详解,抽象类详解,final关键字、static关键字详解,接口详解等等,感谢阅读!
这篇博文算是三大特性篇的最后一篇😎,之后up会将三大特性对应的三篇博客合并起来,并在此基础之上写一篇总结Java面向对象的十万+字博文,力争以最通俗易懂的方式让初学者快速入门并理解面向对象的灵魂。
还有是CSDN貌似不欢迎长文章😂,因此up计划将这篇博文中部分内容比较长的知识点,单独再出篇博文,然后以插入链接的方式呈现给大家。谢谢理解,感谢阅读!
一、为什么需要多态 :
1.白璧微瑕 :
继承的使用,给我们带来了极大的便利,不但提高了代码的可维护性,而且提高了代码的复用性,使我们免于敲写过多繁冗重复的代码。然而,甘瓜苦蒂,天下物无全美!我们承认,继承已经是个很🐂🖊的特性了。但在某些情况下,继承也会显出它的颓势:
2.举栗(请甘雨,刻晴,钟离吃饭):
这不,马上海灯节就要到了。 旅行者答应要请刻晴,钟离,和甘雨一起去新月轩吃晚饭。为了让大家开心,旅行者提前了解了三个人喜爱的食物 : 刻晴喜欢吃金丝虾球,钟离喜欢吃豆腐,甘雨则喜欢吃清心。 现在让你用Java来描述这件事情,你怎么做?
思路 :
①首先,刻晴,钟离和甘雨是三个不同的对象,因此我们需要分别定义三个类来模拟和描述刻晴,钟离,甘雨。同理,金丝虾球,杏仁豆腐和清心也是三个不同的对象,因此也需要分别定义三个类来模拟这三种食物。而三类人,三类食物均有共同属性;且刻晴,钟离和甘雨均有各自特有的行为;因此考虑使用封装和继承特性来实现。
②假设刻晴,钟离和甘雨都是璃月人,那么我们就可以定义一个父类来表示璃月人,然后让表示刻晴,钟离和甘雨的子类去继承表示璃月人的父类。
同理 : 假设金丝虾球,豆腐,和清心都属于料理,那么我们就可以定义一个父类来表示料理,然后让表示金丝虾球,豆腐和清心的子类去继承表示料理的父类。
③根据假设,先定义一个Liyue_people类,然后再分别定义Keqing类,Zhongli类和Ganyu类,并让它们继承Liyue_people类。同时,在Keqing类,Zhongli类和Ganyu类中定义它们的特有方法。其中 :
Keqing类特有方法——天街巡游 : sky_street_cruise()
Zhongli类特有方法——天星 : sky_stars()
Ganyu类特有方法——降众天华 : descend_to_heaven()
④同样根据假设,先定义Cooking类,然后再分别定义Shrimp_balls类,Bean_curd类和Qingxin类,并让它们继承Cooking类。注意 : 每种料理都要有名字,营养,和味道三个属性。假设新月轩在海灯节期间会稿活动,部分菜肴有优惠,我们需要在Cooking类中定义一个cooking_info()方法,来打印出料理的基本信息(菜品名,营养,风味等)。
⑤定义Traveler类(旅行者类),并在Traveler类中分别定义方法名为my_treat的三个重载方法,第一个方法需要传入一个刻晴类对象和一个金丝虾球对象;第二个方法需要传入一个钟离类对象和一个豆腐类对象;第三个方法则需要传入一个甘雨类对象和一个清心类对象。利用方法重载可以实现请不同对象吃饭的需求。
⑥最后定义Treat类进行测试。在Treat类先创建好刻晴,钟离和甘雨的吃饭对象,以及金丝虾球,豆腐和清心的料理对象。之后再定义旅行者类对象,并调用旅行者类中的my_treat方法(),传入对象参数即可成功请客吃饭,过一个完美的海灯节。
关系图(如下) :
3.代码 :
为了简洁,up直接将Keqing类,Zhongli类和Ganyu类都写在Liyue_people类的源文件中,将Shrimp_balls类,Bean_curd类和Qingxin类都写在Cooking类的源文件中。
Liyue_people类,Keqing类,Zhongli类和Ganyu类代码如下 :
package knowledge.polymorphism.introduction;
//父类 : 璃月人类
public class Liyue_people {
//成员变量
private String name;
private int age;
private String gender;
//构造器
public Liyue_people() {
}
public Liyue_people(String name, int age, String gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
//setter,getter方法
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
}
//子类 : 刻晴类
class Keqing extends Liyue_people {
public Keqing(String name, int age, String gender) {
super(name, age, gender);
}
//刻晴————天街巡游
public void sky_street_cruise() {
System.out.println("剑光如我,斩尽芜杂!");
}
}
//子类 : 钟离类
class Zhongli extends Liyue_people {
public Zhongli(String name, int age, String gender) {
super(name, age, gender);
}
//钟离————天星
public void sky_stars() {
System.out.println("天动万象!");
}
}
//子类 : 甘雨类
class Ganyu extends Liyue_people {
public Ganyu(String name, int age, String gender) {
super(name, age, gender);
}
//甘雨————降众天华
public void descend_to_heaven() {
System.out.println("为了岩王帝君!");
}
}
Cooking类,Shrimp_balls类,Bean_curd类和Qingxin类代码如下 :
package knowledge.polymorphism.introduction;
//父类 : 料理类
public class Cooking {
//成员变量
private String food_name;
private String nutrition;
private String flavor;
//构造器
public Cooking() {
}
public Cooking(String food_name, String nutrition, String flavor) {
this.food_name = food_name;
this.nutrition = nutrition;
this.flavor = flavor;
}
//setter,getter方法
public String getFood_name() {
return food_name;
}
public void setFood_name(String food_name) {
this.food_name = food_name;
}
public String getNutrition() {
return nutrition;
}
public void setNutrition(String nutrition) {
this.nutrition = nutrition;
}
public String getFlavor() {
return flavor;
}
public void setFlavor(String flavor) {
this.flavor = flavor;
}
//打印出菜肴信息的方法
public void cooking_info() {
System.out.print("\t菜品: " + this.food_name);
System.out.print("\t\t营养:" + this.nutrition);
System.out.println("\t\t风味:" + this.flavor);
}
}
//子类 : 金丝虾球类
class Shrimp_balls extends Cooking {
public Shrimp_balls(String food_name, String nutrition, String flavor) {
super(food_name, nutrition, flavor);
}
}
//子类 : 豆腐类
class Bean_curd extends Cooking {
public Bean_curd(String food_name, String nutrition, String flavor) {
super(food_name, nutrition, flavor);
}
}
//子类 : 清心类
class Qingxin extends Cooking {
public Qingxin(String food_name, String nutrition, String flavor) {
super(food_name, nutrition, flavor);
}
}
Traveler类代码如下 :
package knowledge.polymorphism.introduction;
//旅行者类
public class Traveler {
//反正是个哑巴,要什么属性
//打工人行为 :
//1.请刻晴吃金丝虾球
public void my_treat(Keqing keqing, Shrimp_balls shrimpBalls) {
System.out.println("海灯节,👴" + "请" + keqing.getName() + "吃" + shrimpBalls.getFood_name());
}
//2.请钟离吃杏仁豆腐
public void my_treat(Zhongli zhongli, Bean_curd bean_curd) {
System.out.println("海灯节,👴" + "请" + zhongli.getName() + "吃" + bean_curd.getFood_name());
}
//3.请甘雨吃清心
public void my_treat(Ganyu ganyu, Qingxin qingxin) {
System.out.println("海灯节,👴" + "请" + ganyu.getName() + "吃" + qingxin.getFood_name());
}
}
Treat类代码如下 :
package knowledge.polymorphism.introduction;
public class Treat {
public static void main(String[] args) {
//1.创建要吃饭的角色对象
//创建刻晴对象
Keqing keqing = new Keqing("刻晴", 18, "女");
//创建钟离对象
Zhongli zhongli = new Zhongli("摩拉克斯", 6000, "男");
//创建甘雨对象
Ganyu ganyu = new Ganyu("王小美", 3000, "女");
//2.创建被吃的食物对象
//创建金丝虾球对象
Shrimp_balls shrimp_balls = new Shrimp_balls("金丝虾球", "蛋白质是🐂🥩的六倍!", "鲜香味美");
//创建杏仁豆腐对象
Bean_curd bean_curd = new Bean_curd("杏仁豆腐", "富含微生物", "清甜爽口");
//创建清心对象
Qingxin qingxin = new Qingxin("清心", "妹说就是0卡", "苦的一批");
//3.创建旅行者对象
Traveler traveler = new Traveler();
//4.打扫干净屋子再请客
System.out.println("===============欢迎来到新月轩===============");
System.out.println("---------------以下是本店的海灯节热门菜肴 : ");
shrimp_balls.cooking_info();
bean_curd.cooking_info();
qingxin.cooking_info();
System.out.println("\n旅行者:\"哎呀我去,整挺好,不用挑了!👴全都要!\"\n");
traveler.my_treat(keqing, shrimp_balls); // 传入刻晴对象和金丝虾球对象
traveler.my_treat(zhongli, bean_curd); // 传入钟离对象和杏仁豆腐对象
traveler.my_treat(ganyu, qingxin); // 传入甘雨对象和清心对象
}
}
Treat类代码如下 :
4.问题 :
不知道大家发现没有,旅行者每请一个角色吃饭,都得重新定义一个新的重载方法,现在才请了三五个角色吃饭,可能还觉得没那么麻烦。但是,提瓦特大陆上的角色成百上千啊。到时候难道要定义100个方法,1000个方法?既然都是请客,为什么不能在一个方法中实现呢。就拿方才我们举得栗子来打个比方,既然刻晴,钟离,和甘雨都是璃月人,为什么我们不直接定义一个方法来请璃月人吃饭?这样不就省事儿多了!
这时候我们开始认识到继承特性的一些弊端 : 只有继承的程度,无法实现诸如“旅行者直接请璃月人吃饭”的需求。那怎么办?害!铺垫一大堆废话,不就为了讲咱的多态么!这不就来了!
二、什么是多态 :
1.定义 :
所谓多态,其实字面意思就是多种状态,没那么复杂。在Java中,方法和对象,都可以体现出多态。
①对于方法,多态表现在方法重载和方法重写上。
方法重载 : 同一行为,形参不同则表现形式不同。
方法重写 : 同一行为,调用者不同则表现形式不同。
②对于对象,多态表现在同一对象在不同情况下表现出不同的状态或行为。对象不仅仅是体现多态,更重要的应用多态。因此,对象的多态就是Java多态的核心。
2.多态的实现步骤(重要) :
①有继承(或实现)关系。(继承是多态的前提。"实现"指实现类或接口,后面我们会讲到)
②有方法重写。
③父类引用指向子类对象。
对于第三条,要特别补充一些内容。我们先来举个栗子吧。
eg : 假设有Cat类继承了Animal类,如下所示 :
public class Animal {
public void eat() {}
}
class Cat extends Animal {
public void eat() {
System.out.println("🐱喜欢吃🥩");
}
}
那么父类引用指向子类对象即 :
Animal animal = new Cat();
Δ解释 :
在上面这行代码中,等号左面的animal是一个Animal类型的引用变量,但是,这个Animal类型的引用所指向的对象,即堆空间中真正的对象,却是一个Cat类型。这就叫做父类引用指向子类对象。有什么用呢?别着急,下面多态的使用就会正式介绍到。这里我们要说一点别的内容 :
在多态中,我们将等号左边的这个引用变量的类型,称为编译类型。而将等号右边的——在堆空间中new出来的——真正的对象的类型,称为运行类型。其中,编译类型决定了引用变量可以调用哪些属性和行为;而运行类型则是在程序运行过程中jvm实际使用的类型。
比方说,现在我们通过animal对象来调用eat() 方法,因为编译类型是Animal类,因此在编译时,编译器要判断Animal类有没有eat() 方法。诶,有eat() 方法,那就可以调用。但在实际调用时,jvm会优先使用运行类型Cat类中的eat() 方法,因此打印结果为“🐱喜欢吃🥩”。
三、多态的使用 :
1.多态中成员方法的使用(重要) :
①使用规则 :
编译看左(即左边的编译类型有没有这个成员,这决定了我们能不能调用目标成员)
运行看右(即右边的运行类型中的该成员,才是运行中实际使用的成员)
②代码演示 :
up就以Animal类为父类,Cat类为子类,TestMethod类为测试类(置于同一包下)。我们给Animal类定义一些动物共同的属性,例如species_name(物种名),average_life(平均寿命),living_habit(生活习性)等;定义一些动物共同的行为,例如eat(吃),sleep(睡)等。
然后我们在Cat类中重写eat()方法和sleep()方法,并在TestMethod类中使用多态,根据多态中成员方法的使用规则 : 如果我们以Animal类的引用去调用eat()或者sleep()方法,实际运行中,优先使用的一定是子类Cat类中的eat()和sleep()方法。
Animal类代码如下 :
package knowledge.polymorphism.about_method;
public class Animal {
//成员变量
private String species_name; //物种名
private int average_life; //平均寿命
private String living_habit; //生活习性
//构造器
public Animal() {
}
public Animal(String species_name, int average_life, String living_habit) {
this.species_name = species_name;
this.average_life = average_life;
this.living_habit = living_habit;
}
//setter, getter方法
public String getSpecies_name() {
return species_name;
}
public void setSpecies_name(String species_name) {
this.species_name = species_name;
}
public int getAverage_life() {
return average_life;
}
public void setAverage_life(int average_life) {
this.average_life = average_life;
}
public String getLiving_habit() {
return living_habit;
}
public void setLiving_habit(String living_habit) {
this.living_habit = living_habit;
}
//成员方法
public void eat() {
System.out.println("这是Animal类的eat()方法,需要被重写捏😋");
}
public void sleep() {
System.out.println("这是Animal类的sleep()方法,需要被重写捏🤗");
}
}
/*
按下insert键后,输入会变成改写模式,需要再按下一次才能取消。
*/
Cat类代码如下 :
package knowledge.polymorphism.about_method;
public class Cat extends Animal {
//构造器
public Cat() {
}
public Cat(String species_name, int average_life, String living_habit) {
super(species_name, average_life, living_habit);
}
//重写父类的eat()方法和sleep()方法
@Override
public void eat() {
System.out.println("🐱🐱喜欢吃🥩捏");
}
@Override
public void sleep() {
System.out.println("🐱🐱一般蜷缩着睡觉,且不喜欢在光线强烈的地方睡");
}
//定义成员方法用于打印出猫的基本信息
public void cat_info() {
System.out.println("猫的物种学名:" + this.getSpecies_name() +
"\t猫的平均寿命 : " + this.getAverage_life() +
"\t猫的生活习性 : " + this.getLiving_habit());
}
}
TestMethod类代码如下 :
package knowledge.polymorphism.about_method;
public class TestMethod {
public static void main(String[] args) {
//父类引用指向子类对象
Animal animal = new Cat();
animal.setSpecies_name("Felinae"); //Felinae是猫的拉丁学名
animal.setAverage_life(15);
animal.setLiving_habit("喜欢独自生活,喜欢干净,喜欢肉类");
System.out.println("简单介绍一下猫这一物种 : ");
((Cat)animal).cat_info(); //强转类型,后面会讲到。
System.out.println("------------------------------------------");
//调用成员方法
animal.eat();
animal.sleep();
}
}
运行结果 :
果然不出我们所料,实际运行的eat() 方法和sleep() 方法是Cat类中重写过后的方法。
为了更进一步验证我们的结论,现在我们把Cat类中的两个重写方法都给注释掉,如下图所示 :
此时,若通过animal引用再次调用eat() 方法和 sleep() 方法,因为子类重写方法被注释,因此会调用父类的eat() 和 sleep() 方法,运行结果如下 :
通过演示,相信大家对于多态中成员方法的使用已有了一定的了解。但这时候可能有p小将(personable小将,指风度翩翩的人)出来说了 : 你就用了Cat一个子类,也好意思说自己是多态😅,我咋看不出来捏😅?
不愧是p小将,一针见血的😓!没错,前面我们说过——对象的多态才是Java多态的核心!同一对象在不同情况下表现出不同的状态或行为称为对象的多态。现在我们使用父类引用仅调用了Cat一个运行类型,没有体现多种状态!
好滴,于是我们在本包下新建一个Dog类,Dog类也继承Animal类。此时,继承关系如下图 :
🆗,我们先讲Cat类中重写的eat() 方法和sleep方法恢复, 如下图所示 :
然后在Dog类中对eat() 和sleep() 方法进行重写,并且也定义一个dog_info() 方法,用于打印出狗的基本信息。Animal类和Cat类代码保持不变。
Dog类代码如下 :
package knowledge.polymorphism.about_method;
public class Dog extends Animal {
//构造器
public Dog() {
}
public Dog(String species_name, int average_life, String living_habit) {
super(species_name, average_life, living_habit);
}
//重写父类eat() 方法 和 sleep() 方法
@Override
public void eat() {
System.out.println("🐕也喜欢吃🥩捏");
}
@Override
public void sleep() {
System.out.println("🐕想睡就睡,活在当下");
}
//定义成员方法用于打印出狗的基本信息
public void dog_info() {
System.out.println("狗的物种学名:" + this.getSpecies_name() +
"\t狗的平均寿命 : " + this.getAverage_life() +
"\t狗的生活习性 : " + this.getLiving_habit());
}
}
在TestMethod类中,当指向Cat对象的animal引用变量调用完成员方法后,我们改变animal引用的指向,使它指向一个Dog类对象,并去调用Dog类中重写的方法。
TestMethod类代码如下 :
package knowledge.polymorphism.about_method;
public class TestMethod {
public static void main(String[] args) {
//1.父类引用指向子类对象
Animal animal = new Cat();
animal.setSpecies_name("Felinae"); //Felinae是猫的拉丁学名
animal.setAverage_life(15);
animal.setLiving_habit("喜欢独自生活,喜欢干净,喜欢肉类");
System.out.println("简单介绍一下猫这一物种 : ");
((Cat)animal).cat_info(); //强转类型,后面会讲到。
System.out.println("--------------------------------");
//调用成员方法
animal.eat();
animal.sleep();
System.out.println("=======================================================");
//2.改变animal引用变量的指向,使其指向一个Dog类对象
animal = new Dog();
animal.setSpecies_name("Canis lupus familiaris");
animal.setAverage_life(12);
animal.setLiving_habit("喜欢啃骨头,喜欢嗅闻东西,喜欢摇尾巴");
System.out.println("简单介绍一下狗这一物种 : ");
((Dog)animal).dog_info(); //强制向下转型,后面会讲到。
System.out.println("--------------------------------");
//调用成员方法
animal.eat();
animal.sleep();
}
}
运行结果 :
通过运行结果我们可以看出 : animal引用变量指向Cat类对象时,运行的eat() 和 sleep() 方法就是Cat类中的方法,而当我们改变animal引用的指向,使其指向Dog类对象时,运行的方法就变成了Dog类中的方法。
什么概念?
同一引用变量,调用相同的方法,却因为指向的对象不同而产生了不同的结果。而我们平时习惯于将指向对象的引用变量当作对象的简称。那么,此处animal对象体现的不就是多态么。
③利用多态实现“旅行者请璃月人吃饭” :
有了多态后,前面的“旅行者请吃饭”问题便可以迎刃而解了 :
我们只需要在Traveler类(旅行者类)中定义一个my_treat() 方法,但与之前不一样的是,形参类型定义为Liyue_people类和Cooking类,即角色对象和料理对象各自的父类。如下所示 :
public void my_treat(Liyue_people liyue_people, Cooking cooking) {
System.out.println("海灯节,👴" + "请" + liyue_people.getName() + "吃" +
cooking.getFood_name());
}
注意,这时候, 因为Liyue_people类是Keqing类,Zhongli类和Ganyu类的父类,因此,不管你传入这三个对象中的哪一个,都会形成相当于" Liyue_people liyue_people = new 子类对象;"的形式,即父类引用指向子类对象——多态的形式。此时,传入的是哪个对象,就调用哪个对象的方法。Cooking类和它的子类也是同理。
璃月人类,刻晴类,钟离类,甘雨类以及料理类,金丝虾球类,豆腐类,清心类代码均不变。Traveler类代码如下 :
package knowledge.polymorphism.introduction;
public class Traveler {
//利用多态,一个方法解决请客吃饭问题
public void my_treat(Liyue_people liyue_people, Cooking cooking) {
System.out.println("海灯节,👴" + "请" + liyue_people.getName() + "吃" +
cooking.getFood_name());
}
}
Treat类代码也不变,不过注意,此时Treat类中使用的my_treat() 方法已变化。
运行结果 :
利用多态,将父类类型作为形参,一个方法照样实现了我们的需求😎!
2.多态中成员变量的使用 :
①使用规则 :
编译看左(即左边的编译类型有没有这个成员,这决定了我们能不能调用目标成员)
运行看左(多态关系中,成员变量是不涉及重写的)
②代码演示 :
1)up以Fruit类(水果类)为父类,子类Apple类(苹果类)和Grape类(葡萄类)分别继承Fruit类,以TestField类为测试类。
2)我们在父类Fruit类中定义一些水果公有的属性(不用private修饰),例如species_name(物种学名), sweetness(平均甜度), shape(形态特征)。并分别在Apple类和Grape类中定义它们自己的这三个同名属性。
3)最后在测试类中,分别建立Fruit类和Apple类,Grape类间的多态关系,并通过Fruit类型的引用去调用这三个属性并输出。根据多态关系中成员变量的使用规则,输出的三个属性应该每次都以Fruit类中的为基准。
4)为使代码简洁,up将两个子类都写在Fruit类的源文件中。Fruit类,Apple类,Grape类代码如下 :
package knowledge.polymorphism.about_field;
//父类 : Fruit类
public class Fruit {
//成员变量
String species_name = "物种学名噢";
double sweetness = 9.5; //水果的平均甜度大致为8~10左右。
String shape = "水果的外观形状捏";
//构造器
public Fruit() {
}
public Fruit(String species_name, double sweetness, String shape) {
this.species_name = species_name;
this.sweetness = sweetness;
this.shape = shape;
}
}
//子类 : 葡萄类
class Grape extends Fruit {
//成员变量
String species_name = "Vitis_vinifera_L"; //葡萄的拉丁学名
double sweetness = 16.5; //葡萄的平均甜度
String shape = "长得和葡萄差不多"; //葡萄的形态特征
//构造器
public Grape() {super();}
public Grape(String species_name, double sweetness, String shape) {
super(species_name, sweetness, shape);
}
}
//子类 : 苹果类
class Apple extends Fruit {
//成员变量
String species_name = "Malus_pumila_Mill"; //苹果的拉丁学名
double sweetness = 8.5; //苹果的平均甜度
String shape = "长得和苹果一样"; //苹果的形态特征
//构造器
public Apple() {super();}
public Apple(String species_name, double sweetness, String shape) {
super(species_name, sweetness, shape);
}
}
TestField类代码如下 :
package knowledge.polymorphism.about_field;
public class TestField {
public static void main(String[] args) {
//多态
Fruit fruit = new Apple();
System.out.println("苹果的物种学名:" + fruit.species_name);
System.out.println("苹果的平均甜度:" + fruit.sweetness);
System.out.println("苹果的形态特性:" + fruit.shape);
System.out.println("---------------------------------");
//改变fruit引用变量的指向,使其指向Grape类对象。
fruit = new Grape();
System.out.println("葡萄的物种学名:" + fruit.species_name);
System.out.println("葡萄的平均甜度:" + fruit.sweetness);
System.out.println("葡萄的形态特性:" + fruit.shape);
}
}
运行结果 :
果然不出我们所料,不管你fruit引用指向的是哪个子类水果对象,直接调用成员变量,永远优先使用Fruit类本身的属性。这时候p小将(personable小将,指风度翩翩的人)又要出来bb问了 : 🤬tnnd(太难弄哒),好不容易知道多态有个能解决继承缺陷的用处,怎么现在又用不了子类的属性了😅?那你写这么一堆干嘛,博主你搁这儿搞笑呢🤗?
p小将你先别急。以上演示只是要说明 : 多态关系中,直接使用成员变量的规则是编译看左,运行看左。即使子类定义了与父类同名的属性,但本质上那也是子类特有的属性了。我们之前一直在说,封装是继承的前提,继承是多态的前提。了解了封装和继承就知道,实际开发中通过对象直接调用属性的情况是不常见的,不符合封装的要求。我们编写的类应该尽量靠近JavaBean标准。那我们就不能在多态的前提下使用子类的属性了吗?当然不是😎,这不,多态的优缺点总结就来了。
四、多态的优点和缺点 :
1.益处 :
①可维护性 : 基于继承关系,只需要维护父类代码,提高了代码的复用性,降低了维护工作的工作量。
②可拓展性 : 把不同的子类对象都当作父类看待,屏蔽了不同子类对象间的差异,做出了通用的代码,派生类的功能可以被基类的方法或引用变量所调用,以适应不同的需求,实现了向后兼容。
2.弊端 : (重点)
父类引用不能直接使用子类的特有成员。
这也很好解释 : 前面在讲多态的实现步骤时我们说过——编译类型决定了引用变量可以调用哪些属性和行为;而运行类型则是在程序运行过程中jvm实际使用的类型。父类引用,说明编译类型是父类类型,以父类类型编译当然只能使用父类中存在的成员。当然,这里所说的成员包括成员变量和成员方法,这二者在多态关系中的使用略有不同:使用的成员变量必须是在父类中存在的,且成员变量不涉及重写;使用的成员方法也必须是在父类中存在的,但是如果子类重写了父类方法,优先调用子类重写的方法。
从jvm内存的角度解释就是 : .java文件经"javac.exe"命令编译后会生成.class的字节码文件,当代码中需要使用到某个类,该类的字节码文件就会加载进入方法区,而jvm识别并执行的就是字节码文件。因此,编译类型为父类类型,那jvm识别的当然是这个类的字节码文件,子类的特有成员,根本就不在这个字节码文件里面,jvm当然不认识。而对于子类重写的方法,父类字节码文件中包含有被重写方法的信息,jvm能够识别。而因为父类引用真正指向的是堆空间中的子类类型对象,所以此时会优先从堆空间中的子类对象里面找,使用子类重写后的方法,若子类没有重写,根据继承机制,则使用父类的该方法。
五、类型转换 :
Δ前言 :
当需要使用到子类特有功能,比如要使用子类重写后的方法,或者要使用子类的特有成员,这时候就需要进行类型转换。类型转换分为向上类型转换和向下类型转换两种。其中,向下转型是一个重点知识。
1.向上转型(自动):
①含义 :
即子类类型转换成父类类型(父类引用指向子类对象)。向上转型是自动进行的,我们的多态就是一种向上转型。eg : Animal animal = new Cat();
②语法格式 :
父类类型 父类引用变量 = new 子类类型();
③代码演示 :
这个说实话😅没啥好演示的。因为我们前面举过的所有多态的例子,都属于向上转型。 请继续看向下转型,那才是重点。
2.向下转型(强制):
①含义 :
即父类类型转换成子类类型。为什么叫强制类型转化呢? 因为向下转型不会自动发生,需要人为强转。并且,向下转型改变了编译类型,而编译类型决定了我们可以使用哪些成员,当编译类型由父类类型转换为子类类型后,我们当然可以使用子类的特有成员了。因此,我们说要使用子类的特有功能,靠的就是向下转型!
②语法格式 :
子类类型 子类引用变量 = (子类类型) 父类引用变量。
或者 直接使用 " ((子类类型)父类引用变量) " 的方式来调用子类特有成员,而不去做接收。
什么意思呢?给大家举个栗子 :
eg : Animal animal = new Cat();
Cat cat = new (Cat) animal; 后一条语句将Animal类型的引用变量animal转换成了子类Cat类类型的引用变量cat,相当于animal和cat两个引用指向了同一Cat对象,但堆空间中实际存在的Cat对象本身并没有变化。
③代码演示 :
演示Ⅰ:
我们就先来解决刚刚在演示多态中成员变量的使用时,Fruit类引用无法直接调用Apple类和Grape类成员变量的问题。
Fruit类,Apple类,Grape类代码均不变,大家可以往上翻翻看,就在上面“多态中成员变量的使用”这一部分。当然,懒得去翻也没关系,重在演示向下类型转换。我们只需要在TestField类输出语句中的成员变量前使用强制类型转换,即可成功输出子类特有成员。
TestField类代码如下 :
package knowledge.polymorphism.about_field;
public class TestField {
public static void main(String[] args) {
//多态
Fruit fruit = new Apple();
System.out.println("苹果的物种学名:" + ((Apple)fruit).species_name);
System.out.println("苹果的平均甜度:" + ((Apple)fruit).sweetness);
System.out.println("苹果的形态特性:" + ((Apple)fruit).shape);
System.out.println("---------------------------------");
//改变fruit引用变量的指向,使其指向Grape类对象。
fruit = new Grape();
System.out.println("葡萄的物种学名:" + ((Grape)fruit).species_name);
System.out.println("葡萄的平均甜度:" + ((Grape)fruit).sweetness);
System.out.println("葡萄的形态特性:" + ((Grape)fruit).shape);
}
}
运行结果 :
演示Ⅱ :
不知道大家还记不记得在“多态中的成员方法的使用”的演示中,我们在Cat类和Dog类中分别定义了cat_info()方法和dog_info()方法,用于打印出Cat对象和Dog对象的基本信息,其实up在当时已用了强制向下转型,大家不用再往上翻了😂,我给大家放个截图吧,如下 :
当时我们将Animal类型的引用变量animal分别向下转型为了Cat类引用和Dog类引用。以调用它们特有的方法,运行结果如下 :
演示Ⅲ :
不知道大家是否还记得😂,在开篇多态的引用中,我们举了旅行者请刻晴,钟离,甘雨吃饭的栗子, 当时up还分别在刻晴类,钟离类和甘雨类中定义了它们特有的成员方法,但是没有再测试类中调用!其实就是为了等现在演示呢😆。
Keqing类,Zhongli类,Ganyu类各自的特有成员方法如下,我还是直接放截图吧 :
Treat类(请客吃饭的测试类)代码如下 :
package knowledge.polymorphism.introduction;
public class Treat {
public static void main(String[] args) {
//在多态关系下,调用Keqing类,Zhongli类,Ganyu类的特有成员方法
//刻晴
Liyue_people lp1 = new Keqing("刻晴", 18, "女");
System.out.println("刻晴特有的行为是天街巡游:");
Keqing kq = (Keqing) lp1;
kq.sky_street_cruise();
System.out.println("------------------------------------------------");
//钟离
lp1 = new Zhongli("摩拉克斯", 6000, "男");
System.out.println("钟离特有的行为是天星:");
Zhongli zl = (Zhongli) lp1;
zl.sky_stars();
System.out.println("------------------------------------------------");
//甘雨
lp1 = new Ganyu("王小美", 3000, "女");
System.out.println("甘雨特有的行为是降众天华:");
Ganyu gy = (Ganyu) lp1;
gy.descend_to_heaven();
}
}
运行结果 :
3.注意事项 :
①只有在继承关系的继承上才可以进行类型转换,否则会报出ClassCastException(类型转换异常)。如下图所示 :
②在对引用变量进行向下转型之前,必须保证该引用变量指向的——堆空间中真正的对象的类型就是目标类型。(重要)
比如,Animal animal = new Cat(); 现在animal引用指向了一个Cat类型的对象,如果要对animal引用进行强制向下转型,就只能转换成Cat类型的引用;如果想转换成其他类型的引用,就需要先改变animal引用的指向,使其指向目标类型的对象。否则,同样会报出类型转换异常。
③那么,我们在进行向下转型之前,怎么就能知道——当前引用指向的对象是不是我们想要的目标类型的对象呢?
答案是 : 在进行强制类型转化之前,使用instanceof关键字来进行判断。
4.instanceof关键字(重要):
①概述 :
instanceof关键字,可以判断指定对象是否为指定的类型,并返回一个boolean类型的值,常与if条件语句一起使用。instanceof关键字在多态的应用——多态参数和多态数组中也会频繁使用,当然,多态的应用我们在后面会讲到的。
②用法 :
对象名 instanceof 数据类型
说明 :
1° 前面的“对象名”即引用变量。
2° 实际参与判断的是引用变量指向的——堆空间中真正的对象。
③代码演示 :
up以Person类为父类,让Teacher类和Student类分别继承Person类,最后在Test_instanceof类中进行测试。为了代码简洁,up将Teacher类和Student类写在了Person类的源文件中。
我们要在测试类干什么?
在测试类中,我们先建立Person——Teacher类之间的多态,然后利用instanceof关键字来判断Person引用指向的对象是否为Teacher类型,以及是否为Student类型,如果是Teacher类型,就利用强制向下转型去调用Teacher类中的特有方法;接着,改变Person类型引用的指向,使其指向Student类型的对象,利用instanceof关键字对当前引用指向的对象重新进行判断,以确定对象类型,若确定当前对象是Student类型,同理,利用强转去调用Student类特有的成员方法。
Person类,Teacher类,Student类代码如下 :
package knowledge.polymorphism.ceof_demo;
public class Person { //父类
/*
因为仅演示instanceof关键字,因此父类暂时不以JavaBean标准来敲,
当然,等讲到多态的应用时,我们还会用到instanceof关键字,到时候
代码肯定会向JavaBean标准靠拢。这里大伙儿就先将就看看⑧😋。
*/
}
class Teacher extends Person { //子类
//定义Teacher类的特有成员方法
public void teach() {
System.out.println("教书育人,重在德行,有德无德,大相径庭。");
}
}
class Student extends Person { //子类
//定义Student类的特有成员方法
public void what_time() {
System.out.println("嘿,哥们儿,几点了?还有几分钟下课?");
}
}
Test_instanceof类代码如下 :
package knowledge.polymorphism.ceof_demo;
public class Test_instanceof {
public static void main(String[] args) {
//1.多态先整上。
Person person = new Teacher();
//判断当前对象的类型
boolean boolean_1 = (person instanceof Teacher);
System.out.println("当前对象是Teacher类型吗: " + boolean_1);
boolean boolean_2 = (person instanceof Student);
System.out.println("当前对象是Student类型吗: " + boolean_2);
System.out.println("----------------------------------------------");
//确定对象类型后,进行强制向下转换,调用该类特有的成员方法
if (person instanceof Teacher) {
System.out.println("当前对象指向的对象是Teacher类型,可以将引用强转为Teacher类型");
((Teacher) person).teach();
} else if (person instanceof Student) {
System.out.println("当前对象指向的对象是Student类型,可以将引用强转为Student类型");
((Student) person).what_time();
}
System.out.println("\n===========================================================\n");
//2.改变person引用的指向,并重复上一轮的步骤。
//判断当前对象的类型
person = new Student();
boolean boolean_3 = (person instanceof Teacher);
System.out.println("当前对象是Teacher类型吗: " + boolean_3);
boolean boolean_4 = (person instanceof Student);
System.out.println("当前对象是Student类型吗: " + boolean_4);
System.out.println("----------------------------------------------");
//确定对象类型后,进行强制向下转换,调用该类特有的成员方法
if (person instanceof Teacher) {
System.out.println("当前对象指向的对象是Teacher类型,可以将引用强转为Teacher类型");
((Teacher) person).teach();
} else if (person instanceof Student) {
System.out.println("当前对象指向的对象是Student类型,可以将引用强转为Student类型");
((Student) person).what_time();
}
}
}
注意测试类中的代码,我们是先以一个boolean类型做接收,以证明使用instanceof关键字返回的是boolean类型,后又搭配 if 条件语句来调用子类特有的方法。其实学了static关键字之后,我们就可以把判断部分的代码单独写在main方法外的一个静态方法中,不用这么繁冗写两遍了。
运行结果 :
六、Java的动态绑定机制(重要):
限于博客字数已过20000,从动态绑定机制开始,up将把知识点的讲解另外放到一篇博文中,然后把博客的链接给大家放在这里,以增强大家的阅读体验。感谢理解,博文链接如下 :
https://blog.csdn.net/TYRA9/article/details/128880552?spm=1001.2014.3001.5501
七、多态的应用 :
多态的应用常见有多态数组和多态参数。博文链接如下 :
https://blog.csdn.net/TYRA9/article/details/128880552?spm=1001.2014.3001.5501
八、抽象类,抽象方法,abstract关键字详解 :
抽象类详解。博文链接如下 :
https://blog.csdn.net/TYRA9/article/details/129137305?spm=1001.2014.3001.5501
九、final关键字详解 :
final关键字详解。博文链接如下 :
https://blog.csdn.net/TYRA9/article/details/129097055?spm=1001.2014.3001.5501
十、static关键字(类变量和类方法)详解 :
static关键字详解。博文链接如下 :
https://blog.csdn.net/TYRA9/article/details/129039405?spm=1001.2014.3001.5501
十一、接口详解 :
https://blog.csdn.net/TYRA9/article/details/129174384?spm=1001.2014.3001.5501
十二、Debug详解 :
https://blog.csdn.net/TYRA9/article/details/128884528?spm=1001.2014.3001.5501
十三、多态篇总结 :
🆗,以上就是面向对象篇章——多态篇的全部内容了。讲真的,多态篇的内容真是巨多😂,要是up不放链接的话,可能这一篇的字数就过10万字了。回顾一下,我们从一个有趣的例子引入了多态的必要性;接着介绍了Java中非常非常重要的动态绑定机制。接着又详细介绍了多态的使用;然后是抽象类,final关键字,static关键字,和接口这四大块对多态的补充和应用。最后,up又给大家补充了一篇介绍Java Dubug的博文。其实Debug中我也是主要演示了一些最基本的逻辑语句和动绑机制,按道理讲Debug更应该放到继承篇。但是我觉得讲面向对象讲得好好的,突然插入这么个玩意儿,多少有些抽象,于是最后还是把Debug放到了多态篇。好滴,之后up会出一篇总结性质的博文,把Java面向对象三大特性——封装、继承,多态三大节合并为一章,并且做一些对面向对象基础的引入和内容的完善。我们不见不散😎!感谢阅读!
- 点赞
- 收藏
- 关注作者
评论(0)