文章目录
- 一、多态的形式和体现
- (1)为什么需要多态性(polymorphism)?
- (2) 对象的多态性
- 二、 多态的理解
- (1)如何理解多态性
- (2)Java中多态性的体现
- (3)多态性的应用
- (4)虚方法调用
- (5)多态的使用注意事项
- (6)举例
- (7)多态的好处和弊端
- 1、好处
- 举例1
- 举例2
- 2、弊端
- (8)成员变量没有多态性
- 三、向上转型与向下转型
- (1)为什么要类型转换
- (2) 如何向上或向下转型
- (3) instanceof关键字
- 四、 练习
- (1)练习1
- (2)练习2
- (3)练习3
- (4)练习4
- 五、面试题
- (1)题目1
- (2)题目2
一千个读者眼中有一千个哈姆雷特。–多种形态
一、多态的形式和体现
(1)为什么需要多态性(polymorphism)?
开发中,有时我们在设计一个数组、或一个成员变量、或一个方法的形参、返回值类型时,无法确定它具体的类型,只能确定它是某个系列的类型。
案例:
(1)声明一个Dog类,包含public void eat()方法,输出“狗啃骨头”
(2)声明一个Cat类,包含public void eat()方法,输出“猫吃鱼仔”
(3)声明一个Person类,功能如下:
- 包含宠物属性
- 包含领养宠物方法 public void adopt(宠物类型Pet)
- 包含喂宠物吃东西的方法 public void feed(),实现为调用宠物对象.eat()方法
public class Dog {public void eat(){System.out.println("狗啃骨头");}
}
public class Cat {public void eat(){System.out.println("猫吃鱼仔");}
}
public class Person {private Dog dog;//adopt:领养public void adopt(Dog dog){this.dog = dog;}//feed:喂食public void feed(){if(dog != null){dog.eat();}}/*问题:1、从养狗切换到养猫怎么办? 修改代码把Dog修改为养猫?2、或者有的人养狗,有的人养猫怎么办? 3、要是还有更多其他宠物类型怎么办?如果Java不支持多态,那么上面的问题将会非常麻烦,代码维护起来很难,扩展性很差。*/
}
(2) 对象的多态性
多态性,是面向对象中最重要的概念,在Java中的体现:
对象的多态性:父类的引用指向子类的对象
格式:(父类类型:指子类继承的父类类型,或者实现的接口类型)
父类类型 变量名 = 子类对象;
举例:
Person p = new Student();Object o = new Person();//Object类型的变量o,指向Person类型的对象o = new Student(); //Object类型的变量o,指向Student类型的对象
对象的多态:在Java中,子类的对象可以替代父类的对象使用。所以,一个引用类型变量可能指向(引用)多种不同类型的对象
二、 多态的理解
(1)如何理解多态性
理解:理解为一个事物的多种形态。
生活举例:
女朋友:我想养一个宠物。(若将宠物看作变量类型,真正赋的值是一个具体的宠物,它有多种)
孩子:我想要一个玩具。(最终的玩具有很多种,不确定)
老板:张秘书,安排一个技术科的同事,跟我一起下周出差。(最终的技术科同事有很多,不确定)
(2)Java中多态性的体现
Java代码层面多态性:比如需要一个人,现在声明就是一个Person,但是实际上给这个Person赋值的时候,new的不是Person,而是Person子类的一个对象。
举例:
父类【Person.java】
package yuyi06;public class Person {//属性String name;int age;//方法public void eat(){System.out.println("人要吃饭");}public void walk(){System.out.println("人要走路");}
}
子类【Man.java】
package yuyi06;public class Man extends Person {//属性boolean isSmoking;//方法//重写public void eat(){System.out.println("男人吃饭不要挑食");}public void walk(){System.out.println("男人走路要端正");}public void earnMoney(){System.out.println("男人要挣钱养家糊口");}
}
子类【Woman.java】
package yuyi06;public class Woman extends Person{//属性boolean isBeauty;//方法//重写public void eat(){System.out.println("女人吃饭要根据自己喜好");}public void walk(){System.out.println("女人走路很有特色");}public void goShopping(){System.out.println("女人喜欢逛街");}
}
测试类【PersonTest.java】
package yuyi06;public class PersonTest {public static void main(String[] args) {//多态性之前Person p1=new Person(); //声明的是Person,new的也是PersonMan m1=new Man();//多态性:子类对象的多态性Person p2=new Man(); //声明的是Person,new的是Man}
}
子类对象的多态性:父类的引用指向子类的对象。(或子类的对象赋给父类的引用)
比如:Person p2 = new Man();
(声明是一个父类的,在new具体对象的时候,拿的是具体子类的对象)
(3)多态性的应用
Java引用变量有两个类型:编译时类型
和运行时类型
。编译时类型由声明
该变量时使用的类型决定,运行时类型由实际赋给该变量的对象
决定。简称:编译时,看左边;运行时,看右边。
- 若编译时类型和运行时类型不一致,就出现了对象的多态性(Polymorphism)
- 多态情况下,“看左边”:看的是父类的引用(父类中不具备子类特有的方法)
“看右边”:看的是子类的对象(实际运行的是子类重写父类的方法)
举例:
测试类【PersonTest.java】
package yuyi06;public class PersonTest {public static void main(String[] args) {//多态性之前Person p1=new Person(); //声明的是Person,new的也是PersonMan m1=new Man();//多态性:子类对象的多态性Person p2=new Man(); //声明的是Person,new的是Man//多态性的应用p2.eat();p2.walk();}
}
上面的代码输出结果?
很明显,p2它new的对象是Man,所以输出就是Man里面重写的方法。如下:
但是此时按住Ctrl键然后点击p2后面的eat()
方法,跳转的却是Person类里面的方法,大家可以自己去试试:
所以,编译器认为这个eat()方法是Person类的(编译的时候),但是真正执行的时候,是右边new的类的方法。
多态性的应用:虚拟方法调用
在多态场景下,调用方法时:
编译时,认为方法是左边声明的父类的类型的方法(即被重写的方法)
执行时,实际执行的是子类重写父类的方法
简称为:编译看左边,运行看右边()。
编译时看左边什么类型,决定后面编译的是谁;运行时看右边子类对象是谁,就调用它重写之后的方法。
看一下字节码文件:
invokevirtual:调用虚的–>虚方法
(4)虚方法调用
在Java中虚方法是指在编译阶段不能确定方法的调用入口地址,在运行阶段才能确定的方法,即可能被重写的方法。
Person e = new Student();
e.getInfo(); //调用Student类的getInfo()方法
子类中定义了与父类同名同参数的方法,在多态情况下,将此时父类的方法称为虚方法,父类根据赋给它的不同子类对象,动态调用属于子类的该方法。这样的方法调用在编译期是无法确定的。
举例:
前提:Person类中定义了welcome()方法,各个子类重写了welcome()。
执行:多态的情况下,调用对象的welcome()方法,实际执行的是子类重写的方法。
拓展:
静态链接(或早起绑定)
:当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时。这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接。那么调用这样的方法,就称为非虚方法调用。比如调用静态方法、私有方法、final方法、父类构造器、本类重载构造器等。
动态链接(或晚期绑定)
:如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接。调用这样的方法,就称为虚方法调用。比如调用重写的方法(针对父类)、实现的方法(针对接口)。
(5)多态的使用注意事项
1、多态性的使用前提
:① 要有类的继承关系 ② 要有方法的重写
2、多态的适用性
:适用于方法,不适用于属性。
【测试属性是否满足多态】
【Person.java】
package yuyi06;public class Person {//属性int id=1001;
}
【Man.java】
package yuyi06;public class Man extends Person {//属性int id=1002;
}
【PersonTest.java】
package yuyi06;public class PersonTest {public static void main(String[] args) {//多态性:子类对象的多态性Person p2=new Man(); //声明的是Person,new的是Man//测试属性是否满足多态性System.out.println(p2.id);}
}
输出结果:
可以看到,输出结果是父类里面声明的值。
Person p2=new Man();
因为声明的就是Person,编译的时候看的是左边,运行时看的也是左边。
综上,属性不满足多态性!!
多态性的应用只看方法,不管属性;本来属性也不建议大家在子父类中去定义重名的。
(6)举例
再举例:
package com.atguigu.polymorphism.grammar;public class Pet {private String nickname; //昵称public String getNickname() {return nickname;}public void setNickname(String nickname) {this.nickname = nickname;}public void eat(){System.out.println(nickname + "吃东西");}
}
package com.atguigu.polymorphism.grammar;public class Cat extends Pet {//子类重写父类的方法@Overridepublic void eat() {System.out.println("猫咪" + getNickname() + "吃鱼仔");}//子类扩展的方法public void catchMouse() {System.out.println("抓老鼠");}
}
package com.atguigu.polymorphism.grammar;public class Dog extends Pet {//子类重写父类的方法@Overridepublic void eat() {System.out.println("狗子" + getNickname() + "啃骨头");}//子类扩展的方法public void watchHouse() {System.out.println("看家");}
}
1、方法内局部变量的赋值体现多态
package com.atguigu.polymorphism.grammar;public class TestPet {public static void main(String[] args) {//多态引用Pet pet = new Dog();pet.setNickname("小白");//多态的表现形式/*编译时看父类:只能调用父类声明的方法,不能调用子类扩展的方法;运行时,看“子类”,如果子类重写了方法,一定是执行子类重写的方法体;*/pet.eat();//运行时执行子类Dog重写的方法
// pet.watchHouse();//不能调用Dog子类扩展的方法pet = new Cat();pet.setNickname("雪球");pet.eat();//运行时执行子类Cat重写的方法}
}
2、方法的形参声明体现多态
package com.atguigu.polymorphism.grammar;public class Person{private Pet pet;public void adopt(Pet pet) {//形参是父类类型,实参是子类对象this.pet = pet;}public void feed(){pet.eat();//pet实际引用的对象类型不同,执行的eat方法也不同}
}
package com.atguigu.polymorphism.grammar;public class TestPerson {public static void main(String[] args) {Person person = new Person();Dog dog = new Dog();dog.setNickname("小白");person.adopt(dog);//实参是dog子类对象,形参是父类Pet类型person.feed();Cat cat = new Cat();cat.setNickname("雪球");person.adopt(cat);//实参是cat子类对象,形参是父类Pet类型person.feed();}
}
3、方法返回值类型体现多态
package com.atguigu.polymorphism.grammar;public class PetShop {//返回值类型是父类类型,实际返回的是子类对象public Pet sale(String type){switch (type){case "Dog":return new Dog();case "Cat":return new Cat();}return null;}
}
package com.atguigu.polymorphism.grammar;public class TestPetShop {public static void main(String[] args) {PetShop shop = new PetShop();Pet dog = shop.sale("Dog");dog.setNickname("小白");dog.eat();Pet cat = shop.sale("Cat");cat.setNickname("雪球");cat.eat();}
}
(7)多态的好处和弊端
1、好处
好处:变量引用的子类对象不同,执行的方法就不同,实现动态绑定。代码编写更灵活、功能更强大,可维护性和扩展性更好了。
极大的减少了代码的冗余,不需要定义多个重载的方法。
举个例子看一下多态使用的场景。
举例1
package yuyi07;/*** ClassName: AnimalTest* Package: yuyi07* Description:** @Author 雨翼轻尘* @Create 2023/11/7 0007 20:52*/
public class AnimalTest {public static void main(String[] args) {AnimalTest test=new AnimalTest(); //通过test来调用adopt方法}public void adopt(Animal animal){//只能调用Animal里面声明的动物animal.eat();animal.jump();}
}class Animal{public void eat(){System.out.println("动物吃饭饭");}public void jump(){System.out.println("动物跳高高");}
}
在AnimalTest里,此时在某一个场景下想要调用adopt
方法。就需要造一个对象test,然后通过test来调用adopt方法。
①没有多态性
若是没有多态性,因为在adopt()方法里面参数声明的是Animal类型,所以在用test调用adopt()方法的时候,参数也需要是Animal类型的。
这个时候都不知道养的是啥,如果没有多态性,此时想领养一只狗,就需要再声明一个参数是狗的方法。如下:
public class AnimalTest {public static void main(String[] args) {AnimalTest test=new AnimalTest();test.adopt(new Animal());}public void adopt(Animal animal){//只能调用Animal里面声明的动物2animal.eat();animal.jump();}//方法:动物狗public void adopt(Dog dog){dog.eat();dog.jump();}//方法:动物猫public void adopt(Cat cat){cat.eat();cat.jump();}//...
}
如果还有更多的需要领养的动物,那么就需要不停地AnimalTest类里面写adopt()方法的重载
。
②有多态性
若此时有多态性,就可以在AnimalTest方法外面写关于很多动物的类,将它继承于Animal,那么在用test调用adopt()方法的时候,就可以在参数里面new任何一个动物了,不再需要继续在AnimalTest类里面声明adopt()方法的重载了。
如下:
package yuyi07;public class AnimalTest {public static void main(String[] args) {AnimalTest test=new AnimalTest();test.adopt(new Dog());}public void adopt(Animal animal){//只能调用Animal里面声明的动物animal.eat();animal.jump();}
}class Animal{public void eat(){System.out.println("动物吃饭饭");}public void jump(){System.out.println("动物跳高高");}
}//类:关于狗
class Dog extends Animal {public void eat(){System.out.println("小狗进食");}public void jump(){System.out.println("狗急跳墙");}public void watchDoor(){System.out.println("小狗看家");}
}//类:关于猫
class Cat extends Animal {public void eat(){System.out.println("小猫吃小鱼");}public void jump(){System.out.println("小猫跳舞");}public void catMouse(){System.out.println("猫抓老鼠");}
}
执行结果:
⚡注意
test.adopt(new Dog());
形式上来看,匿名对象new Dog()赋值给了animal。
此时就构成多态的“形”了,父类(Animal)的引用指向子类(Dog)的对象:Animal animal=new Dog();
编译的时候(创建AnimalTest类的时候),调用animal的eat()方法和jump()方法,只能调用Animal类里面有的方法,所以编译的时候,eat()方法和jump()方法都是Animal类里面的。
但是真正执行的时候:test.adopt(new Dog());
,传递的是Dog对象,所以最终执行结果表示的是Dog类里面重写的方法,如下:
编译时看左边,运行时看右边
若此时想养猫,可以直接new一个Cat对象,test调用的adopt()还是同一个方法,以多态的方式去呈现。编译左边,运行时右边。
package yuyi07;/*** ClassName: AnimalTest* Package: yuyi07* Description:** @Author 雨翼轻尘* @Create 2023/11/7 0007 20:52*/
public class AnimalTest {public static void main(String[] args) {AnimalTest test=new AnimalTest();test.adopt(new Dog());test.adopt(new Cat());}public void adopt(Animal animal){ //Animal animal=new Dog(); 声明的是Animal,实际上new了一个Dog//只能调用Animal里面声明的动物animal.eat();animal.jump();}/*public void adopt(Dog dog){dog.eat();dog.jump();}public void adopt(Cat cat){cat.eat();cat.jump();}*/
}class Animal{public void eat(){System.out.println("动物吃饭饭");}public void jump(){System.out.println("动物跳高高");}
}class Dog extends Animal {public void eat(){System.out.println("小狗进食");}public void jump(){System.out.println("狗急跳墙");}public void watchDoor(){System.out.println("小狗看家");}
}class Cat extends Animal {public void eat(){System.out.println("小猫吃小鱼");}public void jump(){System.out.println("小猫跳舞");}public void catMouse(){System.out.println("猫抓老鼠");}
}
运行结果:
多态性不光是少定义了几个重载的方法,在实际开发当中,Animal 只能写这一个父类,因为不知道具体有多少个子类。
举例2
再举个例子巩固一下多态的使用。
class Account{public void withdraw(){} //取钱
}class CheckAccount extends Account{ //信用卡//存在方法的重写public void withdraw(){} //取钱
}
class SavingAccount extends Account{ //储蓄卡//存在方法的重写
}class Customer{Account account;public void setAccount(Account account){this.account = account;}public Account getAccount(){return accout;}}class CustomerTest{main(){Customer cust = new Customer();cust.setAccount(new CheckAccount());cust.getAccount().withdraw(); //取钱}
}
⚡注意
1.多态场景:
2.编译是Account,真正调用运行的是CheckAccount里面重写的方法:
3.这里只需要传递一个账户就好,具体什么账户,其实都是Account子类的对象,去银行办卡都是具体的卡(比如信用卡、储蓄卡等),不会是抽象的Account。
2、弊端
弊端:一个引用类型变量如果声明为父类的类型,但实际引用的是子类对象,那么该变量就不能再访问子类中添加的属性和方法。
在多态的场景下,我们创建了子类的对象,也加载了子类特有的属性和方法。但是由于声明为父类的引用,
导致我们没有办法直接调用子类特有的属性和方法。
举例
Student m = new Student();
m.school = "pku"; //合法,Student类有school成员变量
Person e = new Student();
e.school = "pku"; //非法,Person类没有school成员变量// 属性是在编译时确定的,编译时e为Person类型,没有school成员变量,因而编译错误。
开发中:
使用父类做方法的形参,是多态使用最多的场合。即使增加了新的子类,方法也无需改变,提高了扩展性,符合开闭原则。
【开闭原则OCP】
- 对扩展开放,对修改关闭
- 通俗解释:软件系统中的各种组件,如模块(Modules)、类(Classes)以及功能(Functions)等,应该在不修改现有代码的基础上,引入新功能
接下来还是拿之前的例子来看:
①问题1:Person p2 = new Man();
针对于创建的对象,在内存中是否加载了Man类中声明的特有的属性和方法?
加载了!这当然是有的,new的就是Man,自己类里面和父类里面的功能都有加载进内存的。
②问题2:能不能直接调用Man中加载的特有的属性和方法呢?
不能。
比如:
在方法里面来看,下面两种写法明显第二种更好:
//1.多态
Person p2=new Man(); //只能调用父类Person中的方法,Man中的方法只是加载到内存中了,但是调用不了//2.非多态
Man m2=new Man(); //Man里面的方法和父类中的方法都可以调用//第二种写法很明显比第一种好。
多态主要使用的场景就是在形参的场景下。比如:
既然Person p2 = new Man();
内存中有Man特有的属性和方法,如果在特殊场景下想调用可以吗?
上面咱们只说了不能直接调用,那处理之后呢?
怎么处理?这里涉及到一个操作–向下转型。(请看第三个大标题)
(8)成员变量没有多态性
- 若子类重写了父类方法,就意味着子类里定义的方法彻底覆盖了父类里的同名方法,系统将不可能把父类里的方法转移到子类中。
- 对于实例变量则不存在这样的现象,即使子类里定义了与父类完全相同的实例变量,这个实例变量依然不可能覆盖父类中定义的实例变量
package com.atguigu.polymorphism.grammar;public class TestVariable {public static void main(String[] args) {Base b = new Sub();System.out.println(b.a);System.out.println(((Sub)b).a);Sub s = new Sub();System.out.println(s.a);System.out.println(((Base)s).a);}
}
class Base{int a = 1;
}
class Sub extends Base{int a = 2;
}
三、向上转型与向下转型
首先,一个对象在new的时候创建是哪个类型的对象,它从头至尾都不会变。即这个对象的运行时类型,本质的类型用于不会变。
但是,把这个对象赋值给不同类型的变量时,这些变量的编译时类型却不同。
(1)为什么要类型转换
因为多态,就一定会有把子类对象赋值给父类变量的时候,这个时候,在编译期间
,就会出现类型转换的现象。
但是,使用父类变量接收了子类对象之后,我们就不能调用
子类拥有,而父类没有的方法了。这也是多态给我们带来的一点"小麻烦"。所以,想要调用子类特有的方法,必须做类型转换,使得编译通过
。
基本数据类型之间的自动类型提升与强制类型转换。
比如int类型的变量i,将它赋值给double类型的变量j,就是自动类型提升:double j=int i;
若再将double j转换为int i,就需要强制类型转换:int i=(int)double j;
比如:
- 向上转型:当左边的变量的类型(父类) > 右边对象/变量的类型(子类),我们就称为
向上转型
- 此时,编译时按照左边变量的类型处理,就只能调用父类中有的变量和方法,不能调用子类特有的变量和方法了
- 但是,运行时,仍然是对象本身的类型,所以执行的方法是子类重写的方法体。
- 此时,一定是安全的,而且也是自动完成的
- 向下转型:当左边的变量的类型(子类)< 右边对象/变量的编译时类型(父类),我们就称为
向下转型
- 此时,编译时按照左边变量的类型处理,就可以调用子类特有的变量和方法了
- 但是,运行时,仍然是对象本身的类型
- 不是所有通过编译的向下转型都是正确的,可能会发生ClassCastException,为了安全,可以通过isInstanceof关键字进行判断
(2) 如何向上或向下转型
向上转型:自动完成
向下转型:(子类类型)父类变量
【举例1】
package com.atguigu.polymorphism.grammar;public class ClassCastTest {public static void main(String[] args) {//没有类型转换Dog dog = new Dog();//dog的编译时类型和运行时类型都是Dog//向上转型Pet pet = new Dog();//pet的编译时类型是Pet,运行时类型是Dogpet.setNickname("小白");pet.eat();//可以调用父类Pet有声明的方法eat,但执行的是子类重写的eat方法体
// pet.watchHouse();//不能调用父类没有的方法watchHouseDog d = (Dog) pet;System.out.println("d.nickname = " + d.getNickname());d.eat();//可以调用eat方法d.watchHouse();//可以调用子类扩展的方法watchHouseCat c = (Cat) pet;//编译通过,因为从语法检查来说,pet的编译时类型是Pet,Cat是Pet的子类,所以向下转型语法正确//这句代码运行报错ClassCastException,因为pet变量的运行时类型是Dog,Dog和Cat之间是没有继承关系的}
}
【举例2】
【Man.java】
package yuyi06;/*** ClassName: Man* Package: yuyi06* Description:** @Author 雨翼轻尘* @Create 2023/11/6 0006 8:54*/
public class Man extends Person {//属性boolean isSmoking;int id=1002;//方法//重写public void eat(){System.out.println("男人吃饭不要挑食");}public void walk(){System.out.println("男人走路要端正");}public void earnMoney(){System.out.println("男人要挣钱养家糊口");}
}
【PersonTest1.java】
package yuyi06;/*** ClassName: PersonTest1* Package: yuyi08* Description:** @Author 雨翼轻尘* @Create 2023/11/8 0008 23:50*/
public class PersonTest1 {public static void main(String[] args) {Person p1=new Man();//不能调用子类特有的结构:/* p1.earnMoney(); //通过p1去调用Man里面特有的方法是不可以的p1.isSomking; //通过p1去调用Man里面特有的属性也是不可以的*///向下转型Man m1=(Man) p1; //想声明为一个Man类型,括号里面写的就是Man//此时可以调用Man类里面特有的属性和方法了m1.earnMoney();System.out.println(m1.isSmoking);}
}
👻执行结果
⚡注意
1.不能调用子类特有的结构:
2.向下转型:使用强转符
3.此时p1和m1指向的是同一个堆空间中的对象
Person p1=new Man();
Man m1=(Man) p1;
可以测试一下:
输出结果:
测试结果为true,说明p1与m1指向堆空间的同一个对象。
那为啥不能用p1直接调用Man()里面的特有方法呢?是因为声明的类型不一样,只需要将类型强转一下就好了。
4.向下转型可能会出现:类型转换异常
之前说基本数据类型的强制类型转换的时候,说到了精度损失的问题,比如double类型的数据转换为int类型的数据,就会有精度损失。
这里向下转型的时候,也会出现这种类似的情况。
比如:
//举例2
Person p2=new Woman();
Man m2=(Man)p2; //将Woman强转为Man
m2.earnMoney(); //这里虽然可以通过m2调用Man里面的方法,但是内存中并没有加载Man里面特有方法,所以执行的时候不可以了
运行报错:ClassCastException
根本不是这个类型的,非要强转就会出错,但是对于编译器来说并不会报错。(编译时正确,运行时报错)
(3) instanceof关键字
为了避免ClassCastException的发生,Java提供了 instanceof
关键字,给引用变量做类型的校验。如下代码格式:
//检验对象a是否是数据类型A的对象,返回值为boolean型
对象a instanceof 数据类型A
- 举例1
package com.atguigu.polymorphism.grammar;public class TestInstanceof {public static void main(String[] args) {Pet[] pets = new Pet[2];pets[0] = new Dog();//多态引用pets[0].setNickname("小白");pets[1] = new Cat();//多态引用pets[1].setNickname("雪球");for (int i = 0; i < pets.length; i++) {pets[i].eat();if(pets[i] instanceof Dog){Dog dog = (Dog) pets[i];dog.watchHouse();}else if(pets[i] instanceof Cat){Cat cat = (Cat) pets[i];cat.catchMouse();}}}
}
-
说明:
- 只要用instanceof判断返回true的,那么强转为该类型就一定是安全的,不会报ClassCastException异常。
- 如果对象a属于类A的子类B,a instanceof A值也为true。
- 要求对象a所属的类与类A必须是子类和父类的关系,否则编译错误。
-
举例2
还是刚才报错的例子,如下:
//举例2
Person p2=new Woman();
// Man m2=(Man)p2; //将Woman强转为Man
// m2.earnMoney(); //这里虽然可以通过m2调用Man里面的方法,但是内存中并没有加载Man里面特有方法,所以执行的时候不可以了//建议在向下转型之前,使用instanceof进行判断,避免出现类型转换异常
if(p2 instanceof Man){ //判断一下p2是不是Man,若是Man则返回true,则可以转换Man m2=(Man)p2; //可以强转,并且调用Man里面的方法m2.earnMoney();
}
再次输出看一下,可以发现没有异常了,因为压根没有到if里面去,就没有转换成功!
⚡注意
-
建议在向下转型之前,使用
instanceof
进行判断,避免出现类型转换异常 -
格式:
a instanceOf A
: 判断对象a是否是类A的实例。结果是布尔类型。
左边a是一个对象,右边A是一个类型,若对象a是A类型的一个对象,那么就是true,否则是false。
若此时比较的是:p2是不是Woman类型(p2 instanceof Woman),那就会是true,可以进入(既然能进入,那么强转是没有问题的),如下:
//举例2
Person p2=new Woman();
// Man m2=(Man)p2; //将Woman强转为Man
// m2.earnMoney(); //这里虽然可以通过m2调用Man里面的方法,但是内存中并没有加载Man里面特有方法,所以执行的时候不可以了//建议在向下转型之前,使用instanceof进行判断,避免出现类型转换异常
if(p2 instanceof Man){ //判断一下p2是不是Man,若是Man则返回true,则可以转换Man m2=(Man)p2; //可以强转,并且调用Man里面的方法m2.earnMoney();
}if(p2 instanceof Woman){System.out.println("Woman"); //能够打印输出
}
输出:
- 如果
a instanceOf A
返回true,则:
a instanceOf superA 返回也是true。其中,A 是superA的子类。
比如:
问a是不是一个Woman?若是true,则a是一个Person。其中Woman是Person的子类。
只要将Woman换成是它的父类,就都可以进入if语句,如下:
//举例2
Person p2=new Woman();
// Man m2=(Man)p2; //将Woman强转为Man
// m2.earnMoney(); //这里虽然可以通过m2调用Man里面的方法,但是内存中并没有加载Man里面特有方法,所以执行的时候不可以了//建议在向下转型之前,使用instanceof进行判断,避免出现类型转换异常
if(p2 instanceof Man){ //判断一下p2是不是Man,若是Man则返回true,则可以转换Man m2=(Man)p2; //可以强转,并且调用Man里面的方法m2.earnMoney();
}if(p2 instanceof Woman){System.out.println("Woman"); //能够打印输出
}if(p2 instanceof Person){System.out.println("Person");
}if(p2 instanceof Object){System.out.println("Object");
}
输出结果:
- 结论
如果在开发中需要想要向下转型(想要调用具体的对象实体的特有属性和方法),在转换之前,需要用instanceof
去判断一下是否可以转换,以保证程序的健壮性。
向上转型:多态(前提:继承性、方法的重写)
向下转型:强转
四、 练习
(1)练习1
🌋题目描述
①定义三个类,父类GeometricObject代表几何形状,子类Circle代表圆形,MyRectangle代表矩形。
②定义一个测试类GeometricTest,
编写equalsArea方法测试两个对象的面积是否相等(注意方法的参数类型使用父类,体现多态性),
编写displayGeometricObject方法显示对象的面积(注意方法的参数类型)。
类关系图:
💨代码
①
【GeometricObject.java】
package yuyi08;/*** ClassName: GeometricObject* Package: yuyi08* Description:** @Author 雨翼轻尘* @Create 2023/11/9 0009 9:33*/
public class GeometricObject {//属性protected String color;protected double weight;//构造器protected GeometricObject(String color,double weight){this.color=color;this.weight=weight;}//方法public String getColor(){return color;}public void setColor(String color){this.color=color;}public double getWeight() {return weight;}public void setWeight(double weight) {this.weight = weight;}//求面积public double findArea(){return 0.0;}
}
【Circle.java】
package yuyi08;/*** ClassName: Circle* Package: yuyi08* Description:** @Author 雨翼轻尘* @Create 2023/11/9 0009 9:58*/
public class Circle extends GeometricObject {//在父类构造器中只写了一个带参的构造器,子类由于没有声明构造器,默认是空参构造器,首行默认super(),又因为父类没有super(),所以会报错//属性private double radius; //半径//构造器(显示调用父类中带参的构造器)public Circle(String color, double weight, double radius) {super(color, weight); //用自动生成(Insert+Alt)也会自动调用父类构造器this.radius = radius;}//方法public double getRadius() {return radius;}public void setRadius(double radius) {this.radius = radius;}//计算圆的面积(对父类中的方法重写)@Overridepublic double findArea() { //直接敲findArea,会有弹窗显示,点击回车之后自动生成这个重写方法//return super.findArea(); //默认和父类方法一致,直接return父类的方法了return Math.PI*radius*radius;}
}
【MyRectangle.java】
package yuyi08;/*** ClassName: MyRectangle* Package: yuyi08* Description:** @Author 雨翼轻尘* @Create 2023/11/9 0009 10:11*/
public class MyRectangle extends GeometricObject{//属性private double width; //宽private double height; //高//构造器public MyRectangle(String color, double weight, double width, double height) {super(color, weight);this.width = width;this.height = height;}//方法public double getWidth() {return width;}public void setWidth(double width) {this.width = width;}public double getHeight() {return height;}public void setHeight(double height) {this.height = height;}//计算矩形的面积@Overridepublic double findArea() {return width*height;}
}
②
【GeometricTest.java】
package yuyi08;/*** ClassName: GeometricTest* Package: yuyi08* Description:* 编写equalsArea方法测试两个对象的面积是否相等(注意方法的参数类型使用父类,体现多态性),* 编写displayGeometricObject方法显示对象的面积(注意方法的参数类型)。** @Author 雨翼轻尘* @Create 2023/11/9 0009 10:18*/
public class GeometricTest {/*** 比较两个几何图形的面积是否相等* @param g1* @param g2* @return true:面积相等 false:面积不相等*/public boolean equalsArea(GeometricObject g1,GeometricObject g2){ //调用equalsArea方法的是GeometricTest类,它与几何图形没有关系,所以两个几何图形就需要通过形参来体现了return g1.findArea() == g2.findArea(); //基本数据类型用不了equals}/*** 显示几何图形的面积 displayGeometricObject //display:显示 Geometric:几何 Object:图形* @param g*/public void displayGeometricObject(GeometricObject g){ //具体要显示哪个几何图形呢?肯定不是调用者(GeometricTest),所以需要有参数System.out.println("几何图形的面积为: "+g.findArea());}public static void main(String[] args) {//在当前类中调用上面的方法GeometricTest test=new GeometricTest();//测试1Circle c1=new Circle("red",1.0,2.3);Circle c2=new Circle("blue",1.0,3.3);test.displayGeometricObject(c1); //显示c1几何图形的面积 GeometricObject g=new Circle("red",1.0,2.3);test.displayGeometricObject(c2); //显示c2几何图形的面积boolean isEqual=test.equalsArea(c1,c2);if(isEqual){System.out.println("面积相等");}else{System.out.println("面积不相等");}//测试2//用匿名对象来写(这个对象后面也不再使用,就用匿名来写即可)test.displayGeometricObject(new MyRectangle("blue",1.0,2.3,4.5));}
}
👻运行结果
⚡注意
多态性的体现:
声明的时候是父类的引用,赋值的时候是一个子类的实例。(子类对象的多态性)
GeometricObject g=new Circle("red",1.0,2.3);
多态性:多种形态。比如左边需要一个几何图形,右边给了一个具体的图形。
当我们按住Ctrl键,点击findArea()方法,会跳转到父类中,此时认为是父类中声明的(编译时);
在真正运行的时候,执行的是子类重写父类的方法。
所以最终的运行结果不是父类里面写的0.0,而是具体的传过去的几何图形的面积:
这就是虚方法调用(虚拟方法调用),又叫动态绑定
,编译的时候是方法A,运行的时候是方法B,编译与运行调用的不是同一个方法。动态方法可以重写。
还有一个是静态绑定
,编译的时候是一个方法,运行的时候调用的还是这个方法,编译与运行调用的是同一个方法。静态方法没有重写一说。
(2)练习2
🌋题目描述
修改AnimalTest类的方法,在判断Animal是Dog或Cat时,向下转型,并调用各自特有的方法。
【AnimalTest.java】
package yuyi09;/*** ClassName: AnimalTest* Package: yuyi07* Description:** @Author 雨翼轻尘* @Create 2023/11/7 0007 20:52*/
public class AnimalTest {public static void main(String[] args) {AnimalTest test=new AnimalTest();test.adopt(new Dog());test.adopt(new Cat());}public void adopt(Animal animal){ //Animal animal=new Dog(); 声明的是Animal,实际上new了一个Dog//只能调用Animal里面声明的动物animal.eat();animal.jump();}/*public void adopt(Dog dog){dog.eat();dog.jump();}public void adopt(Cat cat){cat.eat();cat.jump();}*/
}class Animal{public void eat(){System.out.println("动物吃饭饭");}public void jump(){System.out.println("动物跳高高");}
}class Dog extends Animal {public void eat(){System.out.println("小狗进食");}public void jump(){System.out.println("狗急跳墙");}public void watchDoor(){System.out.println("小狗看家");}
}class Cat extends Animal {public void eat(){System.out.println("小猫吃小鱼");}public void jump(){System.out.println("小猫跳舞");}public void catMouse(){System.out.println("猫抓老鼠");}
}
💨代码
【AnimalTest.java】
package yuyi09;/*** ClassName: AnimalTest* Package: yuyi07* Description:** @Author 雨翼轻尘* @Create 2023/11/7 0007 20:52*/
public class AnimalTest {public static void main(String[] args) {AnimalTest test=new AnimalTest();test.adopt(new Dog());test.adopt(new Cat());}public void adopt(Animal animal){ //Animal animal=new Dog(); 声明的是Animal,实际上new了一个Dog//只能调用Animal里面声明的动物animal.eat();animal.jump();//animal.watchDoor(); //不可以/*//不够健壮Dog animal1=(Dog)animal;animal1.watchDoor();*///完美if(animal instanceof Dog){Dog animal1=(Dog)animal;animal1.watchDoor();} else if (animal instanceof Cat) {Cat animal2=(Cat)animal;animal2.catMouse();}}/*public void adopt(Dog dog){dog.eat();dog.jump();}public void adopt(Cat cat){cat.eat();cat.jump();}*/
}class Animal{public void eat(){System.out.println("动物吃饭饭");}public void jump(){System.out.println("动物跳高高");}
}class Dog extends Animal {public void eat(){System.out.println("小狗进食");}public void jump(){System.out.println("狗急跳墙");}public void watchDoor(){System.out.println("小狗看家");}
}class Cat extends Animal {public void eat(){System.out.println("小猫吃小鱼");}public void jump(){System.out.println("小猫跳舞");}public void catMouse(){System.out.println("猫抓老鼠");}public Cat() {}
}
🗃️分析
在测试里面写了一个adopt方法,参数是animal。
在编写的时候,只知道是animal,所以在调用的时候,只能调用父类中声明的方法(编译看左边)。如下:、
public void adopt(Animal animal){ //Animal animal=new Dog(); 声明的是Animal,实际上new了一个Dog//只能调用Animal里面声明的动物animal.eat();animal.jump();
}
若用animal去调用Dog类里面的方法是不可以的,因为Animal类里面并没有这个方法。如下:
animal.watchDoor(); //不可以
此时会报错:
若是想调用Dog类里面的方法必须要做强转,将animal转成Dog,如下:
Dog animal1=(Dog)animal;
animal1.watchDoor();
此时就可以用animal1去调用Dog特有的方法,编译器不会报错:
但问题在于,此时不一定是狗啊。
所以此时new Dog() 没有问题,如下:
但执行new Cat()的时候就会报错了:
这时候adopt()方法就不够健壮。
在test.adopt(new Cat());
的时候调用了adopt()方法,然后Dog animal1=(Dog)animal;
在adopt()方法里面又调用了Dog,所以就会挂掉。
要想不出异常,在刚才的强转之前,需要加一个判断,判断animal是不是Dog,若是,再调用。Cat也一样。如下:
public void adopt(Animal animal){ //Animal animal=new Dog(); 声明的是Animal,实际上new了一个Dog//只能调用Animal里面声明的动物animal.eat();animal.jump();//animal.watchDoor(); //不可以/*//不够健壮Dog animal1=(Dog)animal;animal1.watchDoor();*///完美if(animal instanceof Dog){Dog animal1=(Dog)animal;animal1.watchDoor();} else if (animal instanceof Cat) {Cat animal2=(Cat)animal;animal2.catMouse();}
}
这样运行才是正确的:
⚡注意
看一下test.adopt(new Cat());
的调用过程:
(3)练习3
🌋题目描述
已知代码如下:
class Person {protected String name="person";protected int age=50;public String getInfo() {return "Name: "+ name + "\n" +"age: "+ age;}
}
class Student extends Person {protected String school="pku";public String getInfo() {return "Name: "+ name + "\nage: "+ age+ "\nschool: "+ school;}
}
class Graduate extends Student{public String major="IT";public String getInfo(){return "Name: "+ name + "\nage: "+ age+ "\nschool: "+ school+"\nmajor:"+major;}
}
建立InstanceTest 类,在类中定义方法method(Person e);
在method中:
(1)根据e的类型调用相应类的getInfo()方法。
(2)根据e的类型执行:
如果e为Person类的对象,输出:
“a person”;
如果e为Student类的对象,输出:
“a student”
“a person ”
如果e为Graduate类的对象,输出:
“a graduated student”
“a student”
“a person”
💨代码
【InstanceTest.java】
package yuyi10;/*** ClassName: InstanceTest* Package: yuyi10* Description:* 建立InstanceTest 类,在类中定义方法method(Person e);* 在method中:* (1)根据e的类型调用相应类的getInfo()方法。* (2)根据e的类型执行:* 如果e为Person类的对象,输出:* “a person”;* 如果e为Student类的对象,输出:* “a student”* “a person ”* 如果e为Graduate类的对象,输出:* “a graduated student”* “a student”* “a person”* @Author 雨翼轻尘* @Create 2023/11/10 0010 15:20*/
public class InstanceTest {public void method(Person e){//(1)根据e的类型调用相应类的getInfo()方法。需要判断一下吗?不需要//是哪个类的,就可以直接调用它的getInfo()方法System.out.println(e.getInfo()); //虚方法调用,不用指明是哪个类型//(2)根据e的类型执行//方式1/*if(e instanceof Graduate){System.out.println("a graduated student");System.out.println("a student");System.out.println("a person");} else if (e instanceof Student) {System.out.println("a student");System.out.println("a person");}else{System.out.println("a person");}*///方式2if(e instanceof Graduate){System.out.println("a graduated student");}if(e instanceof Student){System.out.println("a student");}if(e instanceof Person){System.out.println("a person");}}public static void main(String[] args) {InstanceTest test=new InstanceTest();test.method(new Student());}
}
👻运行结果
⚡注意
根据e的类型调用相应类的getInfo()方法。需要判断一下吗?不需要!是哪个类的,就可以直接调用它的getInfo()方法即可。如下:
package yuyi10;/*** ClassName: InstanceTest* Package: yuyi10* Description:* 建立InstanceTest 类,在类中定义方法method(Person e);* 在method中:* (1)根据e的类型调用相应类的getInfo()方法。* @Author 雨翼轻尘* @Create 2023/11/10 0010 15:20*/
public class InstanceTest {public void method(Person e){//根据e的类型调用相应类的getInfo()方法。需要判断一下吗?不需要/*if(e instanceof Graduate){} else if (e instanceof Student) {} else if (e instanceof Person) {}*///是哪个类的,就可以直接调用它的getInfo()方法e.getInfo();}
}
(4)练习4
🌋题目描述
1、在包中声明人Person、男人Man、女人Woman类
(1)在Person类中,包含
①public void eat():打印吃饭
②public void toilet():打印上洗手间
(2)在Man类中,包含
①重写上面的方法
②增加 public void smoke():打印抽烟
(3)在Woman类中,包含
①重写上面的方法
②增加 public void makeup():打印化妆
2、在包中声明测试类Exer4
1)public static void meeting(Person… ps)
在该方法中,每一个人先吃饭,然后上洗手间,然后如果是男人,随后抽根烟;如果是女人,随后化个妆
(2)public static void main(String[] args)
在主方法中,创建多个男人和女人对象,并调用meeting()方法进行测试
💨代码
【Person.java】
package yuyi11;/*** ClassName: Person* Package: yuyi11* Description:* (1)在Person类中,包含* ①public void eat():打印吃饭* ②public void toilet():打印上洗手间* @Author 雨翼轻尘* @Create 2023/11/10 0010 16:36*/
public class Person {//打印吃饭public void eat(){System.out.println("人吃饭");}//打印上洗手间public void toilet(){System.out.println("人去洗手间");}
}
【Man.java】
package yuyi11;/*** ClassName: Man* Package: yuyi11* Description:* (2)在Man类中,包含* ①重写上面的方法* ②增加 public void smoke():打印抽烟* @Author 雨翼轻尘* @Create 2023/11/10 0010 16:37*/
public class Man extends Person{//打印吃饭public void eat(){System.out.println("男人大口地吃饭");}//打印上洗手间public void toilet(){System.out.println("男人去男洗手间");}//打印抽烟public void smoke(){System.out.println("男人不要抽太多烟");}
}
【Woman.java】
package yuyi11;/*** ClassName: Woman* Package: yuyi11* Description:* (3)在Woman类中,包含* ①重写上面的方法* ②增加 public void makeup():打印化妆* @Author 雨翼轻尘* @Create 2023/11/10 0010 16:38*/
public class Woman extends Person {//打印吃饭public void eat(){System.out.println("女人优雅地吃饭");}//打印上洗手间public void toilet(){System.out.println("女人去女洗手间");}//打印化妆public void makeup(){System.out.println("女人画美美的妆");}}
【Exer4.java】
package yuyi11;/*** ClassName: Exer4* Package: yuyi11* Description:* (1)public static void meeting(Person... ps)* 在该方法中,每一个人先吃饭,然后上洗手间,然后如果是男人,随后抽根烟;如果是女人,随后化个妆** (2)public static void main(String[] args)* 在主方法中,创建多个男人和女人对象,并调用meeting()方法进行测试* @Author 雨翼轻尘* @Create 2023/11/10 0010 16:45*/
public class Exer4 {public void meeting(Person... ps){ //可变形参,相当于一个数组,ps相当于数组名for (int i = 0; i < ps.length; i++) {ps[i].eat();ps[i].toilet();if(ps[i] instanceof Woman){Woman woman=(Woman) ps[i];woman.makeup();}else if(ps[i] instanceof Man){Man man=(Man) ps[i];man.smoke();}//换行System.out.println();}}public static void main(String[] args) {Exer4 exer=new Exer4(); //调用非静态地方法,需要通过对象去调用exer.meeting(new Man(),new Woman(),new Man());}
}
👻运行结果
⚡补充
if(ps[i] instanceof Woman w){//Woman woman=(Woman) ps[i];//woman.makeup();w.makeup();
}
JDK7新特性:
如图:
instanceof
后面不仅可以写子类,也可以写父类,比如Object,如下:
但是如果是随意一个类,比如String,编译都会报错,如下:
这里的变量ps有类型,这里是Person,我们通常用instanceof是要判断另一个是不是这个类型的,那么这里基本是拿着基于Person的父类或子类来比较的,编译都可以通过,运行就要看比较结果是true还是false了。
若是这个类与Person没有任何关系(比如String),那么就不可以(光是判断也不可能是true),编译都不会通过。
五、面试题
(1)题目1
下面代码输出结果是多少?
package com.atguigu06.polymorphism.interview;class Base {int count = 10;public void display() {System.out.println(this.count);}
}class Sub extends Base {int count = 20;public void display() {System.out.println(this.count);}
}public class FieldMethodTest {public static void main(String[] args){Sub s = new Sub();System.out.println(s.count);//20s.display();//20Base b = s;System.out.println(b == s); //trueSystem.out.println(b.count);//10b.display();//20Base b1 = new Base();System.out.println(b1.count); //10b1.display();//10}
}
👻输出结果
🤸分析
属性不存在多态性(编译和执行都看左边),方法有多态性(编译看左边,执行看右边)。
(2)题目2
多态是编译时行为还是运行时行为?
package com.atguigu06.polymorphism.interview;import java.util.Random;/*** 面试题:多态是编译时行为还是运行时行为?*/
class Animal {protected void eat() {System.out.println("animal eat food");}
}class Cat extends Animal {protected void eat() {System.out.println("cat eat fish");}
}class Dog extends Animal {public void eat() {System.out.println("Dog eat bone");}
}class Sheep extends Animal {public void eat() {System.out.println("Sheep eat grass");}
}public class InterviewTest {public static Animal getInstance(int key) {switch (key) {case 0:return new Cat ();case 1:return new Dog ();default:return new Sheep ();}}public static void main(String[] args) {int key = new Random().nextInt(3); //0,1,2System.out.println(key);Animal animal = getInstance(key);animal.eat();}
}
👻执行结果
🤸分析
多态表现得特性,泛泛来说就是多种形态,在Java里面目前来说,就是子类对象的多态性。
比如这个例子中,看到的是一个一个的Person类,但是实际上传的值是一个一个的子类对象(多态性)。
现在的问题是,这种多态性,是在编译的时候确定的,还是在运行时才确定下来?
其实是运行的时候,因为编译的时候认为的是声明的类型(父类类型),只有运行的时候才知道真正加载进来的是哪一个子类。
此题中,有Animal一个父类,有Cat、Dog、Sheep三个子类,并且重写了方法。
看图: