Java OOP 第三章 多态

Java OOP 多态

一、学习目标
  1. 掌握多态的优势应用场合
  2. 会进行子类和父类之间的类型转换 (向上转型 向下转型)
  3. 掌握 instanceof 运算符的使用 (true/false)
  4. 会使用父类作为方法形参实现多态
  5. 会使用父类作为返回值实现多态
二、多态
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、如何实现多态?

实现多态的三要素

  1. 继承关系的父类和子类(继承是多态的基础)
  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种方式

  1. 父类引用指向子类对象
  2. 父类为形参,子类为实参,以此实现多态

父类作为方法返回值

需求: 假设在土地上选择种植苹果树和梨树玉米等作物,实现根据用户的选择获取指定种类农作物进行种植的功能。

在这里插入图片描述

分析:实现步骤

  1. 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;
    }
}

设计模式:

简单工厂设计模式——>抽象工厂

  1. 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、项目开发步骤

  1. 定义父类 Book
//计算罚金
public double calFines(int borrowingDays) {
   	return 0;
}
  1. 定义子类,重新父类的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;
}
  1. 定义测试类
    • 假设某用户借阅2本成人书籍、2本儿童书籍以及1本文献资料
    • 借阅30天后才归还,计算该用户总共需要缴纳的罚金
十一、图书馆计算罚金完整版
  1. 定义父类 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;
       }
}
  1. 定义子类,重新父类的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;
       }
}
  1. 罚金计算类
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;
    }
}
  1. 测试类
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 + "元");
        }
    }
}
  1. 运行结果

在这里插入图片描述

十二、本章总结

在这里插入图片描述

  • 5
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

众生云海,一念初见

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值