深入理解Java面向对象编程的三大特性之一:多态 - 详尽解析
目录
前言 :
一、为什么需要多态 :
1.白璧微瑕 :
2.举栗(请甘雨,刻晴,钟离吃饭):
3.代码 :
4.问题 :
二、什么是多态 :
1.定义 :
2.多态的实现步骤(重要) :
三、多态的使用 :
1.多态中成员方法的使用(重要) :
①使用规则 :
②代码演示 :
③利用多态实现“旅行者请璃月人吃饭” :
2.多态中成员变量的使用 :
①使用规则 :
②代码演示 :
四、多态的优点和缺点 :
1.益处 :
2.弊端 : (重点)
五、类型转换 :
Δ前言 :
1.向上转型(自动):
①含义 :
②语法格式 :
③代码演示 :
2.向下转型(强制):
①含义 :
②语法格式 :
③代码演示 :
3.注意事项 :
4.instanceof关键字(重要):
①概述 :
②用法 :
③代码演示 :
六、Java的动态绑定机制(重要):
七、多态的应用 :
八、抽象类,抽象方法,abstract关键字详解 :
九、final关键字详解 :
十、static关键字(类变量和类方法)详解 :
十一、接口详解 :
十二、Debug详解 :
十三、面向对象总结 :
前言 :
Hi,guys.这篇博文是Java面向对象三大特性篇的第三篇———多态篇。之前我们就一直强调,面向对象三大特性中,封装是继承的前提,继承是多态的前提。多态也是实际开发中运用最多,最广泛的一个特性!因此,多态的重要性不言而喻。本篇博文,内容包括但不限于多态的介绍,多态的使用详解,多态中的类型转化机制,动态绑定机制详解,抽象类详解,final关键字、static关键字详解,接口详解等等,感谢阅读!
这篇博文算是三大特性篇的最后一篇????,之后up会将三大特性对应的三篇博客合并起来,并在此基础之上写一篇总结Java面向对象的十万+字博文,力争以最通俗易懂的方式让初学者快速入门并理解面向对象的灵魂。
还有是****貌似不欢迎长文章????,因此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(类型转换异常)。如下图所示 :
②
上一篇: 数据和大数据的本质是什么?解析一下
下一篇: 深入理解Java面向对象编程中的多态特性
推荐阅读
-
深入理解Java面向对象编程中的多态特性
-
深入理解Java面向对象编程的三大特性之一:多态 - 详尽解析
-
深入理解Java面向对象特性之二:继承性的具体实现与格式详细解析
-
深入理解Java面向对象编程(中级):多态的概念与特性详解
-
深入解析JavaScript面向对象的三大特性之一:多态性
-
F#探险之旅(二):函数式编程(上)-函数式编程范式简介 F#主要支持三种编程范式:函数式编程(Functional Programming,FP)、命令式编程(Imperative Programming)和面向对象(Object-Oriented,OO)的编程。回顾它们的历史,FP是最早的一种范式,第一种FP语言是IPL,产生于1955年,大约在Fortran一年之前。第二种FP语言是Lisp,产生于1958,早于Cobol一年。Fortan和Cobol都是命令式编程语言,它们在科学和商业领域的迅速成功使得命令式编程在30多年的时间里独领风骚。而产生于1970年代的面向对象编程则不断成熟,至今已是最流行的编程范式。有道是“*代有语言出,各领风骚数十年”。 尽管强大的FP语言(SML,Ocaml,Haskell及Clean等)和类FP语言(APL和Lisp是现实世界中最成功的两个)在1950年代就不断发展,FP仍停留在学院派的“象牙塔”里;而命令式编程和面向对象编程则分别凭着在商业领域和企业级应用的需要占据领先。今天,FP的潜力终被认识——它是用来解决更复杂的问题的(当然更简单的问题也不在话下)。 纯粹的FP将程序看作是接受参数并返回值的函数的集合,它不允许有副作用(side effect,即改变了状态),使用递归而不是循环进行迭代。FP中的函数很像数学中的函数,它们都不改变程序的状态。举个简单的例子,一旦将一个值赋给一个标识符,它就不会改变了,函数不改变参数的值,返回值是全新的值。 FP的数学基础使得它很是优雅,FP的程序看起来往往简洁、漂亮。但它无状态和递归的天性使得它在处理很多通用的编程任务时没有其它的编程范式来得方便。但对F#来说这不是问题,它的优势之一就是融合了多种编程范式,允许开发人员按照需要采用最好的范式。 关于FP的更多内容建议阅读一下这篇文章:Why Functional Programming Matters(中文版)。F#中的函数式编程 从现在开始,我将对F#中FP相关的主要语言结构逐一进行介绍。标识符(Identifier) 在F#中,我们通过标识符给值(value)取名字,这样就可以在后面的程序中引用它。通过关键字let定义标识符,如: let x = 42 这看起来像命令式编程语言中的赋值语句,两者有着关键的不同。在纯粹的FP中,一旦值赋给了标识符就不能改变了,这也是把它称为标识符而非变量(variable)的原因。另外,在某些条件下,我们可以重定义标识符;在F#的命令式编程范式下,在某些条件下标识符的值是可以修改的。 标识符也可用于引用函数,在F#中函数本质上也是值。也就是说,F#中没有真正的函数名和参数名的概念,它们都是标识符。定义函数的方式与定义值是类似的,只是会有额外的标识符表示参数: let add x y = x + y 这里共有三个标识符,add表示函数名,x和y表示它的参数。关键字和保留字关键字是指语言中一些标记,它们被编译器保留作特殊之用。在F#中,不能用作标识符或类型的名称(后面会讨论“定义类型”)。它们是: abstract and as asr assert begin class default delegate do donedowncast downto elif else end exception extern false finally forfun function if in inherit inline interface internal land lazy letlor lsr lxor match member mod module mutable namespace new nullof open or override private public rec return sig static structthen to true try type upcast use val void when while with yield 保留字是指当前还不是关键字,但被F#保留做将来之用。可以用它们来定义标识符或类型名称,但编译器会报告一个警告。如果你在意程序与未来版本编译器的兼容性,最好不要使用。它们是: atomic break checked component const constraint constructor continue eager event external fixed functor global include method mixinobject parallel process protected pure sealed trait virtual volatile 文字值(Literals) 文字值表示常数值,在构建计算代码块时很有用,F#提供了丰富的文字值集。与C#类似,这些文字值包括了常见的字符串、字符、布尔值、整型数、浮点数等,在此不再赘述,详细信息请查看F#手册。 与C#一样,F#中的字符串常量表示也有两种方式。一是常规字符串(regular string),其中可包含转义字符;二是逐字字符串(verbatim string),其中的(")被看作是常规的字符,而两个双引号作为双引号的转义表示。下面这个简单的例子演示了常见的文字常量表示: let message = "Hello World"r"n!" // 常规字符串let dir = @"C:"FS"FP" // 逐字字符串let bytes = "bytes"B // byte 数组let xA = 0xFFy // sbyte, 16进制表示let xB = 0o777un // unsigned native-sized integer,8进制表示let print x = printfn "%A" xlet main = print message; print dir; print bytes; print xA; print xB; main Printf函数通过F#的反射机制和.NET的ToString方法来解析“%A”模式,适用于任何类型的值,也可以通过F#中的print_any和print_to_string函数来完成类似的功能。值和函数(Values and Functions) 在F#中函数也是值,F#处理它们的语法也是类似的。 let n = 10let add a b = a + blet addFour = add 4let result = addFour n printfn "result = %i" result 可以看到定义值n和函数add的语法很类似,只不过add还有两个参数。对于add来说a + b的值自动作为其返回值,也就是说在F#中我们不需要显式地为函数定义返回值。对于函数addFour来说,它定义在add的基础上,它只向add传递了一个参数,这样对于不同的参数addFour将返回不同的值。考虑数学中的函数概念,F(x, y) = x + y,G(y) = F(4, y),实际上G(y) = 4 + y,G也是一个函数,它接收一个参数,这个地方是不是很类似?这种只向函数传递部分参数的特性称为函数的柯里化(curried function)。 当然对某些函数来说,传递部分参数是无意义的,此时需要强制提供所有参数,可是将参数括起来,将它们转换为元组(tuple)。下面的例子将不能编译通过: let sub(a, b) = a - blet subFour = sub 4 必须为sub提供两个参数,如sub(4, 5),这样就很像C#中的方法调用了。 对于这两种方式来说,前者具有更高的灵活性,一般可优先考虑。 如果函数的计算过程中需要定义一些中间值,我们应当将这些行进行缩进: let halfWay a b = let dif = b - a let mid = dif / 2 mid + a 需要注意的是,缩进时要用空格而不是Tab,如果你不想每次都按几次空格键,可以在VS中设置,将Tab字符自动转换为空格;虽然缩进的字符数没有限制,但一般建议用4个空格。而且此时一定要用在文件开头添加#light指令。作用域(Scope)作用域是编程语言中的一个重要的概念,它表示在何处可以访问(使用)一个标识符或类型。所有标识符,不管是函数还是值,其作用域都从其声明处开始,结束自其所处的代码块。对于一个处于最顶层的标识符而言,一旦为其赋值,它的值就不能修改或重定义了。标识符在定义之后才能使用,这意味着在定义过程中不能使用自身的值。 let defineMessage = let message = "Help me" print_endline message // error 对于在函数内部定义的标识符,一般而言,它们的作用域会到函数的结束处。 但可使用let关键字重定义它们,有时这会很有用,对于某些函数来说,计算过程涉及多个中间值,因为值是不可修改的,所以我们就需要定义多个标识符,这就要求我们去维护这些标识符的名称,其实是没必要的,这时可以使用重定义标识符。但这并不同于可以修改标识符的值。你甚至可以修改标识符的类型,但F#仍能确保类型安全。所谓类型安全,其基本意义是F#会避免对值的错误操作,比如我们不能像对待字符串那样对待整数。这个跟C#也是类似的。 let changeType = let x = 1 let x = "change me" let x = x + 1 print_string x 在本例的函数中,第一行和第二行都没问题,第三行就有问题了,在重定义x的时候,赋给它的值是x + 1,而x是字符串,与1相加在F#中是非法的。 另外,如果在嵌套函数中重定义标识符就更有趣了。 let printMessages = let message = "fun value" printfn "%s" message; let innerFun = let message = "inner fun value" printfn "%s" message innerFun printfn "%s" message printMessages 打印结果: fun value inner fun valuefun value 最后一次不是inner fun value,因为在innerFun仅仅将值重新绑定而不是赋值,其有效范围仅仅在innerFun内部。递归(Recursion)递归是编程中的一个极为重要的概念,它表示函数通过自身进行定义,亦即在定义处调用自身。在FP中常用于表达命令式编程的循环。很多人认为使用递归表示的算法要比循环更易理解。 使用rec关键字进行递归函数的定义。看下面的计算阶乘的函数: let rec factorial x = match x with | x when x < 0 -> failwith "value must be greater than or equal to 0" | 0 -> 1 | x -> x * factorial(x - 1) 这里使用了模式匹配(F#的一个很棒的特性),其C#版本为: public static long Factorial(int n) { if (n < 0) { throw new ArgumentOutOfRangeException("value must be greater than or equal to 0"); } if (n == 0) { return 1; } return n * Factorial (n - 1); } 递归在解决阶乘、Fibonacci数列这样的问题时尤为适合。但使用的时候要当心,可能会写出不能终止的递归。匿名函数(Anonymous Function) 定义函数的时候F#提供了第二种方式:使用关键字fun。有时我们没必要给函数起名,这种函数就是所谓的匿名函数,有时称为lambda函数,这也是C#3.0的一个新特性。比如有的函数仅仅作为一个参数传给另一个函数,通常就不需要起名。在后面的“列表”一节中你会看到这样的例子。除了fun,我们还可以使用function关键字定义匿名函数,它们的区别在于后者可以使用模式匹配(本文后面将做介绍)特性。看下面的例子: let x = (fun x y -> x + y) 1 2let x1 = (function x -> function y -> x + y) 1 2let x2 = (function (x, y) -> x + y) (1, 2) 我们可优先考虑fun,因为它更为紧凑,在F#类库中你能看到很多这样的例子。 注意:本文中的代码均在F# 1.9.4.17版本下编写,在F# CTP 1.9.6.0版本下可能不能通过编译。 F#系列随笔索引页面