Java OOP 多态
文章目录
一、学习目标
- 掌握多态的优势和应用场合
- 会进行子类和父类之间的类型转换 (向上转型 向下转型)
- 掌握 instanceof 运算符的使用 (true/false)
- 会使用父类作为方法形参实现多态
- 会使用父类作为返回值实现多态
二、多态
1、为什么使用多态
问题: 实现“星沐生态农场”输出土地信息的功能
要求: 1.允许用户选择的作物(苹果树或玉米)2.每一块土地不能进行重复种植,如果出现多次种植,则显示提示信息。
分析:
假设每块土地上只能种植一种作物,如果土地被占用,则给出相应提示。例如:该土地被占用,你可以选择其他土地种植!
在实现种植苹果树和种植玉米功能的基础上,新增Land类,添加记录土地是否空闲的变量idle,新增种植作物的方法plant()。编写种植苹果树的方法,编写种植玉米的方法,新增Game类,控制执行过程。
类图:
- Land 土地类的关键代码:
package com.aiden.crop; /* *土地类 */ public class Land { private boolean idle = true;//是否为空闲状态,默认为 空闲\ /** * 收获作物,父类作为参数类型 **/ public void harvestCrop(Crop crop) { if (!idle) { crop.harvest(); } else { System.out.println("您尚未种植任何农作物!"); } } /* *种植苹果 */ public void plant(AppleTree appleTree) { if (!idle) { System.out.println("土地被占用,目前无法种植新的作物"); } else { this.idle = false;//标识被种下了农作物 appleTree.print(); } } /* *种植玉米 */ public void plant(Corn corn) { if (!idle) { System.out.println("土地被占用,目前无法种植新的作物"); } else { this.idle = false; corn.print(); } } public boolean isIdle() { return idle; } public void setIdle(boolean idle) { this.idle = idle; } }
- Game类的关键代码
public static void main(String [] args){ Scanner input=new Scanner(System.in); System.out.println("请选择:1.播种作物 2.查看作物生长状态 3.收获果实 4.退出"); switch(input.nextInt()) { case 1: System.out.print("请选择您要种植的作物:1.苹果树 2.玉米(待实现)");//选择作物 type = input.nextInt(); switch (type) { case 1: // 省略选择苹果品种的代码 AppleTree appleTree = new AppleTree(brand); land.plant( appleTree ); break; case 2: // 省略玉米的实现代码 Corn corn = new Corn(); corn.harvestCost = harvestCost; land.plant( corn ); break; default: System.out.println("您的选择有误!"); break; } break; // 省略代码 } }
如上代码为未使用父类作为方法形参时候的代码,如果农场允许种植其他作物(如Pear、Cherry、Tomato… ),怎么办?
难不成这样写?
//1.添加Crop类的子类Pear、Cherry、Tomato public class Apple extends Crop{…} public class Corn extends Crop{…} public class Pear extends Crop{…} public class Cherry extends Crop{…} public class Tomato extends Crop{…}
public class Land{ //2.添加重载的plant()方法 public void plant(AppleTree appleTree){…} public void plant(Corn corn){…} //省略代码…… public void plant(Pear pear){…} public void plant(Cherry cherry){…} public void plant(Tomato tomato){…} }
结论
- 如上代码看出程序设计需要频繁修改代码
- 代码可扩展性可维护性差
- 参数都是Crop类的子类
解决方案:
- 使用多态优化设计
2、什么是多态?
- 生活中的多态
- 不同类型的打印机打印效果不同
- 程序中的多态(父类引用指向子类对象 )
- 同一个引用类型,使用不同的实例而执行不同操作,同一种操作,由于条件不同,产生的结果也不同。
3、如何实现多态?
实现多态的三要素
- 继承关系的父类和子类(继承是多态的基础)
- 子类重写父类方法
- 父类的引用指向子类的对象 Crop crop=new Corn(); crop=new AppleTree();
多态的类型
- 向上转型——子类到父类的转换:自动类型转换
- 向下转型——父类到子类的转换:强制类型转换
三、向上转型
将一个父类的引用指向一个子类对象称为向上转型
语法:
<父类型 > < 引用变量名 > = new < 子类型 >(); 父类 变量名 =new 子类()
示例:
Crop crop = new AppleTree("富士"); //自动类型转换 crop.print(); //调用AppleTree类重写的print()方法
- 系统会自动进行类型转换
- 通过父类引用变量调用的方法是子类覆盖或继承的子类方法,不是父类的方法
- 通过父类引用变量无法调用子类特有的方法
四、父类作为形参
使用多态,优化Land类
Land类中,使用父类Crop作为plant()方法的形参
//土地类 public class Land { private boolean idle = true; //默认为“空闲” // 种植作物,Crop父类作为形参 public void plant( Crop crop ) { if (!idle) { System.out.println("该土地被占用,您可以选择其他土地种植" + appleTree.getName() + "。"); } else { this.idle = false; crop.print(); } } // 省略其他方法 }
Test类中,测试代码
public class Test { public static void main(String[] args){ //土地1种植苹果 Land land1 = new Land(); Crop corp1 = new AppleTree("富士"); land1.plant( corp1 );//同一种操作方式,不同的操作对象 //土地2种植玉米 Land land2 = new Land(); Crop corp2 = new Corn(200); land2.plant( corp2 );//同一种操作方式,不同的操作对象 land2.plant( corp1 );//land2上种植苹果时,出现重复种植提示 } }
扩展种植梨树
示例
“星沐生态农场”中,种植新作物——梨树
名称 品种 生长期时长 采摘期时长 果实数量 梨树 皇冠梨、香水梨 12天 4天 120 分析
“星沐生态农场”中,种植新作物——梨树
创建Pear类,继承自Crop类
package com.aiden.crop; /** * @author Aiden */ public class Pear extends Crop { private String brand; //品种 public String getBrand() { return brand; } public void setBrand(String brand) { this.brand = brand; } public Pear() { } /** * 类的无参构造方法 */ public Pear(String brand) { super("梨树", 12, 4, 120); //访问父类的构造方法 this.brand = brand; super.setStatus(Constants.GROW); } /* 重写父类的print方法 */ public void printGrowReport(int day) { System.out.println("您种植了" + super.getName() + ",品种:" + this.brand); super.printGrowReport(day); //调用父类printGrowReport方法 } }
Test类,添加实现种植Pear对象代码
public static void main(String[] args) { // 省略种植苹果代码 // 省略种植玉米代码 //种植梨 Land land3 = new Land(); Crop crop3 = new Pear("香水梨"); land3.plant(crop3); }
如上代码实现种植Pear,通过使用继承和多态机制,扩展变得非常容易,无需修改Land类
五、多态的使用案例
示例:
选择查看作物生长状态和收获果实功能
- 两个功能都会判断是否种植作物
- 查看作物生长状态,根据生长期、采摘期、已死亡状态输出不同信息
- 进入采摘期且未收获,则输出收获果实数量,否则输出“抱歉!目前没有果实可收获!”AppleTree类和Corn类中,已有的printGrowReport()方法和harvest()方法可以分别查看其的生长状态和收获果实
分析
- Land类中,需要添加checkGrowReport()和harvestCrop()方法
- 使用父类Crop对象作为方法参数
- 在方法体中调用各自子类重写的方法
- Game类,调用方法实现功能
示例:通过父类引用,调用子类方法从Land类的关键代码中,不难看出我们定义的plant方法、checkAppleGrow方法、harvestCrop方法都是将农作物作这个父类作为形参,而在实际调用时传入子类,以此实现多态。
public class Land{ //土地状态 private boolean idle = true; //默认为“空闲”状态 //用于传递数据,保证数据一致性 public Crop crop; //默认值null //种植作物,父类作为参数类型 public void plant(Crop crop){ if(!idle) { System.out.println("土地被占用,目前无法种植新的作物!"); }else { this.idle = false; crop.print();//通过父类引用,调用子类方法 } } // 查看农作物生长状态,父类作为参数类型 public void checkAppleGrow(Crop crop, int day) { if(!idle) { crop.printGrowReport(day); }else{ System.out.println("您尚未种植任何农作物!"); } } // 收获作物,父类作为参数类型 public void harvestCrop(Crop crop) { if(!idle) { crop.harvest(); } else { System.out.println("您尚未种植任何农作物!"); } } }
测试类的关键代码
//1.定义全局变量 Land land = new Land(); //创建土地类 Crop crop = null; //各种作物的父类对象 //2.播种苹果树 String brand = "富士"; //品种 crop = new AppleTree(brand);//实例化农作物对象。多态运用:父类引用指向子类对象 land.plant(crop);//种植。多态运用:父类作为方法参数 //3.播种玉米 double harvestCost = 80;//收割费用 crop = new Corn(harvestCost);//调用构造函数,实现实例化玉米 land.plant(crop); //种植。多态运用:父类作为方法参数 //4.查看作物生长状态 land.checkAppleGrow(crop,11);//多态运用:父类作为方法参数 //5.收获果实 land.harvestCrop(crop); //多态运用:父类作为方法参数
注意: 为了保证数据的一致性,要将父类对象定义为类的全局变量,使用父类对象作为方法形参时,通过父类引用变量,可调用子类中重写的方法
总结: 实现多态的2种方式
- 父类引用指向子类对象
- 父类为形参,子类为实参,以此实现多态
父类作为方法返回值
需求: 假设在土地上选择种植苹果树和梨树玉米等作物,实现根据用户的选择获取指定种类农作物进行种植的功能。
分析:实现步骤
- CropFactory类中,定义getCrop(String type)方法
package com.aiden.crop.factory; import com.aiden.crop.AppleTree; import com.aiden.crop.Corn; import com.aiden.crop.Crop; import com.aiden.crop.Pear; /** * @author Aiden */ public class CropFactory { /** * 种植农作物的工厂方法 * * @param type 类型 * @return */ public static Crop getCrop(int type) { Crop crop = null; switch (type) { case 1: crop = new AppleTree("富士"); break; case 2: crop = new Corn(50); break; case 3: crop = new Pear("黑凤梨"); break; default: System.out.println("您的输入有误!!!"); break; } return crop; } }
设计模式:
简单工厂设计模式——>抽象工厂
- main()方法中
调用getCrop(String type)方法,根据用户选择获取树苗,并返回树苗对象
调用plant()方法,种植树苗
package com.aiden.crop; import com.aiden.crop.factory.CropFactory; import java.util.Scanner; /** * @author Aiden */ public class Test03 { public static void main(String[] args) { System.out.println("欢迎来到星沐农场"); System.out.println("请选择要种植的农作物(1.苹果树 2.玉米 3.梨树)"); Scanner input = new Scanner(System.in); int type = input.nextInt(); //将父类作为方法的返回值,以此实现多态 Crop crop = CropFactory.getCrop(type); if (crop != null) { System.out.println("成功获取树苗!"); Land land = new Land(); land.plant(crop); } } }
六、向下转型
问题:
- 星沐生态农场”种植的作物,在生长以及采摘过程中,由于特殊情况,需要处理一些变化,要求:
- 使用嫁接技术,将已种植的其他品种苹果转换为苹果新品种“红粉佳人”,后续也可以完成其他品种的嫁接
- 如果玉米等作物赶上丰收年,需申请换用联合收割机,且变更收割费用为100元
分析:
AppleTree类
添加grafting(String newBread)方法,实现果树的嫁接功能
Corn类
添加reHarvester(double cost)方法,实现收割机变更功能
测试类
- 多态形式创建对象并调用
示例:
AppleTree苹果树类嫁接实现的关键代码:
//嫁接新品种 public void grafting(String newBrand) { if(this.brand == newBrand) { System.out.println("同品种果树无需嫁接。"); } else { this.brand = newBrand; System.out.println("经过嫁接,"+super.getName() + "的品种变为"+this.brand+"。"); } }
Corn玉米类关键代码:
//更换收割机 public void reHarvester(double cost) { if(this.harvestCost == cost) { System.out.println("更换收割机后,费用不变!"); } else { this.harvestCost = cost; System.out.println("更换收割机后,费用变为"+this.harvestCost+"。"); } }
Test类关键代码:
问题: 如何才能使父类对象访问其子类特有的属性和方法呢
方案: 向下转型
- 将一个指向子类对象的父类引用赋给一个子类的引用,即将父类类型转换为子类类型,称为向下转型
向下转型必须进行强制类型转换- 将父类类型转换为它的某个子类类型后,才能调用其子类特有的属性
语法:
<< 子类型 > < 引用变量名 > = (< 子类型 >)< 父类型的引用变量 >;
示例:
AppleTree appleTree =(AppleTree) crop; //将crop转换为 AppleTree 类型 appleTree.grafting("粉红佳人"); //调用苹果树的嫁接方法
七、instanceof运算符
- 从父类到子类的向下转型,可以实现多态,即执行不同子类中定义的特定方法
- 但事实上,父类对象的引用可能指向某一个子类对象
- 如果在向下转型时,不是转换为真实的子类类型,就会出现转换异常
示例:
package com.aiden.crop; /** * @author Aiden */ public class Test02 { public static void main(String[] args) { //创建一个农作物的数组 Crop[] crops = new Crop[2]; Crop crop = null; //在数组中初始化了两种农作物 crops[0] = new AppleTree("富士");//苹果 crops[1] = new Corn(50);//玉米 //循环输出农作的个性化信息 for (Crop model : crops) { model.print(); AppleTree appleTree = (AppleTree) model; appleTree.grafting("蛇果"); } } }
JVM检测到两个类型间不兼容时,引发的运行时异常ClassCastException
问题:如何通过代码避免这种异常的发生?
- 使用instanceof运算符
用于判断一个对象是否属于一个类或者实现了一个接口
语法:
对象 instanceof 类 | 接口 //运行结果为true或false
作用
- 避免引发类型转换异常,提高代码的健壮性
应用场合
- 向下转型之前,先使用instanceof进行类型判断
package com.aiden.crop; /** * @author Aiden */ public class Test02 { public static void main(String[] args) { //创建一个农作物的数组 Crop[] crops = new Crop[2]; Crop crop = null; //在数组中初始化了两种农作物 crops[0] = new AppleTree("富士");//苹果 crops[1] = new Corn(50);//玉米 //crops[0].grafting("红粉佳人"); //解决问题 for (Crop model : crops) { model.print(); if (model instanceof AppleTree) { AppleTree appleTree = (AppleTree) model; appleTree.grafting("蛇果"); } else if (model instanceof Corn) { Corn corn = (Corn) model; corn.reHarvester(100); } } } }
注意事项:
对象的类型必须与instanceof参数后所指定的类或接口在继承树上具有上下级关系;否则会出现编译错误
八、多态的优势
九、面向对象编程
面向对象的三大特性:封装、继承、多态
封装是隐藏对象的属性和实现细节
- 将类的成员属性声明为私有的,同时提供公有的方法实现对该成员属性的存取操作
继承是软件可重用性的一种表现
- 新类可以在不增加自身代码的情况下,通过从现有的类中继承其属性和方法充实自身内容
多态是具有表现多种形态的能力的特征
- 在程序设计的术语中,意味着一个特定类型的变量可以引用不同类型的对象,自动地调用引用的对象的方法
- 即根据作用到的不同对象类型,响应不同的操作
十、综合练习
图书馆计算罚金功能
1、需求描述
图书馆为读者提供借阅书籍和文献资料
每位读者可一次借阅多本书籍和文献资料
超时超时未还书的罚款规则
成人书籍
允许借阅的时间是21天,每超时1天,需要缴纳罚金2元
如果超过3天以上,每1天需要缴纳罚金5元
儿童书籍
- 允许借阅的时间是21天,每超时1天,需要缴纳罚金1元
文献资料
允许借阅的时间是14天,每超时1天,需要缴纳罚金5元
如果超过3天以上,每1天需要缴纳罚金10元
使用面向对象编程的多态特性实现计算罚金的功能
2、项目设计
3、项目开发步骤
- 定义父类 Book
//计算罚金 public double calFines(int borrowingDays) { return 0; }
定义子类,重新父类的calFines()方法
成人书籍类(AdultBook)
儿童书籍类(KidBook)
文献资料类(Literature)//成人书籍类(AdultBook) @Override public double calFines(int borrowingDays) { int delay = borrowingDays-this.getBorrowingPeriod(); double fines; //罚金 if(delay<=3) { fines = delay * 2; } else { fines = 3 * 2 + (delay-3) * 5; } return fines; }
//儿童书籍类(KidBook) @Override public double calFines(int borrowingDays) { return (borrowingDays-this.getBorrowingPeriod()) * 1; }
//文献资料类(Literature) @Override public double calFines(int borrowingDays) { int delay = borrowingDays-this.getBorrowingPeriod(); double fines; if(delay<=3) fines = delay * 5; else fines = 3 * 5 + (delay-3) * 10; return fines; }
- 定义测试类
- 假设某用户借阅2本成人书籍、2本儿童书籍以及1本文献资料
- 借阅30天后才归还,计算该用户总共需要缴纳的罚金
十一、图书馆计算罚金完整版
- 定义父类 Book
package com.aiden.book; /** * 图书 */ public class Book { private String name; //名称 private int borrowingPeriod;//借阅期限 public Book() { } public Book(String name, int borrowPeriod) { this.name = name; this.borrowingPeriod = borrowPeriod; } public double calFines(int borrowingDays) { return 0; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getBorrowingPeriod() { return borrowingPeriod; } public void setBorrowingPeriod(int borrowingPeriod) { this.borrowingPeriod = borrowingPeriod; } }
定义子类,重新父类的calFines()方法
成人书籍类(AdultBook)
儿童书籍类(KidBook)
文献资料类(Literature)package com.aiden.book; /** * 成人书籍类 */ public class AdultBook extends Book { public AdultBook(String name) { super(name, 21); } @Override public double calFines(int borrowingDays) { int delay = borrowingDays - this.getBorrowingPeriod(); double fines; //罚金 if (delay <= 3) { fines = delay * 2; } else { fines = 3 * 2 + (delay - 3) * 5; } return fines; } }
package com.aiden.book; /** * 儿童书籍类 */ public class KidBook extends Book { public KidBook() { } public KidBook(String name) { super(name, 21); } @Override public double calFines(int borrowingDays) { return (borrowingDays - this.getBorrowingPeriod()) * 1; } }
package com.aiden.book; /** * 文献资料类 */ public class Literature extends Book { public Literature() { } public Literature(String name) { super(name, 14); } @Override public double calFines(int borrowingDays) { int delay = borrowingDays - this.getBorrowingPeriod(); double fines; if (delay <= 3) { fines = delay * 5; } else { fines = 3 * 5 + (delay - 3) * 10; } return fines; } }
- 罚金计算类
package com.aiden.book; public class Customer { public int calTotalFines(Book books[], int borrowingDays){ int totalFines = 0; //总罚金 for(int i = 0; i<books.length;i++){ totalFines += books[i].calFines(borrowingDays); } return totalFines; } }
- 测试类
package com.aiden.book; public class Test { public static void main(String[] args) { //延迟归还的书籍列表 Book[] books = new Book[5]; books[0] = new AdultBook("半小时漫画中国史"); books[1] = new AdultBook("博弈论"); books[2] = new KidBook("法布尔昆虫记"); books[3] = new KidBook("最好的朋友"); books[4] = new Literature("冰雪公主2"); Customer customer = new Customer(); int borrowingDays = 30; //借阅时间 int fines = customer.calTotalFines(books,30); System.out.println("您共归还书籍"+books.length+"本"); for(int i=0;i<books.length;i++){ System.out.println((i+1)+" " + books[i].getName()); } System.out.println("借阅时间:"+borrowingDays+"天"); if(fines >0){ System.out.println("共需交纳罚金:" +fines + "元"); } } }
- 运行结果