多态的独特见解,多态学习一篇必懂。
什么是多态
在面向对象的程序设计语言中,多态是继数据抽象和继承之后的第三中基本特征。多态通过分离做什么和怎么做,从另一角度将接口和实现分离出来。多态不但能改善代码的组织结构和可读性,还能够创建可扩展的程序。
对象即可以作为它自己本身的类型使用,也可以作为它的基类型使用。而这种把对某个对象的引用视为对其基类型的引用的做法被称作向上转型(因为在继承树的画法中,基类是放置在上方的)。
public class test{
public static void main(String[] args){
BaseClass A = new ExportClass();
System.out.println(A);
}
}
class BaseClass{
public String toString(){
return "BaseClass";
}
}
class ExportClass extends BaseClass{
public String toString(){
return "ExportClass";
}
}
/* 执行结果:
* ExportClass
*/
静态绑定与动态绑定
将一个方法调用同一个方法主体关联起来被称作绑定,若在程序执行前进行绑定叫做前期绑定。运行时根据对象的类型进行绑定称为后期绑定。后期绑定也称为动态绑定和运行时绑定。Java中除了static和final方法(private方法属于final方法)之外,其它的所有方法都是后期绑定(后期绑定的方法具有多态性)。通常情况下,不必判定后期绑定——它会自动生成。
了解多态机制,可能就会开始认为所有的事物都可以多态的发生。然而,只有普通的方法调用可以是多态的。列如下面的示例所演示:
//ing.java
class Super{
public int field = 0;
public int getField(){
return field;
}
}
class Sub extends Super{
public int field = 1;
public int getField(){
return field;
}
public int getSuperField(){
return super.field;
}
}
public class ing{
public static void main(String[] args){
Super sup = new Sub();
System.out.println( " sup.field = " + sup.field +
" sup.getField() = " + sup.getField());
Sub sub = new Sub();
System.out.println(" sub.field = " + sub.field +
" sub.getField() = " + sub.getField() +
" sub.getSuperField() = " + sub.getSuperField());
}
}
/*
* 执行结果
* sup.field = 0, sup.getField() = 1
* sub.field = 1, sub.getField() = 1, sub.getSuperField() = 0
*/
在上面的例子中,为Super.field和Sub.field分配了不同的存储空间。然而,在调用sup在调用field的时候,调用的是基类中的field。由此可见只有普通方法才具有多态性。平常我们见不到这种情况,通常情况我们会将所有的对象设置成private,因此不能直接访问它们。只能调用方法来访问。
此外某个方法是静态的也不具备多态性(如下例子):
//test.java
public class test{
public static void main(String[] args){
BaseClass b = new ExportClass();
b.fun();
}
}
class BaseClass{
public static void fun(){
System.out.println("BaseClass");
}
}
class ExportClass extends BaseClass{
public static void fun(){
System.out.println("ExportClass");
}
}
/*
* 执行结果:
* BaseClass
*/
构造器和多态
构造器不同于其他方法,构造器隐式的声明了static(被static声明不具备多态性),那么构造器怎么通过多态在复杂的程序结构中运行?
注意下面这个程序
//PolyConstructors.java
class Glyph{
void draw(){
System.out.println("Glyph.draw()");
}
Glyph(){
System.out.println("Glyph() before draw()");
draw();
System.out.println("Glyph() after draw()");
}
}
class RoundGlyph extends Glyph{
private int radius = 1;
RoundGlyph(int r){
radius = r;
System.out.println("RoundGlyph.draw().radius = " + radius);
}
void draw() {
System.out.println("RoundGlyph.draw().radius = " + radius);
}
}
public class PolyConstructors{
public static void main(String[] args){
new RoundGlyph(5);
}
}
/*
*
* 调试结果:
* Glyph() before draw()
* RoundGlyph.draw().radius = 0
* Glyph() after draw()
* RoundGlyph.draw().radius = 5
*/
仔细看运行结果,基类的构造器总是在导出类的构造过程中先被调用,而且按照继承层次逐渐向上链接,使每个基类的构造器都能够调用。
在执行中先执行基类的构造方法。那么问题来了为什么没有执行基类中的draw()方法。当基类的构造器调用被覆盖后的draw()方法时,radius并不是默认值1。我们的执行结果不应该是下面这样的吗?
- 调试结果:(这只是设想的执行结果)
- Glyph() before draw()
- Glyph.draw()
- Glyph() after draw()
- RoundGlyph.draw().radius = 5
其实初始化的实际过程是:
1)在其他任何事物发生之前,将分配给对象的储存空间初始化成二进制的零
2)如前所述那样调用基类构造器,此时,调用被覆盖后的draw()方法(要在调用RoundGlyph构造器之前调用),由于会给对象储存空间初始化成二进制零的缘故,我们此时会发现radius的值是0。
3)按照声明的顺序调用成员的初始化方法。
4)调用导出类的构造器主体
这样做有一个好处,那就是所有东西都至少初始化成零(或者一些特殊数据类型中与“零”等价的值),而不是留作垃圾。
另一方面,我们应该对这个程序的结果相当震惊。在逻辑方面,做的已经非常完美,而它的行为却不可思议地错了,而且它并没有报错。这时就是构造器在捣乱,此类错误很容易让人忽略,会让浪费很多时间才能发现。因此,编写构造器时有一条有效的准则:“用尽可能简单的方法使对象进入正常状态;如果可以的话,避免调用其他方法(避免调用没有被static、final和private关键字声明的方法)”。在构造器内唯一能够安全调用的那些方法是基类中的static和final方法(也适用与private方法,它们自动属于final方法)。如下例子:
class Glyph{
private void draw(){
System.out.println("Glyph.draw()");
}
//这里把draw()方法声明成了private,也就不会出现基类中draw()调用导出类中的draw()
Glyph(){
System.out.println("Glyph() before draw()");
draw();
System.out.println("Glyph() after draw()");
}
}
class RoundGlyph extends Glyph{
private int radius = 1;
RoundGlyph(int r){
radius = r;
System.out.println("RoundGlyph.draw().radius = " + radius);
}
void draw() {
System.out.println("RoundGlyph.draw().radius = " + radius);
}
}
public class PolyConstructors{
public static void main(String[] args){
new RoundGlyph(5);
}
}
/*
*
* 调试结果:
* Glyph() before draw()
* Glyph.draw()
* Glyph() after draw()
* RoundGlyph.draw().radius = 5
*/
这些方法不能被覆盖,因此也就不会出现上述问题
谈向上(下)转型
由于向上转型(在继承层次中向上移动)会丢失具体的类型信息,如下例子:
class BaseClass{
public int number = 0;
public String f(){
return "BaseClass:f()";
}
public String g(){
return "BaseClass:g()";
}
}
class ExportClass extends BaseClass{
public int number = 1;
public String f(){
return "ExportClass:f()";
}
public String g(){
return "ExportClass:g()";
}
public String u(){
return "ExportClass:u()";
}
public String v(){
return "ExportClass:v()";
}
public String w(){
return "ExportClass:w()";
}
}
public class Test{
public static void main(String[] args){
BaseClass[] x = {
new BaseClass(),
new ExportClass()
};
System.out.println(x[0].f());
System.out.println(x[1].u());
}
}
运行结果
Test.java:20: 错误: 找不到符号
System.out.println((x[1]).u());
^
符号: 方法 u()
位置: 类 BaseClass
1 个错误
可以看到x[1]中没有u()方法,这就是向上转型所带来的损失。向上转型时只保留导出类继承基类的普通方法。因此我们想,通过向下转型——也就是在继承层次中向下移动。然而,我们知道向上转型是安全的,因为基类不会具有大于导出类的接口(基类中的普通方法,导出类中都会有)。
class BaseClass{
public int number = 0;
public String f(){return "BaseClass:f()";}
public String g(){return "BaseClass:g()";}
}
class ExportClass extends BaseClass{
public int number = 1;
public String f(){return "ExportClass:f()";}
public String g(){return "ExportClass:g()";}
public String u(){return "ExportClass:u()";}
public String v(){return "ExportClass:v()";}
public String w(){return "ExportClass:w()";}
}
public class ooo{
public static void main(String[] args){
BaseClass[] x = {
new ExportClass(),
new ExportClass()
};
System.out.println(x[0].number);
System.out.println(((ExportClass)x[1]).number);
}
}
/*
* 运行结果:
* 0
* 1
* /
在这里可以看到x[1]被向下转型了,调用了ExportClass中的number。然而并不建议向下转型,这个操作相当于你把风险转移到了运行时,并不可取。如果非要向下转型不可,一定要做好检验和异常处理
本文地址:https://blog.csdn.net/hello_ing/article/details/111831067
上一篇: BUAA OO 第二单元总结
下一篇: 第3篇 构造函数与析构函数