【多态知识点】
1. 多态能解决什么问题?
可以确定某个变量、元素等它的类型是属于某个系列的,但是不能确定它更具体的类型。
这个变量需要赋值的对象可能是这个系列的子类的任意一种对象。
那么就需要多态这种特性支持。
2. 什么是多态?
(1)多态引用的概念
父类类型 变量= 子类的对象;
如:
public class Pet{
public void eat(){
System.out.println("~~~");
}
}
public class Cat extends Pet{
@Override
public void eat(){
System.out.println("猫在吃鱼");
}
}
public class Dog extends Pet{
@Override
public void eat(){
System.out.println("狗在啃骨头");
}
}
public class Person{
private String name;
private Pet pet;
public void adopt(Pet pet){
this.pet = pet;
}
public void feed(){
pet.eat();
}
}
//测试
public class Test{
public static void main(String[] args){
Person p = new Person();
Pet dog = new Dog();
p.adopt(dog);
p.feed();
//将会返回:狗在啃骨头,而不是默认的~~~
}
}
(2)多态的现象
编译时类型和运行时类型不一致
编译时,变量为父类类型,它可以,也只能调用父类的方法
运行时,这个变量是子类类型,所以会执行子类重写的方法体。
如上面的程序,执行的是重写后的eat();而如果在Dog类中再写一个方法watchHouse(),在测试类中调用p.watchHouse()将会报错
==》对象的本质类型是子类的类型,只是编译器在编译时,把它当成父类类型来检查
(3)多态的优势和弊端
好处:实现动态绑定,代码编写更灵活,如上面程序中的Dog和Cat,执行不同子类重写方法
弊端:失去个性,只能调用父类声明的方法,子类扩展方法缺失
3. 应用场景
(1)多态数组
元素的类型声明为父类类型,元素实际存储的是子类对象
(2)多态参数
形参类型为父类类型,实参对象是子类对象
(3)多态返回值
方法返回值类型是父类类型,实际返回是子类对象
(4)变量类型是父类类型,变量值为子类对象
重点:重写的方法:编译时看父类,运行时看子类
练习1:图形
(1)父类Graphic图形
public double area()方法:返回0.0
public double perimeter()方法:返回0.0
public String getInfo()方法:返回图形面积和图形周长
(2)子类Circle圆继承Graphic图形
包含属性:radius,属性私有化
包含get/set方法
重写area()求面积方法
重写perimeter()求周长方法
重写getInfo()方法,返回圆的半径,面积和周长
(3)子类矩形Rectangle继承Graphic图形
包含属性:length、width,属性私有化
包含get/set方法
重写area()求面积方法
重写perimeter()求周长方法
重写getInfo()方法,返回长和宽,面积、周长信息
(4)在测试类的main方法中创建多个圆和矩形对象方法放到Graphic[]类型的数组中,并按照面积从小到大排序
代码如下:
Graphic类:
public class Graphic {
public double area(){
return 0.0;
}
public double perimeter(){
return 0.0;
}
public String getInfo(){
return "面积:"+area()+"周长:"+perimeter();
}
}
Rectangle类:
public class Rectangle extends Graphic{
private double length;
private double width;
public double getLength() {
return length;
}
public void setLength(double length) {
this.length = length;
}
public double getWidth() {
return width;
}
public void setWidth(double width) {
this.width = width;
}
@Override
public double area() {
return length*width;
}
@Override
public double perimeter() {
return (length+width)*2;
}
@Override
public String getInfo() {
return "长"+length+"宽"+width+super.getInfo();
}
}
Circle类:
public class Circle extends Graphic {
private double radius;
public double getRadius() {
return radius;
}
public void setRadius(double radius) {
this.radius = radius;
}
@Override
public double area() {
return Math.PI*radius*radius;
}
@Override
public double perimeter() {
return Math.PI*radius*2;
}
@Override
public String getInfo() {
return "半径为:"+radius+super.getInfo();
}
}
测试类:
public class GraphicTest {
public static void main(String[] args) {
Graphic[] arr = new Graphic[4];
Rectangle r1 = new Rectangle();
r1.setLength(5);
r1.setWidth(2);
arr[0] = r1;
Rectangle r2 = new Rectangle();
r2.setLength(4);
r2.setWidth(3);
arr[1] = r2;
Circle c1 = new Circle();
c1.setRadius(2);
arr[2] = c1;
Circle c2 = new Circle();
c2.setRadius(1);
arr[3] = c2;
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i].getInfo());
}
for (int i = 1; i < arr.length; i++) {
for (int j = 0; j < arr.length-i; j++) {
if(arr[j].area()>arr[j+1].area()){
Graphic tmp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = tmp;
}
}
}
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i].getInfo());
}
}
}
4. 向上转型与向下转型
向上转型:
自动类型转换。
当把子类对象赋值给父类的变量时,在“编译时”会自动类型提升为父类的类型。
向下转账:
需要调用子类“扩展”的方法时,必须进行向下转型,通过强制类型转换完成,这样才能通过编译
对象的本质类型从头到尾都没有变,只是骗编译器的。
5. 什么情况下向上、向下转型才能成功
向上转型:只要满足对象是这个变量的类型或它的子类类型就可以
如: Person p1 = new Man();
向下转型:必须满足对象的运行时类型 <= ()中向下转的类型
如:Person p1 = new Man();
Man m = (Man)p1;
又如:Person p2 = new ChineseMan();
Man m2 = (Man)p2;
6. 如何避免ClassCastException?
加入instanceof判断
作用为:判断某个变量、对象的运行时类型是否小于等于转换类型
7. 成员变量和成员方法有无多态?
成员变量:没有多态概念,变量的寻找只看编译时类型,没有编译时类型和运行时类型不一致的说法。
成员方法:有多态的概念,编译时看父类,运行时看子类,如果子类重写了,一定执行子类重写的方法体
如:
public class Test{
public static void main(String[] args){
Son s = new Son();
System.out.println(s.a);//输出2
System.out.println(((Father)s).a);//输出1,
//把s向上转型为Father类,让编译器从Father中取a
Father f = new Son();
System.out.println(f.a); //1
//此时f的编译类型为father,让编译器从father中取a
System.out.println(((son)f).a); //2
Son son = new Son();
son.method();
((Father)son).method(); //方法重写了,只要运行类型为Son,那么一定执行重写方法
}
}
class Father{
int a = 1;
public void method(){
System.out.println("Father.method");
}
}
class Son{
int a = 2;
@Override
public void method(){
System.out.println("Son.method");
}
}
8. 虚方法
凡是可以被子类重写的方法,都叫做虚方法
子类不能重写的方法就不是虚方法
不能重写的方法有:private等
9. 虚方法的调用原则
(1)编译时:看父类
在父类中找到能够匹配的方法
(2)运行时:看子类
如果子类重写了这个虚方法,那么一定执行重写的方法
如果子类没有重写这个虚方法,那么还是执行父类方法
练习2:判断输出
public class Test {
public static void main(String[] args) {
MyClass my = new MySub();
Father f = new Father();
Son s = new Son();
Daughter d = new Daughter();
my.method(f);
/*my变量的编译时类型是MyClass,编译时看MyClass;运行时看MySub
(1)第一步,编译器去MyClass类中找匹配的方法
public void method(Father f)
public void method(Son s)
实参f的编译时类型Father,和它最匹配的是public void method(Father f),找到了
(2)第二步,运行时,MySub中找重写
public void method(Father d){
System.out.println("sub-father");
}
输出sub-father
*/
my.method(s);
/*my变量的编译时类型是MyClass,编译时看MyClass;运行时看MySub
(1)第一步,编译器去MyClass类中找匹配的方法
public void method(Father f)
public void method(Son s)
实参s的编译时类型Son,和它最匹配的是public void method(Son s),找到了
(2)第二步,运行时,MySub中找重写
没找到,输出son
*/
my.method(d);
/*my变量的编译时类型是MyClass,编译时看MyClass;运行时看MySub
(1)第一步,编译器去MyClass类中找匹配的方法
public void method(Father f)
public void method(Son s)
实参d的编译时类型Daughter,没有匹配的,找兼容的,public void method(Father f)
(2)第二步,运行时,MySub中找重写
public void method(Father d){
System.out.println("sub-father");
}
输出sub-father
*/
}
static class MyClass{
public void method(Father f){
System.out.println("father");
}//第一个方法
public void method(Son s){
System.out.println("son");
}
//重载第一个方法
}
static class MySub extends MyClass{
public void method(Father d){
System.out.println("sub-father");
}//重写第一个方法
public void method(Daughter d){
System.out.println("daughter");
}//重载
}
static class Father{}
static class Son extends Father{}
static class Daughter extends Father{}
}