作为面向对象三大知识点之一, 多态可以讲是相对最难理解那个. 本人在这里也是简单分析一下.
一, 多态的定义.
面向对象编程中, 1个超类的引用可以指向属于超类的对象, 也可以根据需要指向超类的派生类族对象.
这个过程中根据引用指向不同类型对象, 可以调用不同方法, 这就是多态.
1.1 什么是类的引用.
Java 中, 1个实例化后的对象的内存是分配在内存的Heap(堆)区中的, 但是如果使用这个对象, 必须在Stuck(栈) 区定义1个 变量来存放那个堆区类型的地址.
例如:
Human A;
上面的代码定义了1个Human类型 的变量A, 这个A只是存放在Stuck区, 我们就说这个A是1个引用, 它可以指向堆区的Human类型的内存.
但是上面这个A类型并没有指向任何内存, 就是所谓的"对象A没有实例化"
接下来:
A = new Human();
new Human() 这个1句是在Heap区动态分配1块Human类型的内存. A = new Human(); 就是把该内存的头部地址赋给A.
这是我们就说引用A已经有了指向, 也就是所谓的"对象A 被实例化"
1.2 类的引用的指向可以改变
既然应用类型的指向是通过赋值来实现的, 也就代表这个指向可以修改, 意思就是上面的引用A可以指向另一块堆区内存的头部地址.
看个例子:
Human A = new Human();
Human B = new Human();
上面的两句代码就在Heap区分配了两块Human类型的内存, 并把它们的头部地址分别赋予两个不同的引用A 和 B
A = B;
再执行上面的语句, 意思就是把A指向了B保存的地址, 也就是说A 和 B 现在是指向同一块内存地址了.
但是, A原来指向的Heap区地址就消失了, 如果在C/C++ 中, 这就是明显的内存泄露. 但是Java中会自动把A原来的地址释放的.
1.3 超类的引用可以指向其子孙派生类的内存地址.
这个就是多态关键了.
看下面的例子:
package Object_kng.Ploy_kng;
class Human_1{
private int id;
private int name;
}
class Student_1 extends Human_1{
private String school;
}
class Section_monitor_1 extends Student_1{
private String subject;
}
public class Poly_1{
public static void f(){
Human_1 hm = new Human_1();
Student_1 st = new Student_1();
Section_monitor_1 sm = new Section_monitor_1();
//st = hm; //error
//st = (Student_1)hm; //error (can pass the compilation, but will fail in excution)
//ok
hm = st;
//ok
hm = sm;
//st = hm; //error
st = (Student_1)hm; //ok
}
}
上面定义了3个业务类, 其中学生类继承 Human_1类, 而课代表类继承学生类.
在f() 函数中, 分别对上述3个类各实例化1个对象, 也就是3个引用 hm, st, sm分别具有了自己的指向.
接下来一句一句讲:
//st = hm; //error
//st = (Student_1)hm; //error (can pass the compilation, but will fail in excution)
注意, 这时st 和 hm属于不同的类. 但是hm所属的类Student_1 继承自 st所属的类 Human_1.
这一句会编译失败, 是因为多态中,
不允许派生类的引用 指向 超类的对象.
原因也不难理解:
首先可以看看Human_1 类, 有两个私有成员, Student_1 貌似只有1个私有成员, 但是因为继承关系, Student_1 会
隐藏继承 Human_1的所有成员的. 只不过这里没有对Human_1的成员进行封装, 所以不能直接使用隐藏的成员.
因为隐藏 也就是说超类对象内存肯定是小于等于 其派生类的对象的.
可就是说应用类型st (Student_1) 类本身是可以直接或间接使用3个成员的(id, name, school), 而 hm本身指向的对象只有两个成员(id, name).
如果st指向h的对象地址, 那么当st使用school成员时, 就会在h对象内存里找不到school这个成员.
这个就是java里不允许 派生类应用类型 指向 超类对象的原因.
如果按现实意义理解也可以:
st = hm 的意思就是把人类 作为学生来处理, 这个肯定是不和常理的. 因为不是所有人类都是学生. 反过来就可以了.
st = (Student_1)hm;
这一句是把hm这个对象强制转换成Student_1类的
新对象(注意hm对象本身并没有改变), 然后赋予st.
这一句是能通过编译的, 但是当执行时就会抛出异常:
[java] java.lang.ClassCastException: Object_kng.Poly_kng.Human_1 cannot be cast to Object_kng.Poly_kng.Student_1
java里不允许把超类对象转成 派生类对象.... 也就是不是任意1个普通人类都能作为学生来看待了.
hm = st;
上面这句就是正确的, 意思是把引用hm 指向 引用st 指向的对象.
也就是Human_1 的引用类型 指向了他的子派生类 的对象. 这是可以的.
因为派生类Student 隐藏继承了Human_1 的所有成员.
现实意义就是任意1个学生都可以作为人来看待.
hm = sm;
//st = hm; //error
st = (Student_1)hm; //ok
这段就稍稍复杂, 首先把 hm 指向它的 孙派生类的对象, 这个也没问题.
然后再执行
st = (Student_1)hm;
这一句在上面是错误的, 为何在这里是正确的呢.
因为上面的 hm当时是指向 Human_1 类的对象 . 而在这里hm 已经指向了 Section_monitor_1类的对象.
而把 Section_monitor_1对象转化成 它的父超类 Student_1 是没问题的..
也就说任何1个科代表都可以被看作学生来看待嘛.
1.4 据引用指向不同类型对象, 可以调用不同方法
因为面向对象中, 派生类继承的方法是可以重写的. 两个继承关系的类里面分别有两个相同名字相同参数相同返回值的 函数.
但是函数体可以是不同的.
下面的看下面的例子:
package Object_kng.Poly_kng;
class Human_2{
public void print(){
System.out.printf("it's Human\n");
}
}
class Student_2 extends Human_2{
public void print(){
System.out.printf("it's Student\n");
}
}
class Section_monitor_2 extends Student_2{
public void print(){
System.out.printf("it's Section_monitor\n");
}
}
public class Poly_2{
public static void f(){
Human_2 hm = new Human_2();
Student_2 st = new Student_2();
Section_monitor_2 sm = new Section_monitor_2();
hm.print();
hm = st;
hm.print();
hm = sm;
hm.print();
}
}
上面一样定义了3个类... 他们是继承关系
这3个类都有1个print 方法. 其中 Studnet_2类重写了 Human_2 的print方法.. Section_monitor_2 类重写了Student_2类的print 方法
在接下来的f() 函数中.
根据引用hm指向的不同,
hm.print() 分别执行的是3个不同类的方法:
hm.print() 分别执行的是3个不同类的方法:
输出:
[java] it's Human
[java] it's Student
[java] it's Section_monitor
二, 多态的一个简单应用.
接下来就利用上面的特征作1个简单的应用.
class Human_3{
private int id;
private String name;
public Human_3(int id,String name){
this.id = id;
this.name = name;
}
public String get_mem(){
return "Human: " + id + " " + name;
}
}
class Student_3 extends Human_3{
public Student_3(int id, String name){
super(id,name);
}
public String get_mem(){
return "student: " + super.get_mem();
}
}
class Section_monitor_3 extends Student_3{
public Section_monitor_3(int id, String name){
super(id,name);
}
public String get_mem(){
return "section_monitor: " + super.get_mem();
}
}
public class Poly_3{
public static String extend_mem(Human_3 hm){
java.util.Date now = new java.util.Date();
java.text.DateFormat d1 = java.text.DateFormat.getDateInstance();
return d1.format(now) + ": " + hm.get_mem();
}
public static void print(){
Human_3 hm = new Human_3(1,"Jack");
Student_3 st = new Student_3(2,"Dick");
Section_monitor_3 sm = new Section_monitor_3(3,"Cindy");
System.out.printf("%s\n",extend_mem(hm));
System.out.printf("%s\n",extend_mem(st));
System.out.printf("%s\n",extend_mem(sm));
}
}
上面一样定义了那个3个类, 而且具有两个成员, 进行了简单的封装...
每个类都有1个get_mem()函数, 返回不同的字符串.
现在需要1个函数, 为每1个类的get_mem()返回值前增加1个日期字符串:
利用多态技术:
可以简单地定义1个 extend_mem() 函数, 参数是超类的对象.
那么这个函数就可以根据参数的不同类型而调用不同对象的 get_mem()函数.
如果没有多态技术, 就必须根据参数的类型不同 而写上3个函数了.
这个还不是最重要的, 假如以后第三个类 以后还再派生出第4个类, 那么这个函数一样适用不必改写.
也就是说多态可以实现面向对象程序的扩展性, 这个就是多态最重要的意义.
三, 多态的一些简单要点:
这里也是算是1个简单总结吧:
1. 派生类对象更可以直接赋给父类引用, 但父类对象任何情况下都不能直接赋给超类引用.
2. 如果1个超类引用指向了1个派生类对象, 只能访问派生类对象继承自己超类的成员or方法. 而不能访问派生类独有的成员or方法.
3. 超类引用永远不可能直接赋给派生类引用.
例如用 B extends A. a 是 A的一个引用, b 是B的一个引用
b = a; 肯定是错的.
4. 只有在超类引用本身指向的就是1个派生类或其子孙类(派生类的派生类)对象时, 才可以把超类引用强制转化为派生类引用.
上面的例子, 假如a 指向的实际是B的一个对象.
那么
b = (B)a; 是正确的. 这里必须强制转换.
5. 但是其他情况下不允许把超类引用强制转化为子类引用, 否则会抛出异常, 上面说过了.