多态:面向对象编程的重要特性
摘要:本文全面阐述了面向对象编程中多态这一关键特性,从多态的形式、使用场景、定义及前提条件出发,深入分析其运行特点、弊端,详细介绍引用类型转换相关知识,并通过综合练习强化对多态的理解与应用,为深入掌握面向对象编程提供了有力支撑。
关键词:多态;继承;方法重写;引用类型转换
参考资料::黑马程序员
一、引言
多态作为面向对象的第三大特性,在继承或实现关系中发挥重要作用,极大地增强了程序的灵活性与扩展性。本文将系统地探讨多态的各个方面。
二、多态的形式
多态在继承或实现关系中得以体现,其常见格式为:
父类类型 变量名 = new 子类/实现类构造器;
变量名.方法名();
多态存在的前提是具有继承关系,子类对象能够赋值给父类类型变量。例如,Animal
为动物类型,Cat
为猫类型,Cat
继承自Animal
,那么Cat
对象可赋值给Animal
类型变量。
三、多态的使用场景
在未使用多态时,如register
方法若仅能接收学生对象,对于Teacher
和Administrator
对象则需定义不同的register
方法。
public void register(Student s){}
而借助多态,register
方法的形参可定义为共同的父类Person
。
当方法形参为类时,可传递该类的所有子类对象;当形参为接口时,可传递该接口的所有实现类对象(后续学习),并且多态能够依据传递的不同对象调用不同类中的方法。
以代码示例说明:
// 父类
public class Person {private String name;private int age;// 空参构造public Person() {}// 带全部参数的构造public Person(String name, int age) {this.name = name;this.age = age;}// get和set方法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 void show() {System.out.println(name + ", " + age);}
}// 子类1
public class Administrator extends Person {@Overridepublic void show() {System.out.println("管理员的信息为:" + getName() + ", " + getAge());}
}// 子类2
public class Student extends Person {@Overridepublic void show() {System.out.println("学生的信息为:" + getName() + ", " + getAge());}
}// 子类3
public class Teacher extends Person {@Overridepublic void show() {System.out.println("老师的信息为:" + getName() + ", " + getAge());}
}// 测试类
public class Test {public static void main(String[] args) {// 创建三个对象,并调用register方法Student s = new Student();s.setName("张三");s.setAge(18);Teacher t = new Teacher();t.setName("王建国");t.setAge(30);Administrator admin = new Administrator();admin.setName("管理员");admin.setAge(35);register(s);register(t);register(admin);}// 这个方法既能接收老师,又能接收学生,还能接收管理员// 只能把参数写成这三个类型的父类public static void register(Person p) {p.show();}
}
PS:@Override
注解最主要的作用是帮助编译器进行检查,确保被注解的方法确实重写了父类中的方法。如果一个方法使用了 @Override
注解,但父类中不存在对应的方法,编译器会报错。这样可以避免因方法签名错误而导致的意外行为
四、多态的定义和前提
多态指同一行为具有多个不同表现形式。例如,Cat
和Dog
同为动物,都有“吃”这一行为,但具体表现不同。
其前提条件如下:
- 继承或实现关系:类之间需存在继承或实现关系。
- 方法的重写:子类对父类方法进行重写,若无重写,多态意义不大。
- 父类引用指向子类对象:即采用
父类类型 变量名 = new 子类类型();
的格式,其中父类类型指子类对象继承的父类类型或实现的父接口类型。
五、多态的运行特点
- 调用成员变量时:编译阶段依据左边父类类型判断是否存在该成员变量,运行阶段也使用左边父类类型的成员变量值。
- 调用成员方法时:编译阶段检查左边父类是否存在该方法,运行阶段则执行右边子类重写后的方法。
以代码示例说明:
Fu f = new Zi();
// 编译看左边的父类中有没有name这个属性,没有就报错
// 在实际运行的时候,把父类name属性的值打印出来
System.out.println(f.name);// 编译看左边的父类中有没有show这个方法,没有就报错
// 在实际运行的时候,运行的是子类中的show方法
f.show();
六、多态的弊端
多态在编译阶段依赖左边父类类型,这导致若子类具有独有的功能,多态写法无法直接访问。例如:
class Animal {public void eat() {System.out.println("动物吃东西!");}
}class Cat extends Animal { public void eat() { System.out.println("吃鱼"); } public void catchMouse() { System.out.println("抓老鼠"); }
} class Dog extends Animal { public void eat() { System.out.println("吃骨头"); }
}class Test {public static void main(String[] args) {Animal a = new Cat();a.eat();a.catchMouse();// 编译报错,编译看左边,Animal没有这个方法}
}
七、引用类型转换
7.1 为什么要转型
多态无法直接调用子类独有的方法,为实现对这些方法的调用,需要进行转型。回顾基本数据类型转换,存在自动转换(范围小的赋值给范围大的,如double d = 5;
)和强制转换(范围大的赋值给范围小的,如int i = (int)3.14
)。多态的转型分为向上转型(自动转换)与向下转型(强制转换)。
7.2 向上转型(自动转换)
向上转型是子类类型向父类类型自动转换的过程,当父类引用指向子类对象时即发生向上转型,格式为:
父类类型 变量名 = new 子类类型();
如:Animal a = new Cat();
原因在于父类类型范围相对子类较大,例如Animal
包含所有动物,而Cat
只是其中一种,因此子类可自动转型给父类类型变量。
7.3 向下转型(强制转换)
向下转型是父类类型向子类类型的强制转换过程。对于已向上转型的子类对象,将父类引用转为子类引用需使用强制类型转换格式,即:
子类类型 变量名 = (子类类型) 父类变量名;
如:Aniaml a = new Cat();Cat c = (Cat) a;
7.4 案例演示
为调用子类特有方法,需进行向下转型。以如下代码为例:
定义类:
abstract class Animal { abstract void eat();
} class Cat extends Animal { public void eat() { System.out.println("吃鱼"); } public void catchMouse() { System.out.println("抓老鼠"); }
} class Dog extends Animal { public void eat() { System.out.println("吃骨头"); } public void watchHouse() { System.out.println("看家"); }
}
定义测试类:
public class Test {public static void main(String[] args) {// 向上转型 Animal a = new Cat(); a.eat(); // 调用的是 Cat 的 eat// 向下转型 Cat c = (Cat)a; c.catchMouse(); // 调用的是 Cat 的 catchMouse}
}
7.5 转型的异常
转型过程中可能出现问题,如下代码虽能编译通过,但运行时会抛出ClassCastException
类型转换异常:
public class Test {public static void main(String[] args) {// 向上转型 Animal a = new Cat(); a.eat(); // 调用的是 Cat 的 eat// 向下转型 Dog d = (Dog)a; d.watchHouse(); // 调用的是 Dog 的 watchHouse 【运行报错】}
}
这是因为创建的是Cat
类型对象,运行时无法转换为Dog
对象。
7.6 instanceof关键字
为避免ClassCastException
异常(ClassCastException
是 Java 中的一个运行时异常,当你尝试将一个对象强制转换为不兼容的类型时就会抛出该异常。),Java提供instanceof
关键字用于校验引用变量类型,格式为:
变量名 instanceof 数据类型
如果变量属于该数据类型或者其子类类型,返回true。
如果变量不属于该数据类型或者其子类类型,返回false。
因此,转换前可先进行判断,代码如下:
public class Test {public static void main(String[] args) {// 向上转型 Animal a = new Cat(); a.eat(); // 调用的是 Cat 的 eat// 向下转型 if (a instanceof Cat){Cat c = (Cat)a; c.catchMouse(); // 调用的是 Cat 的 catchMouse} else if (a instanceof Dog){Dog d = (Dog)a; d.watchHouse(); // 调用的是 Dog 的 watchHouse}}
}
7.7 instanceof新特性
JDK14引入新特性,可将判断和强转合并为一行:
// 新特性
// 先判断a是否为Dog类型,如果是,则强转成Dog类型,转换之后变量名为d
// 如果不是,则不强转,结果直接是false
if(a instanceof Dog d){d.lookHome();
} else if(a instanceof Cat c){c.catchMouse();
} else{System.out.println("没有这个类型,无法转换");
}
八、综合练习
8.1 需求
- 定义狗类:包含年龄、颜色属性,以及
eat(String something)
(表示吃的东西)和看家lookHome
(无参数)方法。 - 定义猫类:包含年龄、颜色属性,以及
eat(String something)
和逮老鼠catchMouse
(无参数)方法。 - 定义Person类(饲养员):包含姓名、年龄属性,以及
keepPet(Dog dog, String something)
(喂养宠物狗)和keepPet(Cat cat, String something)
(喂养宠物猫)方法,生成空参、有参构造及set
和get
方法。 - 定义测试类:实现
keepPet(Dog dog, String something)
方法打印“年龄为30岁的老王养了一只黑颜色的2岁的狗,2岁的黑颜色的狗两只前腿死死的抱住骨头猛吃”;keepPet(Cat cat, String something)
方法打印“年龄为25岁的老李养了一只灰颜色的3岁的猫,3岁的灰颜色的猫眯着眼睛侧着头吃鱼”。 - 思考:
Dog
和Cat
均为Animal
的子类,现有针对不同动物定义不同keepPet
方法的方式较为繁琐,思考如何简化及体会简化后的好处。- 尽管
Dog
和Cat
是Animal
的子类,但都有特有方法,思考如何在keepPet
中调用这些特有方法。
8.2 代码示例
// 动物类(父类)
public class Animal {private int age;private String color;public Animal() {}public Animal(int age, String color) {this.age = age;this.color = color;}public int getAge() {return age;}public void setAge(int age) {this.age = age;}public String getColor() {return color;}public void setColor(String color) {this.color = color;}public void eat(String something) {System.out.println("动物在吃" + something);}
}// 猫类(子类)
public class Cat extends Animal {public Cat() {}public Cat(int age, String color) {super(age, color);}@Overridepublic void eat(String something) {System.out.println(getAge() + "岁的" + getColor() + "颜色的猫眯着眼睛侧着头吃" + something);}public void catchMouse() {System.out.println("猫抓老鼠");}}// 狗类(子类)
public class Dog extends Animal {public Dog() {}public Dog(int age, String color) {super(age, color);}// 行为// eat(String something)(something表示吃的东西)// 看家lookHome方法(无参数)@Overridepublic void eat(String something) {System.out.println(getAge() + "岁的" + getColor() + "颜色的狗两只前腿死死的抱住" + something + "猛吃");}public void lookHome() {System.out.println("狗在看家");}
}// 饲养员类
public class Person {private String name;private int age;public Person() {}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 void keepPet(Dog dog, String something) {System.out.println("年龄为" + age + "岁的" + name + "养了一只" + dog.getColor() + "颜色的" + dog.getAge() + "岁的狗");dog.eat(something);}// 饲养猫public void keepPet(Cat cat, String something) {System.out.println("年龄为" + age + "岁的" + name + "养了一只" + cat.getColor() + "颜色的" + cat.getAge() + "岁的猫");cat.eat(something);}*/// 想要一个方法,能接收所有的动物,包括猫,包括狗// 方法的形参:可以写这些类的父类 Animalpublic void keepPet(Animal a, String something) {if(a instanceof Dog d) {System.out.println("年龄为" + age + "岁的" + name + "养了一只" + a.getColor() + "颜色的" + a.getAge() + "岁的狗");d.eat(something);} else if(a instanceof Cat c) {System.out.println("年龄为" + age + "岁的" + name + "养了一只" + c.getColor() + "颜色的" + c.getAge() + "岁的猫");c.eat(something);} else {System.out.println("没有这种动物");}}
}// 测试类
public class Test {public static void main(String[] args) {// 创建对象并调用方法/*Person p1 = new Person("老王",30);Dog d = new Dog(2,"黑");p1.keepPet(d,"骨头");Person p2 = new Person("老李",25);Cat c = new Cat(3,"灰");p2.keepPet(c,"鱼");*/// 创建饲养员的对象Person p = new Person("老王",30);Dog d = new Dog(2,"黑");Cat c = new Cat(3,"灰");p.keepPet(d,"骨头");p.keepPet(c,"鱼");}
}