06_01 多态-动态-静态-替换
1. 静态行为和动态行为
- 面向对象语言的强大之处在于对象可以在运行时动态地改变其行为。
- 编程语言中,术语静态总是用来表示在编译时绑定于对象并且不允许以后对其进行修改的属性或特征。
- 术语动态用来表示直到运行时绑定于对象的属性或特征。
静态类和动态类
- 变量的静态类是指用于声明变量的类。静态类在编译时就确定下来,并且再也不会改变。类型由声明该变量时使用的类型决定。
- 变量的动态类指与变量所表示的当前数值相关的类。动态类在程序的执行过程中,当对变量赋新值时可以改变。类型由实际赋给该变量的对象决定。
- 如果变量的动态类和静态类的类型不一致,会出现所谓的多态。
多态的四种形式
- 重载
- 改写
- 多态变量
- 泛型
2.OVERRIDE(改写,重写,覆盖,重置,覆写)
- 重写,英文是override(overriding,也有称做overwrite),是指在继承情况下,子类中定义了与其基类中方法具有相同名称、相同类型签名(相同返回值类型或兼容类型和相同参数类型)的方法,但重新编写了方法体,就叫做子类把基类的方法重写了。
- 这种方法在使用过程中,Java虚拟机会根据被调用这个方法的对象类型来确定哪个方法被调用
与替换原则结合使用。
实现运行时多态技术的条件:
有一个继承层次关系;
在子类中改写/重写父类的方法;
通过父类的引用对子类对象进行调用。
采用多态技术的优点:
引进多态技术之后,尽管子类的对象千差万别,但都可以采用 父类引用.方法名([参数]) 统一方式来调用,在程序运行时能根据子对象的不同得到不同的结果。这种“以不变应万变”的形式可以规范、简化程序设计,符合软件工程的“一个接口,多种方法”思想。
代替与改进
两种不同的关于改写/覆盖/重写的解释方式:
- 代替(replacement):在程序执行时,实现代替的方法完全覆盖父类的方法。即,当操作子类实例时,父类的代码完全不会执行。
- 改进(refinement):实现改进的方法将继承自父类的方法的执行作为其行为的一部分。这样父类的行为得以保留且扩充。
向上转型(重要)
- 因为子类其实是一种特殊的父类,因此java允许把一个子类对象直接赋值给一个父类引用变量,无须任何类型转换,或者被称为向上转型,由系统自动完成。
- 向上转型时,父类引用指向子类对象会遗失与父类对象共有之外的其他方法,也就是在转型过程中,子类的新有的方法都会遗失掉,在编译时,系统会提示找不到方法的错误。
class Animal {
public void speak() {
System.out.println("Animal speaks??");
}
}
class Dog extends Animal {
public void speak() {
bark();
}
public void bark() {
System.out.println("Dog barks!");
}
}
class Bird extends Animal {
public void speak() {
}
public void twitter() {
System.out.println("Bird twitters!");
}
}
Dog fido;
fido = new Dog();
fido. speak() ; // will bark
//Dog barks!
fido.bark() ; // will bark
//Dog barks!
Animal pet;
pet = fido; //父类变量由子类进行了实例化,即向上转型。
pet.speak(); // pet是父类的变量,但由于实例化是由子类完成的,所以父类中的speak()已经被子类中的speak()覆写。故实际执行的是子类中的speak()方法。
// Dog barks !
pet.bark() ; →error, Animal中没有bark方法,编译错误
静态类型和动态类型的区别
- 对于静态类型面向对象编程语言,在编译时消息传递表达式的合法性(调用的合法性)不是基于接收器的当前动态数值,而是基于接收器的静态类来决定的。
- 运行时执行动态类所具有类型的方法
- 当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;
- 如果有,再去调用子类的该同名方法。
强制(coercion)、转换(conversion)和造型(cast)
- 强制是一种隐式的类型转换,它发生在无需显式引用的程序中。
- 关于这种类型转换的一个典型例子就是两个变量(一个声明为实数,一个声明为整数)之间的相加操作
double x=2.8;
int i=3;
x=i+x;//integer i will be converted to real
- 转换表示程序员所进行的显式类型转换。在许多语言里这种转换操作称为“造型”。
x=((double)i)+x;
int i=0; double x=1.2; i=(int) x;
运行时类型决定
- 替换原则可以通过提升数值在继承层次上的位置来体现(向上转型)。
将子类Dog 类型的数值赋值给类型为父类Animal 的变量。
- 有时则相反,还需要判断一种变量目前所包含的数值是否为类层次中的低层次类(向下转型)。
判断类型为Animal的变量所包含的数值是否为Dog。
向下造型(反多态)(重要)
- 做出数值是否属于指定类的决定之后,通常下一步就是将这一数值的类型由父类转换为子类。
- 这一过程称为向下造型,或者反多态,因为这一操作所产生的效果恰好与多态赋值的效果相反。
Animal aPet=new Dog();
Dog d;
d = (Dog) aPet;
class Person {
public void fun1() {
System.out.println(“1.Person{fun1()}”); }
public void fun2() {
System.out.println(“2.Person{fun2()}”); }
}
class Student extends Person //继承了父类//Person,自然继承了方法fun1、fun2 {
public void fun1() //覆写了父类中的方法fun1() {
System.out.println(“3.Student{fun1()}”); }
public void fun3() {
System.out.println(“4.Student{fun3()}”); }
}
class TestJavaDemo2 {
public static void main(String[] args) {
Person p = new Student(); Student s = (Student)p;
//将p对象向下转型
p.fun1();
p.fun2();
}
}
如果p.fun3();//编译错误 ,用父类引用调用父类不存在的方法
class Person {
public void fun1() {
System.out.println("1.Person{fun1()}"); }
public void fun2() {
System.out.println("2.Person{fun2()}"); }
}
class Student extends Person{ //继承了父类//Person,自然继承了方法fun1、fun2
public void fun1() { //覆写了父类中的方法fun1()
System.out.println("3.Student{fun1()}"); }
public void fun3() {
System.out.println("4.Student{fun3()}"); }
}
public class hrt {
public static void main(String args[]) {
Person p = new Person();
Student s = (Student)p; //将p对象向下转型
p.fun1();
p.fun2();
}
}
Exception in thread “main” java.lang.ClassCastException: Person cannot be cast to Student
编译无错,但是运行出错!
在向下造型转换非法时返回空值,因此,在造型转换的同时也实现了类型测试
方法绑定
- 方法绑定(Binding,也叫联编)是指一个方法的调用关联其方法体的过程
一个方法的调用与方法所在的类(方法主体)关联起来。 - 静态方法绑定:在编译时刻进行方法绑定
- 静态联编:在程序编译连接阶段进行的联编, 又称为早期联编; 因为这种联编是在程序运行之前完成的。
- 动态方法绑定:在运行时刻进行方法绑定
- 动态联编:在程序运行时进行的联编,又称为晚期联编;
- 响应消息时对哪个方法进行绑定是由接收器当前所包含的动态数值来决定的。
- 动态绑定灵活性相对静态绑定来说要高,因为它在运行之前可以进行选择性的绑定。但动态绑定的执行效率要低些,实现起来更加复杂。
多态变量
- 如果方法所执行的消息绑定是由最近赋值给变量的数值的类型来决定的,那么就称这个变量是多态的。
- Java,Smalltalk等语言变量都可以是多态的。
小结
- 父类引用可以指向子类对象,子类引用不能指向父类对象。
- 把子类对象直接赋给父类引用叫upcasting向上转型,向上转型不用显式转型。
如Father father = new Son();
- 把指向子类对象的父类引用赋给子类引用叫向下转型(downcasting),要显式转型。
如father就是一个指向子类对象的父类引用,把father赋给子类引用son 即Son son =(Son)father;
其中father前面的(Son)必须添加,进行显式类型转换。
- upcasting 会丢失子类特有的方法,但是子类overriding 父类的方法,子类方法有效
- 向上转型的作用,减少重复代码,父类为形参,调用时用子类作为实参,就是利用了向上转型。这样使代码变得简洁。体现了JAVA的抽象编程思想
class Human {
public void run() {
System.out.println("Human run..");
}
}
class Male extends Human {
@Override
public void run() {
System.out.println("Male run..");
}
}
class Female extends Human {
@Override
public void run() {
System.out.println("Female run..");
}
}
public class hrt {
public static void dorun(Human h) {
h.run();
}
public static void main(String[] args) {
Female f= new Female (); dorun(f); }
}
结果:
Female run…
3.多态的形式-1:重载
重载(专用多态):(参数)类型签名区分
class overloader{
//three overloaded meanings for the same name
public void example (int x){……}
public void example (int x,double y){……}
public void example (string x){……}
}
类型签名
函数类型签名是关于函数参数个数、参数类型、参数顺序和返回值类型的描述。
4.多态的形式-2
改写/覆盖/重写(包含多态):层次关系中,相同类型签名
发生在有父类和子类关系的上下文中
class Parent{
public void example(int x){……}
}
class Child extends Parent{
//same name, different method body
public void example(int x){……}
}
5.重载
重载:同一个类定义中有多个同名的方法,但有不同的形参,而且每个方法有不同的方法体,调用时根据形参的个数、顺序和类型来决定调用的是哪个方法
Overloaded
语言中很多单词都是重载的,需要使用上下文来决定其确切含义。
例如:set out 开始,set off 出发,set down 记下
基于类型签名的重载
多个过程(或函数、方法)允许共享同一名称,且通过该过程所需的参数数目、顺序、和类型来对它们进行区分。即使函数处于同一上下文,这也是合法的。
class Example{
//same name, three different methods
int sum(int a){return a;}
int sum(int a,int b){return a+b;}
int sum(int a,int b,int c){return a+b+c;}
}
重载
- 重载是在编译时执行的,而改写是在运行时选择的。
- 重载是多态的一种很强大的形式。
- 非面向对象语言也支持。
- 关于重载的解析,是在编译时基于参数值的静态类型完成的。(重要)
- 不涉及运行时机制。
class Parent { }
class Child extends Parent { }
void Test(Parent p) { }
void Test(Child c) { }
Parent value = new Child( );
Test (value);//执行哪个方法?
答案:Parent
重载的解析
如果两个或更多的方法具有相同的名称和相同的参数数目,编译器如何匹配?
当类的设计者提供了重载方法之后,类的使用者在使用这些方法时,编译器需要确定调用哪一个方法,确定的依据是参数列表,确定的过程被称为重载的解析。
对于 Java 语言,如果两个或者更多的方法具有相同的名称和相同的参数数目,则编译器将使用下面的算法来确定如何匹配。
- 找到所有可能进行调用的方法,亦即,各个参数(实参)可以合法地赋值给各个参数类型(形参)的所有方法。如果找到一个在调用时可以完全匹配所使用的参数类型的方法,那么就执行这个方法。
- 对于第一步所产生的集合中的方法,两两进行比较,如果一个方法的所有参数类型都可以赋值给另外一个方法,那么就将第二个方法从集合中移走。(大的移走)
重复以上操作,直至无法实现进一步的缩减为止。
- 如果只剩下一个方法,那么这个方法就非常明确了,调用这个方法即可。如果剩余的方法不止一个,那么调用就产生歧义了,此时编译器将报告错误。
例子:
void order (Dessert d, Cake c);
void order (Pie p, Dessert d);
void order (ApplePie a, Cake c);
order (aDessert, aCake);//执行方法一
order (anApplePie , aDessert);//执行方法二
order (aDessert , aDessert);//错误 (都没有)
order (anApplePie , aChocolateCake);//执行方法三
order (aPie , aCake);//错误 (一和二冲突)
order (aChocolateCake, anApplePie );//错误 (都没有)
order (aChocolateCake, aChocolateCake);//方法一
order (aPie , aChocolateCake);//错误 (一和二冲突)
重定义
当子类定义了一个与父类具有相同名称但类型签名不同的方法时,发生重定义。
类型签名的变化是重定义区别于改写/覆盖/重写的主要依据。
JAVA的重载与重写(改写/覆盖/重置)
重载(overload):
- 必须具有不同的参数列表;
- 可以有不同的访问修饰符;
- 重载可以发生在基类和派生类之间,同样要求函数名相同,参数列表不同,返回值类型可以相同可以不相同
- 可以抛出不同的异常;
- 调用方法时通过传递给它们的不同参数个数和参数类型来决定具体使用哪个方法(编译器决定), 这就是多态性。
重写
- 子类方法的名称,参数签名和返回类型必须与父类方法的名称,参数签名和返回类型一致(或返回类型兼容)。
- 重写方法不能使用比被重写的方法更严格的访问权限,即访问修饰符的限制一定要大于被重写方法的访问修饰符,亦即子类方法不能缩小父类方法的访问权限。 (public>protected>private)
- 子类方法不能抛出比父类方法更多的异常。子类方法抛出的异常必须和父类方法抛出的异常相同,或者子类方法抛出的异常类是父类抛出的异常类的子类。
- 重写方法只能存在于具有继承关系中,重写方法只能重写父类非私有的方法。
方法的改写/覆盖/重置与重载的区别
- 方法的改写/覆盖是子类和父类之间的关系,而重载一般是同一类内部多个方法间的关系
- 方法的改写/覆盖一般是两个方法间的,而重载时可能有多个重载方法
- 改写/覆盖的方法有相同的方法名和形参表,而重载的方法只能有相同的方法名,不能有相同的形参表
- 改写/覆盖时区分方法的是根据被调用方法的对象,而重载是根据参数来决定调用的是哪个方法
- 用final修饰的方法是不能被子类覆盖的,只能被重载
6. 替换的本质
继承和替换原则的引入对编程语言的影响
- 类型系统
- 赋值的含义
- 等价测试
- 复制建立
- 存储分配
7.内存
分配方案
- 最小静态空间分配:只分配基类所需的存储空间。
- 最大静态空间分配:无论基类还是派生类,都分配可用于所有合法的数值的最大的存储空间。
- 动态内存分配:只分配用于保存一个指针所需的存储空间。在运行时分配对象所需的存储空间,同时将指针设为相应的合适值(地址)。
C++使用最小静态空间分配策略。结果?
Window x;
TextWindow y;
x = y;
后果:切割
C++规则
-
对于指针(引用)变量:当消息调用可能被改写的成员函数时,选择哪个成员函数取决于接收器的动态数值。
-
对于其他变量:关于调用虚拟成员函数的绑定方式取决于静态类(变量声明时的类),而不取决于动态类(变量所包含的实际数值的类)。
-
C++保证变量x只能调用定义于Window类中的方法,不能调用定义于TextWindow类中的方法。
-
定义并实现于Window类中的方法无法存取或修改定义于子类中的数据,因此不可能出现父类存取子类的情况。
最大静态分配
对象大小?
不论是父类变量还是子类变量,都分配变量值可能使用的最大存储空间
对整个程序扫描?
只有当整个程序运行完之后才会知道所有对象的大小
动态内存分配
- 栈中不保存对象值。
- 栈通过指针大小空间来保存标识变量,对象值/数据值保存在堆中。
- 指针变量都具有恒定不变的大小,变量赋值时,不会有任何问题。
- Smalltalk、Java都采用该方法。
8. 赋值(重要)
赋值
内存分配方法影响赋值的含义:
- 复制语义:
- 赋值会将操作符右侧的变量值复制给操作符左侧的变量。
- 此后,这两个变量值是互相独立的,其中一个变量值的改变不会影响到另外一个变量值
- 指针语义:
- 两个变量不仅具有相同的数值,而且还指向存储数值的同一内存地址
复制和克隆
- 浅复制(shallow copy):共享实例变量。
原有变量和复制产生的变量引用相同的变量值
- 深复制(deep copy):建立实例变量的新的副本。
C++:拷贝构造函数
Java:改写clone方法
什么是clone?
- 在实际编程过程中,我们常常要遇到这种情况:有一个对象A,在某一时刻A中已经包含了一些有效值,此时可能会需要一个和A完全相同新对象B,并且此后对B任何改动都不会影响到A中的值,也就是说,A与B是两个独立的对象,但B的初始值是由A对象确定的。
- 在Java语言中,用简单的赋值语句是不能满足这种需求的。要满足这种需求虽然有很多途径,但实现clone()方法是其中最简单,也是最高效的手段。
- Java的所有类都默认继承java.lang.Object类,在java.lang.Object类中有一个方法clone()。JDK API的说明文档解释这个方法将返回Object对象的一个拷贝。
要说明的有两点:
- 拷贝对象返回的是一个新对象,而不是一个引用。
- 拷贝对象与用new操作符返回的新对象的区别就是这个拷贝已经包含了一些原来对象的信息,而不是对象的初始信息。
- Object.clone() 是一个特殊方法。在一个类implement了Cloneable之后,它会保证调用clone()的对象的类跟克隆过来的对象的类是一致的
什么是影子clone?
下面的例子包含三个类UnCloneA,CloneB,CloneMain。CloneB类包含了一个UnCloneA的实例和一个int类型变量,并且重载clone()方法。CloneMain类初始化CloneB类的一个实例b1,然后调用clone()方法生成了一个b1的拷贝b2。最后考察一下b1和b2的输出:
- 输出的结果说明int类型的变量aInt和UnCloneA的实例对象unCA的clone结果不一致,int类型是真正的被clone了,因为改变了b2中的aInt变量,对b1的aInt没有产生影响,也就是说,b2.aInt与b1.aInt已经占据了不同的内存空间,b2.aInt是b1.aInt的一个真正拷贝。
- 相反,对b2.unCA的改变同时改变了b1.unCA,很明显,b2.unCA和b1.unCA是仅仅指向同一个对象的不同引用!从中可以看出,调用Object类中clone()方法产生的效果是: 先在内存中开辟一块和原始对象一样的空间,然后原样拷贝原始对象中的内容。 对基本数据类型,这样的操作是没有问题的,但对非基本类型变量,我们知道它们保存的仅仅是对象的引用,这也导致clone后的非基本类型变量和原始对象中相应的变量指向的是同一个对象。
- 大多时候,这种clone的结果往往不是我们所希望的结果,这种clone也被称为“影子clone”。要想让b2.unCA指向与b2.unCA不同的对象,而且b2.unCA中还要包含b1.unCA中的信息作为初始信息,就要实现深度clone。
- 默认的克隆方法为浅克隆,只克隆对象的非引用类型成员
- 怎么进行深度clone?
- 把上面的例子改成深度clone很简单,需要两个改变:
- 让UnCloneA类也实现和CloneB类一样的clone功能(实现Cloneable接口,重写clone()方法)。
- 在CloneB的clone()方法中加入一句o.unCA = (UnCloneA)unCA.clone();
9.相同(equality)和同一(identity)
与赋值一样,决定一个对象与另外一个对象之间是否相同比我们想象的要更复杂
-
对于字符串变量来说,使用“= =”和“equals()”方法比较字符串时,其比较方法不同。
-
“==”比较两个变量本身的值,即两个对象在内存中的首地址。
-
“equals()”比较字符串中所包含的内容是否相同。
s1 = new String("abc");
s2 = new String("abc");
//那么:
s1==s2 是 false //两个变量的内存地址不一样,也就是说它们指向的对象不一样
s1.equals(s2) 是 true //两个变量的所包含的内容是abc,故相等。
对于非字符串变量来说,“==”和“equals”方法的作用是相同的,都是用来比较其对象在堆内存的首地址,即用来比较两个引用变量是否指向同一个对象。
相同检验的悖论(疑惑)
- 相同意义与特定领域相关
- 面向对象语言允许程序员定义相同检验操作符的含义。
- 相同检验:将另外一个值作为参数传给一个给定值。
- 根类中定义相同检验操作符,并可以在子类中改写。
非传递特性
Parent p Child c
验证p是否等于c由父类方法实现。
验证c是否等于p由子类方法实现。
父类方法得到p既等于c1又等于c2
使用子类方法比较c1和c2也可能返回假。
p.equals(c1);
p.equals(c2);
c1.equals(c2);
END