目录
基本介绍
Java中的多态就是指我们使用父类类型的引用来引用子类对象。这意味着一个父类引用可以指向多个不同的子类对象,并在运行时根据对象的实际类型来调用相应的方法。
简单来说,多态就是一个变量(或接口)可以有多种形态,具体的形态在运行时才确定。
比如,我们创建一个"主人"类,它有一个“喂动物”的行为。然后我们有两个父类,一个是“动物”,另一个是“食物”。父类"动物"下有包含有"狗"、"猫"等等等子类,父类"食物"下有包含有"骨头"、"鱼"等等等子类,"主人"执行"喂动物"的行为时,会喂"狗"“骨头”,喂"猫""鱼"。当我们用一个动物类型的引用来指向狗或猫的对象,并调用“喂动物”这个行为时,就会根据实际指向的对象类型(狗或猫)来选择不同的"食物"。这就是多态的体现。
这或许有些难以理解,我们先介绍一个例子引出多态,以便我们理解。
引出多态
我们用这一篇代码来引出多态,代码要求:创建一个"master"类,其中包含一个“feed“方法,可以显示主人给不同的动物喂食的信息。
例:
——————————————————————主人类——————————————————————
public class master {
String name; //主人的名字
public String getName() { //get方法
return name;}
public void setName(String name) { //set方法
this.name = name;}
public master(String name) { //构造函数
this.name = name;}
public void feed(dog mydog,bone bigbone){//给狗喂食的方法
System.out.println("主人"+name+"给"+mydog.getName()+"吃"+bigbone.getName());
}
public void feed(cat mycat,fish littlefish){//给猫喂食的方法
System.out.println("主人"+name+"给"+mycat.getName()+"吃"+littlefish.getName());
}
}
——————————————————————动物类(主类)——————————————————————
public class Animal {
private String name;
public Animal(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
——————————————————————食物类(主类)——————————————————————
public class food {
private String name;
public food(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
——————————————————————狗类(子类)——————————————————————
public class dog extends Animal{
public dog(String name) {
super(name);
}
}
——————————————————————猫类(子类)——————————————————————
public class cat extends Animal{
public cat(String name) {
super(name);
}
}
——————————————————————骨头类(子类)——————————————————————
public class bone extends food{
public bone(String name) {
super(name);
}
}
——————————————————————鱼类(子类)——————————————————————
public class fish extends food {
public fish(String name) {
super(name);
}
}
————————————————————————主方法————————————————————————
public class let_sgo {
public static void main(String[] args) {
master kazuha = new master("枫原万叶");//创建master对象
dog myD = new dog("大黄"); //创建dog对象
bone b = new bone("大骨棒"); //创建bone对象
kazuha.feed(myD,b);
System.out.println("—————————————————————");
cat myC = new cat("小黑"); //创建cat对象
fish f = new fish("红鲤鱼"); //创建fish对象
kazuha.feed(myC,f);
}
}
执行结果:
主人枫原万叶给大黄吃大骨棒
—————————————
主人枫原万叶给小黑吃红鲤鱼
此时我们发现虽然能够显示主人给不同的动物喂食的信息,但代码非常臃肿,长篇累牍,并且每新增一个对象“宠物”类都要重新写一段配套的feed代码,非常的不方便,可否简化一下呢?这时就能够用到多态。
重载体现多态
其实在我们之前学习的内容重载中,多态就有所体现,我们再来回顾一下重载。
例:
public class go {
public static void main(String[] args) {
A a = new A();
System.out.println(a.sum(10,20));
System.out.println(a.sum(10,20,30));
}
}
class B{
}
class A extends B{
public int sum(int n1,int n2){
return n1+n2;
}
public int sum(int n1,int n2,int n3){
return n1+n2+n3;
}
}
执行结果:
30
60
此时两个方法同样为sum方法,但因传入参数个数不同,就会去调用不同的方法,因此对于sum来说,就是多种状态的体现。
不仅在重载中多态就有所体现,重写中也有所体现,我们接下来再用重写举一个简单的例子以便认识多态。
重写体现多态
例:
public class go {
public static void main(String[] args) {
A a = new A();
B b = new B();
a.say();
b.say();
}
}
class B{
public void say(){
System.out.println("B say()方法被调用......");
}
}
class A extends B{
public void say(){
System.out.println("A say()方法被调用......");
}
}
此时两个名称同样的say方法,因为类的不同,其调用的say方法也不同,这也是多态的体现。
对象的多态
一、编译类型和运行类型可以不一致
我们来看创建对象"狗"的这一行代码
Animal D=new dog();//Animal为父类,dog为子类
此时我们创建一个新的子类dog对象,并将其赋给父类Animal的对象D来引用,也就是说在此处我们用父类的一个引用指向子类的一个对象,此处D并不是一个真正的对象,而是对新创建的dog类的一个引用,其本体还是dog类。因此D的编译类型为Animal,而其运行类型为dog。
这就是两者不一致的案例。
当然,两者可以不一致,也可以一致,这都是正确的。我们日常创建新对象大多都是使用两者一致的这种方法。
例:对象的编译类型和运行类型一致
Animal myDog = new Animal();//Animal为父类,dog为子类
Dog D=new dog();
但也有限制,其运行类型必须是编译类型或者其子类,也就是说当编译类型为Animal时,其运行类型只能为Animal或其子类。
二、编译类型不能改变
我们再来看创建对象"狗"的这一行代码
Animal D=new dog();//Animal为父类,dog为子类
其编译类型为Animal,这是在创建这一对象时就已确定的,是不能改变的。
三、运行类型是可以变化的
我们对创建对象"狗"的这一行代码做一些补充
Animal D=new dog();//Animal为父类,dog和cat为子类
D=new Cat();
Animal类对象D在创建时的编辑对象为Animal,运行类型为Dog类;但在后面我们可以修改其运行对象为Cat类,这是合法的。
四、编译类型看定义时=的左边,运行类型看=的右边
我们继续来看创建对象"狗"的这一行代码
Animal D=new dog();//Animal为父类,dog和cat为子类
此时" = "的左边为Animal,其编译类型就为Animal," = "的右边为Animal,其运行类型就为dog。
具体示例
在介绍完对象的多态理论部分后,我们就可以在实际举例中来熟悉对象的多态在例子中的具体体现。
一、编译类型和运行类型不一样时系统的执行顺序
例:
class Animal { //定义父类
public void sound() { //定义方法sound
System.out.println("The animal makes a sound");
}
}
class Dog extends Animal { //定义子类
public void sound() { //重写方法sound
System.out.println("The Dog makes a sound");
}
}
public class go { //主方法
public static void main(String[] args) {
Animal D=new Dog(); //编辑类型为Animal,运行类型为Dog
D.sound(); //执行方法sound
}
}
此处所指执行的方法D.sound其编辑类型为Animal,运行类型为Dog,且都含有sound方法,那么他会执行哪一个sound方法呢?
答案为:执行运行类型中的sound方法
执行结果:The Dog makes a sound
此时如果我们更改D的运行类型,系统又会执行哪一个sound方法呢?
二、更改运行类型时系统的执行顺序
例:
class Animal { //定义父类
public void sound() { //定义方法sound
System.out.println("The animal makes a sound");
}
}
class Dog extends Animal { //定义子类
public void sound() { //重写方法sound
System.out.println("The Dog makes a sound");
}
}
class Cat extends Animal { //定义子类
public void sound() { //重写方法sound
System.out.println("The Cat makes a sound");
}
}
public class go { //主方法
public static void main(String[] args) {
Animal D=new Dog(); //编辑类型为Animal,运行类型为Dog
D=new Cat(); //更改运行类型为Cat
D.sound(); //执行方法sound
}
}
执行结果:The Cat makes a sound
所以当运行类型发生更改时,系统会执行新的运行类型中的方法。
多态的实际运用
在认识完多态后,我们再回首来看篇首的那个例子,我们可以对其进行一定的简化。
例:对feed方法进行简化
————————————————————————原文————————————————————————
public void feed(dog mydog, bone bigbone){
System.out.println("主人"+name+"给"+mydog.getName()+"吃"+bigbone.getName());
}
public void feed(cat mycat, fish littlefish){
System.out.println("主人"+name+"给"+mycat.getName()+"吃"+littlefish.getName());
}
—————————————————————使用多态简化—————————————————————
public void feed(Animal mypet,food petfood){//使用父类对象Animal来接受其子类
System.out.println("主人"+name+"给"+mypet.getName()+"吃"+petfood.getName());
}
输出结果:
主人枫原万叶给大黄吃大骨棒
—————————————
主人枫原万叶给小黑吃红鲤鱼
我们使用更简洁的代码完成了原本所需的内容,系统也成功输出原本的内容
原本feed方法为feed(dog mydog, bone bigbone)和feed(cat mycat, fish littlefish),因为dog和cat为Animal的子类,bone和fish是food的子类,所以此处我们直接使用父类Animal和food类来引用子类对象。
也就是说mypet的编译类型为Animal,可以指向(接收)其子类的对象,petfood同理。
注意事项与细节
一、前提:两个类存在继承关系
多态是建立在封装和继承的基础上的,失去了封装和继承,多态也就失去了其存在的意义,正因如此,多态要求两个类直接必须存在着继承的关系。
二、多态的向上转型
1、概念:
在Java中,如果一个类B继承自类A,那么类B就是类A的子类。在程序运行时,我们可以将一个B类型的对象引用赋值给一个A类型的变量,这种操作被称为向上转型(Upcasting)。
本质:父类的引用指向了子类的对象
因为子类的上一级为父类,而多态正是用父类的引用指向子类,我们称之为向上转型。
2、语法:
父类类型 引用名 =new 子类类型()
我们在使用多态时的写法
Animal D=new dog(); //Animal为父类,dog为子类
但此处不要忘记一点:Object类为所有类的根类,所以无论子类是什么,我们都可以用Object类指向子类。
class Animal { //定义基类
}
class pet extends Animal {//定义父类
}
class dog extends pet{ //定义子类
}
public class go {
public static void main(String[] args) {
pet C=new dog(); //使父类指向其子类
Animal D=new dog(); //使基类指向其子类
Object E=new dog();//使根类指向其子类
}
}
这里三种定义新对象的方法都是合法的。
3、特点:
编译类型看 = 左边,运行类型看 = 右边
这个特点在此不再赘述
4、调用规则:
可以调用父类中的所有成员(需遵守访问权限),不能调用子类中特有成员,最终的运行效果仍要看子类的具体实现;
我们来看下面这串代码:
class Animal { //基类
public void sound() {}
public void see(){}
public void eat(){}
public void smell(){}
}
class pet extends Animal { //父类
public void Asound(){}
public void Asee(){}
public void Aeat(){}
public void Asmell(){}
}
class dog extends pet{ //子类
public void Bsound(){}
public void Bsee(){}
public void Beat(){}
public void Bsmell(){}
}
public class go { //主方法
public static void main(String[] args) {
dog B=new dog();
pet C=new dog();
Animal D=new dog();
Object E=new dog();
}
}
其中四个类为依次继承的关系,每个类中均有单独的四个方法,总计十二个。我们在主方法中又创建了四个对象,其运行类型均为dog类,但其四个的编译类型均不相同,那这四个对象分别能调用12个方法中的多少个呢?
我们再来看这句话“可以调用父类中的所有成员,不能调用子类中特有成员”。
这里面对象B因为其编译类型为dog,所有类均为他的父类,所以这12个方法他均可以调用,对象C和对象D也依次可以调用自身类和其父类所包含的8个和4个方法,这就是"可以调用父类中的所有成员"。
而对象E的编译类型为Object类,为所有类的基类,这12个函数为其子类中所特有的函数,他均不可调用,这就是“不能调用子类中特有成员”。因为在其编译阶段,能调用哪些成员是由编译类型来决定的。
能否调用 | dog B | pet C | Animal D | Object E |
Animal中的方法 | √ | √ | √ | × |
pet类中的方法 | √ | √ | × | × |
dog类中的方法 | √ | × | × | × |
当然我们也需遵守访问权限,如果想要调用的方法无访问权限,我们也无法调用。
例:可访问范围
class Animal { //父类
public void sound() { //访问权限为public
}
private void see(){ //访问权限为private
}
}
class dog extends Animal{ //子类
public void Asound() {
}
public void Asee(){
}
}
public class go {
public static void main(String[] args) {
Animal A=new dog();//编译类型为父类Animal,可调用其本身中的方法
A.sound(); //正确
A.Asound(); //错误,不可调用其子类中特有方法
dog B=new dog(); //编译类型为子类dog,可调用其父类Animal中的方法
B.sound(); //正确
B.see(); //错误,see()具有 private 访问权限
}
}
我们再来看最后一句:“最终的运行效果仍要看子类的具体实现”。同样用一段代码来解释 例:方法的具体实现
class Animal {
public void sound() {
System.out.println("调用父类");
}
}
class dog extends Animal {
public void sound() {
System.out.println("调用子类");
}
}
public class go {
public static void main(String[] args) {
Animal A = new dog();
A.sound();
dog B=new dog();
B.sound();
}
}
这篇代码中,子类和父类均有方法sound(),我们定义的两个对象其编译类型分别为Animal和dog,那么两者调用方法sound时,分别会调用哪个类中的方法呢?
运行结果:
调用子类
调用子类
这是因为系统在编译时是按照编译类型来处理的,但编译后的实际运行中,系统会按照运行类型来处理,所以其实际调用的为运行类型中dog的方法,如果dog中不存在该方法,系统则会在其父类中寻找该方法。当然,这种情况仅限于编译类型至运行类型中存在重名方法时使用,也就是说当子类中的某一方法或变量非特有时才会调用子类中的方法,否则报错。
总结下来就是:当调用重名方法或变量时,会从运行类型开始向上(向父类)查找该成员进行执行,当调用特有的成员时,直接从编译类型开始向上(向父类)查找该成员进行执行。
三、多态的向下转型
1、概念
在Java中,如果一个类B继承自类A,那么类B就是类A的子类。当我们需要访问B类特有的方法或属性时,我们就需要将A类型的引用转换回B类型,这就是向下转型。
前面介绍的为向上转型,而这部分我们介绍的向下转型与其刚好相反,一些向上转型中无法完成的操作,利用向下转型却刚好能完成。
2、语法
子类类型 引用名=(子类类型)父类引用
我们以一篇代码为例:
class Animal {
}
class dog extends Animal {
public void sound() {
System.out.println("调用子类中的方法");
}
}
public class go {
public static void main(String[] args) {
Animal A = new dog();
A.sound(); //报错,无法解析 'Animal' 中的方法 'sound'
}
}
在这个例子中,我们创建了一个对象A,其编译类型为父类Animal,但却需要调用子类中特有方法,这在之前是不被允许的,但在向下转型中,却可以实现。
使用 (Dog) 显式地将Animal类型的引用 转换为 Dog 类型的引用 B
首先将对象A由对父类的引用转换为对子类的引用,并将其赋给新的引用B
dog B = (dog) A;
此时再用对象B来调用子类中dog的方法 B.sound
此时A的编译类型为Animal,运行类型为dog(上文中定义A时“ = ”左边为Animal,右边为dog),而B的编译类型和引用类型均为dog(定义B时“ = ”左右均为dog),且A和B都指向内存中的同一个dog对象;
但这里执行向下转型有一个要求
3、要求
父类的引用必须指向的是当前目标类型的对象或其子类
也就是说,运行代码dog B = (dog) A;的前提是A所引用的对象实际上是一个dog类型的对象,或者是其子类的对象。我们再来用一段代码以便加深理解:
class Animal { //定义类Animal
}
class pet extends Animal { //定义父类pet
}
class dog extends pet { //定义子类dog
}
public class go {
public static void main(String[] args) {
//_________例1_________
Animal A1 = new dog(); //A1 编译类型为Animal,运行类型为子类dog
dog B1 = (dog) A1; //转换编译类型为子类dog,运行类型为子类dog
//_________例2_________
Animal A2=new pet(); //A2 编译类型为Animal,运行类型为父类pet
dog B2=(dog) A2; //转换编译类型为子类dog,运行类型为子类dog
//_________例3_________
Animal A3=new dog(); //A1 编译类型为Animal,运行类型为子类dog
Animal B3=(pet) A3; //转换编译类型为Animal,运行类型为父类pet
//_________例4_________
dog A4=new dog(); //A1 编译类型为Animal,运行类型为子类dog
pet B4=(dog) A4; //转换编译类型为父类pet,运行类型为子类dog
}
}
试判断这四个例子中哪些能够执行,哪些不能执行?
答案为只有例2不能执行,因为其原本的运行类型为父类pet,向下转型时却尝试将其运行类型改为子类,这是错误的,只能将运行类型改为其父类。
总而言之,在向下转型的过程中,编译类型可以更改为其所在的成员方法中的任一方法(在与对象实际运行类型兼容的情况下),而运行类型只能保持不变或者更改为其父类中的某一方法。
值得一提的是要求中有一句”当前目标类型“,我们借助一个例子来理解这句话
class Animal { //定义Animal类
}
class big extends Animal { //定义第一个父类big
}
class dog extends big { //定义第一个子类dog
}
class small extends Animal{ //定义第二个父类small
}
class cat extends small { //定义第二个子类cat
}
public class go {
public static void main(String[] args) {
//________例1________
Animal A=new cat(); //A编译类型为Animal,运行类型为cat
small A1=(cat)A; //转换编译类型为第二个父类samll,运行类型为第二个子类cat
//________例2________
Animal B=new cat(); //B编译类型为Animal,运行类型为cat
big B1=(cat)A; //转换编译类型为第一个父类big,运行类型为第二个子类cat
//________例3________
Animal C=new cat(); //C编译类型为Animal,运行类型为cat
Animal C1=(dog)A; //转换编译类型为Animal,运行类型为第一个子类dog
}
}
这个例子中各个类的关系可能不够清楚,我们用图来表示其关系:
这样各个类的关系就清晰了,此时再看代码,判断一下三个例子中哪些能够运行呢?
答案为只有例1能够运行,我们接下来分析例2和例3错在何处。
例2尝试将其其编译类型从Animal转换为big,但因为其原本的运行类型为cat,而cat的父类为small和Animal,与big类是两个不相关的子类,不符合”当前目标类型“这句话,所以不能运行。
例3尝试将其运行类型从cat类变为dog类,但两者也为两个独立的子类,也不符合”当前目标类型“这句话,所以不能运行。
4、只能强制父转的引用,不能强转父类的对象
只能强制父转的引用:
这里指的是一个引用变量,它原本指向的是一个父类(或接口)的对象,但实际上这个对象可能是父类的一个具体子类实例。在这种情况下,我们可以将这个引用强制转换为子类的引用,前提是这个引用确实指向了一个子类的实例。
例:
class Animal { //定义父类
}
class Dog extends Animal { //定义子类
}
public class go { //主方法
public static void main(String[] args) {
Animal A=new Dog(); //编辑类型为Animal,运行类型为Dog
Dog D=(Dog)A; //强转
}
}
在这个例子中,A引用实际上指向了一个 Dog对象,所以我们可以安全地将它强制转换为 Dog类型的引用。
不能强转父类的对象:
这句话意味着你不能改变一个对象实际的类型。即使你有一个父类类型的引用,并尝试将其强制转换为子类类型,你仍然得到的是同一个对象,只是现在是通过子类的引用来引用它。对象的实际类型在创建时就已经确定了,并且在运行期间是不可变的。
如果我们将上文的例子进行修改,变为:
class Animal { //定义父类
}
class Dog extends Animal { //定义子类
}
public class go { //主方法
public static void main(String[] args) {
Animal A=new Animal(); //编辑类型为Animal,运行类型为Dog
Dog D=(Dog)A; //强转
}
}
在这个例子中,尝试将 Animal 类型的对象强制转换为 Dog 会发生报错,因为 A 实际上指向的是一个 Animal对象,而不是 Dog对象。俗称:父类太大了,子类装不下。
同时,无论你怎样转换对象的编译类型或者运行类型,这个对象还是引用的自创建时就确定的对象地址,没有发生任何变化,对象的实际类型和储存地址在创建时就已经确定了,并且在运行期间是不可变的。
总结来说,向下转型允许你改变引用的类型,但不能改变对象的实际类型。你只能将引用强制转换为它所指向的对象的实际类型或其子类类型
5、可以调用子类类型中的所有成员
当你将一个父类引用向下转型为子类引用后,你可以通过这个子类引用来访问和调用子类定义的所有成员(包括字段、方法和内部类)。这是因为现在编译器将这个引用视为子类类型,因此允许你访问子类特有的成员。
举个例子来说明这个概念:
class Parent { //父类
void parentMethod() {
System.out.println("Parent method");
}
}
class Child extends Parent { //子类
void childMethod() {
System.out.println("Child method");
}
}
public class go { //主方法
public static void main(String[] args) {
Parent parent = new Child(); // 父类引用指向子类对象
Child child = (Child) parent; // 向下转型
child.parentMethod(); // 可以调用父类方法
child.childMethod(); // 也可以调用子类方法
}
}
在上面的例子中,parent 是一个父类引用,但实际上它指向的是一个 Child 对象。通过向下转型,我们将 parent 转换为 Child 类型的引用 child。现在,我们可以通过 child 引用调用 Parent 类定义的 parentMethod() 方法,也可以调用 Child 类特有的 childMethod() 方法。
变量没有重写之说
首先回顾一下,如果父类Animal和子类dog中出现同样名为show()的方法时,创建对象A: Animal A= new dog(); 并调用 A.show() 方法,那么系统会调用哪个show()方法?
答案是子类中的show方法会覆盖掉父类中的show方法,所以系统会直调用子类中的show方法。但如果将方法替换为变量(属性)呢?
例:重写属性
class Animal {
int num=10;
}
class Dog extends Animal {
int num=20;
}
public class go { //主方法
public static void main(String[] args) {
Animal A=new Dog(); //编辑类型为Animal,运行类型为Dog
System.out.println(A.num);
Dog D=(Dog)A; //强转
System.out.println(A.num);
}
}
运行结果:
10
10
也就是说,系统在调用变量时,会直接从编译类型中查找所需变量,并非像调用方法一样从运行类型中查找。
instanceof比较操作符
此操作符用于判断对象的类型是否为指定类型或指定类型的子类。
基础语法:对象名 instanceof 类名
class Animal { //父类
}
class dog extends Animal {//子类
}
public class go {
public static void main(String[] args) {
dog D = new dog(); //创建dog类对象D
System.out.println(D instanceof dog); //判断D是否为dog类或其子类
System.out.println(D instanceof Animal);//判断D是否为Animal类或其子类
}
}
运行结果:
true
true
第一个判断很好理解,其为dog类,我们再使用instanceof判断他是否为dog类,答案为TRUE;第二个判断则是判断dog类对象D是否是Animal类或者其子类,因为在继承关系中,dog为Animal的子类,所以判断结果也为TRUE。
那么instanceof判断的是编译类型还是运行类型呢?
instanceof判断的类型
instanceof检查的是对象的运行类型,而非编译类型
class Animal { //父类
}
class dog extends Animal { //子类
}
public class go {
public static void main(String[] args) {
Animal D = new dog();
//创建对象D,其编译类型为Animal,运行类型为dog
System.out.println(D instanceof dog); //判断其类是否为dog
System.out.println(D instanceof Animal); //判断其类是否为Animal或其子类
String str="Kaedehara Kazuha"; //创建字符串str
System.out.println(str instanceof Object); //判断其是否为Object或其子类
}
}
运行结果:
true
true
true
第一个判断中,因为对象D编译类型为Animal,运行类型为dog,判断条件为dog类,而输出结果为TRUE;如果其检查类型为编译类型的话,那么第一个判断会因为类型不符而输出FLASE;因此可以得出结论instanceof检查的是对象的运行类型。
第二个判断中,因为运行类型dog为Animal的子类,所以输出结果为TRUE。
第三个判断中,因为String为Object的子类,所以输出结果也为TRUE。
Java动态绑定机制
当调用对象方法时,该方法和该对象的内存地址/运行类型绑定
我们来分析下面这段代码,并分析运行结果及其运行过程:
class Animal { //父类
public int i=10;
public int sum(){
return geti()+10;
}
public int geti(){
return i;
}
}
class dog extends Animal {//子类
public int i=20;
public int geti(){
return i;
}
}
public class go {
public static void main(String[] args) {
Animal A= new dog();
//创建对象A,编译类型为Animal,运行类型为dog
System.out.println(A.sum());
}
}
运行结果
30
我们来分析一下其运行过程,首先为sum方法,因其为方法,所以其先在子类中查找,而子类中不存在sum方法,再到其父类中查找,查找到sum方法并执行,但在sum方法中还存在一个geti方法,而子类和父类中均存在geti方法,那么系统会调用哪个geti方法呢?
答案为运行类型也就是dog中的方法,这就是关于方法的动态绑定机制:当调用一个对象的方法时,实际调用的方法是在运行时根据对象的运行类型确定的,而不是根据编译类型确定的。这就是动态绑定的本质。
但运行类型类中的geti方法需要调用变量i,而子类和父类中均存在变量i,系统会调用哪一个呢?这就引出了变量与动态绑定机制的关系:
当调用对象变量时,没有动态绑定机制,哪里声明 ,哪里使用
也就是说在方法内部需要调用变量时,会直接在该方法所在的类中查找所需变量,无关运行类型或者编译类型;所以在例子中返回了子类中i的值给geti方法,geti的返回值为20,此时再回到父类中的sum方法,其返回值也可得出:20+10=30。
多态数组
数组的定义类型为父类类型,里面保存的实际元素类型为子类类型
我们还是举一个例子以便理解:父类Person,其有两个不同的子类Student和Teacher,
创建一个Person对象,两个Student,两个Teacher对象并存放在长度为5的数组中,再调用每个类都含有的show方法,输出类中的信息:
class Person { //定义父类Person
public String name;
public int age;
public Person() { //默认构造方法
}
public Person(String name, int age) {//带参构造方法
this.name = name;
this.age = age;
}
public String show() { //show方法,用于输出
return name + "\t" + age;
}
}
class Student extends Person {//定义子类Student
public double score;
public Student(String name, int age, double score) {//带参构造
super(name, age);
this.score = score;
}
@Override
public String show() { //重写show方法
return super.show() + "\t成绩为:" + score;
//super.show意为调用父类中的show方法
}
public void stu() {
System.out.println("学生: "+name + " 正在学习 ");
}
}
class Teacher extends Person { //定义子类Teacher
public double sal;
public Teacher(String name, int age, double sal) {//带参构造
super(name, age);
this.sal = sal;
}
@Override
public String show() { //重写show方法
return super.show() + "\t薪资为为:" + sal;
}
public void teach() {
System.out.println("教师: "+name + " 正在教课 ");
}
}
public class go { //主方法
public static void main(String[] args) {
Person[] P = new Person[5];
//定义一个数组,数组长度为5,数组类型为Person
P[0]=new Person("A",20); //分别为其赋值
P[1]=new Student("B",18,100);
P[2]=new Student("C",19,50);
P[3]=new Teacher("D",25,2000);
P[4]=new Teacher("E",24,2100);
for(int i=0;i<P.length;i++){
System.out.println(P[i].show());//依次输出
}
}
}
输出结果:
A 20
B 18 成绩为:100.0
C 19 成绩为:50.0
D 25 薪资为为:2000.0
E 24 薪资为为:2100.0
到此为止还不难理解,接下来如果想要调用子类中特有的方法stu和teach呢?那就要使用向下转型了,但数组中五个成员的运行类型分为三个类,如果每个成员都编写一个对应的向下转型方法很麻烦,我们可以借助 if else 和 instanceof 两个方法相结合的办法来简化代码,将主方法改为:
public class go { //主方法
public static void main(String[] args) {
Person[] P = new Person[5];
//定义一个数组,数组长度为5,数组类型为Person
P[0]=new Person("A",20); //分别为其赋值
P[1]=new Student("B",18,100);
P[2]=new Student("C",19,50);
P[3]=new Teacher("D",25,2000);
P[4]=new Teacher("E",24,2100);
for (int i = 0; i < P.length; i++) {
System.out.println(P[i].show()); //依次输出
if (P[i] instanceof Student) { //判断运行类型是否为Student
Student S = (Student) P[i]; //编译类型向下转型为Student
S.stu(); //执行子类特有方法stu
//可以简化为(Student)P[i]).stu();
} else if (P[i] instanceof Teacher) { //判断运行类型是否为Teacher
((Teacher) P[i]).teach();
} else System.out.println("非校内人员"); //两者均不符则输出"非校内人员"
System.out.println("————————————————————————");
}
}
}
输出结果
A 20
非校内人员
————————————————————————
B 18 成绩为:100.0
学生: B 正在学习
————————————————————————
C 19 成绩为:50.0
学生: C 正在学习
————————————————————————
D 25 薪资为为:2000.0
教师: D 正在教课
————————————————————————
E 24 薪资为为:2100.0
教师: E 正在教课
————————————————————————
多态参数
方法定义的形参类型为父类类型,实参类型允许为子类类型。
我们仍然举一个例子来引入其概念:假设我们有一个Shape类,它有一个color属性,以及Circle和Rectangle两个子类,它们分别代表圆形和矩形。(前文中为方便理解与节省版幅,属性均为public,此例为本博客最后一篇代码,用回常用的private属性,同时为其添加set和get方法。)
class Shape { //父类
protected String color;
public Shape(String color) { //带参构造
this.color = color;
}
public String getColor() { //get和set方法
return color;
}
public void setColor(String color) {
this.color = color;
}
}
class Circle extends Shape { //子类
public Circle(String color) {
super(color);
}
}
class Rectangle extends Shape { //子类
public Rectangle(String color) {
super(color);
}
}
现在,我们创建一个Drawer类,它有一个方法drawShape,该方法接受一个Shape类型的参数,并打印出该形状的颜色:
class Drawer {
public void drawShape(Shape shape) {
System.out.println("Drawing a shape with color: " + shape.getColor());
}
}
public class go {
public static void main(String[] args) {
Drawer drawer = new Drawer();
Shape circle = new Circle("Red");
Shape rectangle = new Rectangle("Blue");
drawer.drawShape(circle); // 输出: Drawing a shape with color: Red
drawer.drawShape(rectangle); // 输出: Drawing a shape with color: Blue
}
}
在这个例子中,drawShape方法接受一个Shape类型的参数,该参数可以是任何Shape的子类实例。当我们调用shape.getColor()时,Java虚拟机会根据实际对象的类型(Circle或Rectangle)来调用相应子类的getColor方法(尽管在这个例子中getColor方法是直接继承自Shape类的)。
重要的是要注意,尽管drawShape方法接受一个Shape类型的参数,但它仍然可以访问和打印出Circle和Rectangle对象的color属性,这是因为color属性在父类Shape中定义,并且被子类所继承。
这就是属性在多态参数中的一个简单应用。多态允许我们编写通用的代码来处理不同的对象类型,而无需关心这些对象的具体类型。这使得代码更加灵活和可维护
完结撒花!!!!第一次写这么长的博客,累死我了