目录
1.一个问题引出多态
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 Fish extends Food{
public Fish(String name) {
super(name);
}
}
public class Bone extends Food{
public Bone(String name) {
super(name);
}
}
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 Cat extends Animal {
public Cat(String name) {
super(name);
}
}
public class Dog extends Animal{
public Dog(String name) {
super(name);
}
}
这些类写好了之后,再单写一个Master类
public class Master{
private String name; //主人名称
public Master(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void feed(Dog dog,Bone bone){
System.out.println("主人 "+name+" 给 "+ dog.getName()+" 吃"+bone.getName());
}
}
然后进行测试
public class Poly01 {
public static void main(String[] args) {
Master tom = new Master("汤姆");
Dog dog = new Dog("大黄");
Bone bone = new Bone("大棒骨");
tom.feed(dog,bone);
}
}
那么我们现在又要让主人给小猫喂鱼,又要在Master类里写一个feed的方法,只是参数不一样,也就是重载。
//主人给小狗 喂食 黄花鱼
public void feed(Cat cat,Fish fish){
System.out.println("主人 "+name+" 给 "+ cat.getName()+" 吃"+fish.getName());
}
测试
public class Poly01 {
public static void main(String[] args) {
Master tom = new Master("汤姆");
Dog dog = new Dog("大黄");
Bone bone = new Bone("大棒骨");
tom.feed(dog,bone);
Cat cat = new Cat("小花猫");
Fish fish = new Fish("黄花鱼");
System.out.println("==========");
tom.feed(cat,fish);
}
}
注:
从功能实现上来说,效果确实是出来了,但是我们这样写下去会出现一种后果
如果随着代码的扩展,将来主人可能还会养猪、兔子、鸟、等各种动物,这样的话不同的动物吃的东西也不一样,猪喜欢吃米饭,鱼喜欢吃虫子,
这样动物很多,食物也很多,就会造成feed方法扩展越来越多,不利于管理和维护。
这里就引出多态
2.多态的基本介绍
方法或对象具有多种形态。是面向对象的第三大特征,多态是建立在封装和继承基础之上的。
public class PloyMethod {
public static void main(String[] args) {
}
}
class B { //父类
public void say() {
System.out.println("B say() 方法被调用...");
}
}
class A extends B { //子类
public int sum(int n1, int n2) { //和下面的sum 构成重载
return n1 + n2;
}
public int sum(int n1, int n2, int n3) {
return n1 + n2 + n3;
}
public void say(){
System.out.println("A say()方法被调用...");
}
}
这里有父类和子类,子类里有2个sum构成重载,那么这是如何构成多态的呢?
此时我们在main方法里这样写:
public static void main(String[] args) {
// 方法重载体现多态
A a = new A();
// 这里我们传入不同的参数,就会调用不同的sum方法,就体现出多态
System.out.println(a.sum(10,20));
System.out.println(a.sum(10,20,30));
}
我们通过不同的参数个数去调用sum方法,就会去调用不同的方法,因此对sum方法来说,就是多种状态的体现。
重载体现出了多态,那么方法的重写又是如何体现出多态?
同样是刚刚的例子,我们可以看到父类由一个say方法,子类也有一个say方法
这里我们在main方法中new一个B对象,调用a的say方法和b的say方法。
//方法重写体现多态
B b = new B();
a.say();
b.say();
这里也会体现出多态。
注:
重载和重写体现的多态是比较好理解的,真正难理解的是对象的多态,因为对象多态里面很复杂,对象里面会有编译类型和运行类型的概念。
3.对象的多态
(1)一个对象的编译类型和运行类型可以不一致
这里Dog类是Animal的子类,这里可以把一个子类对象赋给一个父类的对象引用,也就是说我能用一个父类的引用指向子类对象,现在这个animal准确的讲是对象的引用,并不是对象,而是对象的引用,而后面new的才是真正的对象,此时animal的编译类型是Animal,运行类型是Dog
(2)编译类型在定义对象时,就确定了,不能改变
Animal animal = new Dog();
我们可以看到以上代码,它一但定义好了,也就是animal,它的编译类型就是Animal,不可以改变,已经确定下来了
(3)运行类型是可以改变的
animal = new Cat();
这个时候animal的运行类型变成了Cat,编译类型仍然是Animal
(4)编译类型看定义时 = 号的左边,运行类型看 = 号的右边
代码演示
public class Animal { public void cry(){ System.out.println("Animal 动物在叫..."); } }
public class Cat extends Animal{ public void cry() { System.out.println("Cat cry() 小猫喵喵叫..."); } }
public class Dog extends Animal{ public void cry() { System.out.println("Dog cry() 小狗汪汪叫..."); } }
public class PolyObject { public static void main(String[] args) { //体验对象多态的特点 //animal 编译类型已经确定 --> Animal,运行类型是 Dog Animal animal = new Dog(); //因为运行时,也就是执行到animal.cry()这一行代码的时候,animal的运行类型是Dog,因此这个cry就会找运行类型的cry,也就是dog里的cry animal.cry(); //小狗汪汪叫 } }
运行时,也就是执行到animal.cry()这一行代码的时候,animal的运行类型是Dog,因此这个cry就会找运行类型的cry,也就是dog里的cry
此时我们再把运行类型改成Cat
animal = new Cat(); animal.cry(); //小猫喵喵叫
编译类型在定义的时候就已经确定了,这是无法改变的,一直是animal,但是运行类型是Cat。
一但我们将右边new为Cat的时候,animal的指向就发生变化了,这时候是指向Cat,也就是说,在运行的时候主要看的是堆里面的,既然运行类型变成猫了,那么在执行执行的时候又以猫为主,运行的时候全部看的是堆里面的真正对象。
4.多态快速入门案例
回顾我们一开始多态问题的引出,此时我们来分析一下,这确实要写很多个feed方法
//主人给小狗 喂食 骨头
public void feed(Dog dog,Bone bone){
System.out.println("主人 "+name+" 给 "+ dog.getName()+" 吃"+bone.getName());
}
//主人给小狗 喂食 黄花鱼
public void feed(Cat cat,Fish fish){
System.out.println("主人 "+name+" 给 "+ cat.getName()+" 吃"+fish.getName());
}
我们可以对代码进行简化
//使用多态机制,可以统一的管理主人喂食的问题
public void feed(Animal animal,Food food){
System.out.println("主人 "+name+" 给 "+ animal.getName()+" 吃"+food.getName());
}
里面的参数类型可以用父类来接收,父类的引用可以指向它的子类,所以里面直接写个Animal就好了,同样食物也是。
由于animal的编译类型是Animal类型,它可以指向(接收) Animal子类的对象。
同样的道理,Food的编译类型是food,它可以指向(接收)Food的子类对象
这时候去测试:
public class Poly01 {
public static void main(String[] args) {
Master tom = new Master("汤姆");
Dog dog = new Dog("大黄");
Bone bone = new Bone("大棒骨");
tom.feed(dog,bone);
Cat cat = new Cat("小花猫");
Fish fish = new Fish("黄花鱼");
System.out.println("==========");
tom.feed(cat,fish);
}
}
多态带来的好处不止这个。
假如我们此时让主人喂食给一只猪,我们来编写一下代码
public class Pig extends Animal{
public Pig(String name) {
super(name);
}
}
public class Rice extends Food{
public Rice(String name) {
super(name);
}
}
此时主人要给小猪喂食物,我们会发现,这个feed方法不用动,因为它已经可以接收所有动物的子类对象,也可以接收所有食物的子类对象
//使用多态机制,可以统一的管理主人喂食的问题
public void feed(Animal animal,Food food){
System.out.println("主人 "+name+" 给 "+ animal.getName()+" 吃"+food.getName());
}
直接测试:
public class Poly01 {
public static void main(String[] args) {
Master tom = new Master("汤姆");
//添加给小猪喂米饭
Pig pig = new Pig("小花猪");
Rice rice = new Rice("米饭");
tom.feed(pig,rice);
}
}
5.多态注意事项和细节问题(向上转型和向下转型)
多态的前提是:两个对象(类)存在继承关系
多态的向上转型:
那么为了举例子,我们重新建一个Animal类和Cat类
public class Animal { String name = "动物"; int age=10; public void sleep(){ System.out.println("睡"); } public void run(){ System.out.println("跑"); } public void eat(){ System.out.println("吃"); } public void show(){ System.out.println("hello,你好"); } }
public class Cat extends Animal{ public void eat(){ //方法重写 System.out.println("猫吃鱼"); } public void catchMouse(){ //Cat特有的方法 System.out.println("猫抓老鼠"); } }
public class PolyDetail { public static void main(String[] args) { //(向上转型):父类的引用指向子类的对象 //语法:父类类型引用名 = new 子类类型(); Animal animal = new Cat(); Object object = new Cat(); System.out.println("OK"); } }
将Animal和Object作为引用指向子类的对象这样都是可以的,虽然Object不是Cat的直接父类,但是是Animal的父类。
可以调用父类中的所有成员(需遵守访问权限)
但是就是不能调用子类中的catchMouse()方法,如果需要调用的话,则需要强转
也就是说不能调用子类的特有成员,这个例子里catchMouse()是Cat类独有的方法,在编译阶段,能调用哪些成员,是由编译类型来决定的。
此时我们调用4个方法:
public class PolyDetail { public static void main(String[] args) { //(向上转型):父类的引用指向子类的对象 //语法:父类类型引用名 = new 子类类型(); Animal animal = new Cat(); animal.eat(); animal.run(); animal.show(); animal.sleep(); } }
那么这样的输出结果会是什么呢??
这里我们要清楚,在编译阶段,它指定编译类型,也就是javac,但是一但运行起来以后,其实运行的时候就是交给java虚拟机来做了,它在运行程序的时候,不管你编译类型,只看你运行类型,而当我们执行animal.eat()这一行的时候,会先去Cat类里面找eat方法,如果有的话直接找Cat里的eat方法,那么这里直接输出"猫吃鱼"。
注:
调用方法的时候,按照从子类(运行类型)开始查找方法,然后调用,规则和前面的方法调用规则一致
向下转型:
public class PolyDetail {
public static void main(String[] args) {
//(向上转型):父类的引用指向子类的对象
//语法:父类类型引用名 = new 子类类型();
Animal animal = new Cat();
animal.eat();
animal.run();
animal.show();
animal.sleep();
}
}
向上转型的调用方法规则中有:可以调用父类中的所有成员,但是不能调用子类的特有成员。
那么我们就是想调用子类的特有成员该怎么做呢?
这里就出现了多态的向下转型
//多态的向下转型 //语法:子类类型 引用名 = (子类类型) 父类引用. Cat cat = (Cat)animal; cat.catchMouse();
这个时候catchMouse(),就可以调用了,此时cat的编译类型是Cat,运行类型还是Cat,因为强转为Cat了
那么此时我们再来看一段代码,我们写了一个Dog类,让Dog类也去继承这个Animal
Dog dog = (Dog)animal;
一开始的Cat cat = (Cat)animal这段代码中的animal是因为指向的是Cat对象,那么强转为Cat,这时又有一个cat也是指向到Cat对象。
但是我们想强转为Dog,我们这么想一想,animal指向的是Cat对象,总不可能让一个狗指向猫吧?
6.属性重写问题
属性没有重写之说,属性的值看编译类型。
public class PolyDetail2 {
public static void main(String[] args) {
Base base = new Sub();
System.out.println(base.count); //10
Sub sub = new Sub();
System.out.println(sub.count); //20
}
}
class Base{ //父类
int count=10; //属性
}
class Sub extends Base{ //子类
int count=20; //属性
}
7.instanceOf比较操作符
instanceOf 比较操作符,用于判断对象的运行类型是否为 XX 类型或 XX 类型的子类型
public class PolyDetail3 {
public static void main(String[] args) {
BB bb = new BB(); //true
System.out.println(bb instanceof BB); //true
}
}
class AA{} //父类
class BB extends AA{} //子类
那么instanceOf判断的是编译类型还是判断的运行类型?
//aa 编译类型是 AA,运行类型是BB
AA aa = new BB();
System.out.println(aa instanceof AA); //true
System.out.println(aa instanceof BB); //true
那么得出结论:判断对象的运行类型是否为 XX 类型或 XX 类型的子类型
8.练习题
9.java的动态绑定机制(非常非常重要)
Java 重要特性: 动态绑定机制
我们先来看个问题:
public class DynamicBinding {
public static void main(String[] args) {
A a = new B();
System.out.println(a.sum()); //40
System.out.println(a.sum1()); //30
}
}
class A {
public int i = 10;
public int sum() {
return getI() + 10;
}
public int sum1() {
return i + 10;
}
public int getI() {
return i;
}
}
class B extends A {
public int i = 20;
public int sum() {
return i + 20;
}
public int getI() {
return i;
}
public int sum1() {
return i + 10;
}
}
这里毫无疑问,输出40和30
但是我们此时,将子类的sum方法注释掉
此时由于子类没有sum()方法,则去找父类的,但是父类里面又有一个getI()方法,此时子类和父类都有这个getI()方法,那么调用的到底是父类的getI还是子类的?这里就会出现一个动态绑定机制在里面了。
以上面的例子为例,a.sum()这一步,执行到父类的sum()方法时,我们会发现:
由于a.sum()中的a是运行类型是B,此时去调用子类的getI()
而到了子类的getI()的时候,里面的return i;i是个属性,那么此时这个i就是子类里的i,也就是20;所以在父类里的sum方法,return的getI()就是20+10
而最后的运行结果则是从40变为30。
那么现在我们继续把子类的sum1()注释掉
这里调用方法,同样跟该对象的内存地址绑定,但是B类的sum1被我们注释掉,这里就是没有,但是这里继承机制发生了,去父类去找,父类由sum1方法,此时return i+10;属性是没有动态绑定机制的,也就是哪里声明哪里使用,这个i就是当前类,也就是10,那么最后输出20。
10.多态数组
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
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 say(){
return name + "\t" + age;
}
}
public class Student extends Person{
private double score;
public Student(String name, int age, double score) {
super(name, age);
this.score = score;
}
public double getScore() {
return score;
}
public void setScore(double score) {
this.score = score;
}
//重写父类say
@Override
public String say() {
return super.say() + " score="+score;
}
}
public class Teacher extends Person{
private double salary;
public Teacher(String name, int age, double salary) {
super(name, age);
this.salary = salary;
}
public double getSalary() {
return salary;
}
public void setSalary(double salary) {
this.salary = salary;
}
//重写父类say方法
@Override
public String say() {
return super.say() + " salary="+salary;
}
}
public class PloyArray {
public static void main(String[] args) {
/**
* 应用实例:现有一个继承结构如下:要求创建 1 个 Person 对象
* 2 个 Student 对象和 2 个 Teacher 对象, 统一放在数组中,并调用每个对象 say 方法
*/
Person[] persons = new Person[5];
persons[0] = new Person("jack",20);
persons[1] = new Student("jack",18,100);
persons[2] = new Student("smith",19,30.1);
persons[3] = new Teacher("scott",30,20000);
persons[4] = new Teacher("king",50,25000);
//循环遍历多态数组,调用say方法
for (int i = 0; i < persons.length; i++) {
//person[i]的编译类型是Person,运行类型是根据实际情况由JVM来判断
System.out.println(persons[i].say());//动态绑定机制
}
}
}
既然要统一放在一个数组里,肯定得用父类类型来接收,因为父类的引用可以指向子类的对象,所以可以这样创建对象数组:Person[] persons = new Person[5];那这里面可以放Person和Person的子类,最后循环遍历,就会跟着运行类型进行输入say方法
此时我们对这个问题进行强化一下:
那么我们添加一下方法
//学生特有方法
public void sutdy(){
System.out.println("学生 "+getName()+" 正在学java课程");
}
//老师特有方法
public void teach(){
System.out.println("老师 "+getName()+" 正在讲java课程");
}
但是我们在循环里面调用,是没办法去调用的
因为person[i]的编译类型是Person类,而Person类是没有这个方法的,那么我们可以在循环里面进行判断,判断运行类型是不是学生,instanceof判断运行类型是不是Student,或者是Student的子类。
public class PloyArray {
public static void main(String[] args) {
/**
* 应用实例:现有一个继承结构如下:要求创建 1 个 Person 对象
* 2 个 Student 对象和 2 个 Teacher 对象, 统一放在数组中,并调用每个对象 say 方法
*/
Person[] persons = new Person[5];
persons[0] = new Person("jack", 20);
persons[1] = new Student("mary", 18, 100);
persons[2] = new Student("smith", 19, 30.1);
persons[3] = new Teacher("scott", 30, 20000);
persons[4] = new Teacher("king", 50, 25000);
//循环遍历多态数组,调用say方法
for (int i = 0; i < persons.length; i++) {
//person[i]的编译类型是Person,运行类型是根据实际情况由JVM来判断
System.out.println(persons[i].say());//动态绑定机制
if (persons[i] instanceof Student) { //判断person[i]的运行类型是不是Student
Student student = (Student) persons[i]; //向下转型
student.study();
//这里可以使用一条语句写:((Student)person[i]).study();
} else if (persons[i] instanceof Teacher) {
((Teacher) persons[i]).teach();
} else if (persons[i] instanceof Person){
}else{
System.out.println("你的类型有误,请自行判断...");
}
}
}
}
这里要巧妙的运用instanceof判断运行类型,然后对其进行向下转型的处理,如果运行类型是Person则不进行处理
11.多态参数
其实在这里就已经体现出了参数的多态,cat和pig是Animal的子类,fish和rice是Food的子类。
public class Employee {
private String name;
private double salary;
public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}
//得到年工资的方法
public double getAnnual(){
return 12 * salary;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getSalary() {
return salary;
}
public void setSalary(double salary) {
this.salary = salary;
}
}
public class Manager extends Employee {
private double bonus;
public Manager(String name, double salary, double bonus) {
super(name, salary);
this.bonus = bonus;
}
public void manage() {
System.out.println("经理 " + getName() + "is managing");
}
//重写获取年薪方法
@Override
public double getAnnual() {
return super.getAnnual() + bonus;
}
public double getBonus() {
return bonus;
}
public void setBonus(double bonus) {
this.bonus = bonus;
}
}
public class Worker extends Employee {
public Worker(String name, double salary) {
super(name, salary);
}
public void work() {
System.out.println("普通员工 " + getName() + " is working");
}
@Override
public double getAnnual() { //因为普通员工没有其他收入,则直接调用父类的方法。
return super.getAnnual();
}
}
那么我们就开始写测试类
public class PloyParameter {
public static void main(String[] args) {
Worker tom = new Worker("tom", 2500);
Manager milan = new Manager("milan", 5000, 200000);
PloyParameter ployParameter = new PloyParameter();
ployParameter.showEmpAnnual(tom);
ployParameter.showEmpAnnual(milan);
ployParameter.testWork(tom);
ployParameter.testWork(milan);
}
/**
* showEmpAnnual(Employee employee)
* 实现获取任何员工对象的年工资,并在 main 方法中调用该方法 [employee.getAnnual()]
*/
public void showEmpAnnual(Employee employee) {
System.out.println(employee.getAnnual());
}
}
此时:ployParameter.showEmpAnnual(tom);这里我们传进去的tom,它的编译类型是Worker,运行类型也是Worker,那么传进去肯定会去找worker。
那么我们传milan进去,就会给Manager绑定,因为有动态绑定机制。
那么我们再写第二个方法:
此时们将形参再写Employee,然后用instanceof判断运行类型,如果是Worker,直接调用e.work(),这里编译器会自动转换--->向下转型。
那么继续用istanceof判断是不是Manager。
public class PloyParameter {
public static void main(String[] args) {
Worker tom = new Worker("tom", 2500);
Manager milan = new Manager("milan", 5000, 200000);
PloyParameter ployParameter = new PloyParameter();
ployParameter.showEmpAnnual(tom);
ployParameter.showEmpAnnual(milan);
ployParameter.testWork(tom);
ployParameter.testWork(milan);
}
/**
* showEmpAnnual(Employee employee)
* 实现获取任何员工对象的年工资,并在 main 方法中调用该方法 [employee.getAnnual()]
*/
public void showEmpAnnual(Employee employee) {
System.out.println(employee.getAnnual());
}
//添加一个方法,testWork,如果是普通员工,则调用 work 方法,如果是经理,则调用 manage 方法
public void testWork(Employee e){
if (e instanceof Worker){
((Worker) e).work(); //有一个向下转型的操作
}else if (e instanceof Manager){
((Manager) e).manage();
}else{
}
}
}