多态是数据抽象和继承之后的第三种基本特征. 一句话说,多态分离了做什么和怎么做(再次对埃大爷佩服的五体投地,简直精辟啊). 是从另外一个角度将接口和实现分离开来.
封装通过合并特征和行为来创建新的数据对象,通过私有化隐藏细节,把接口和实现分离开来. 多态则是消除类型之间的耦合关系. 继承是允许将对象视为自己本身的类型或者基类型来处理.
再说说向上转型
把某个对象的引用视为对其基类型的引用的做法被称为向上转型. 为什么老是说向上转型呢,因为在UML图中,一般基类都在子类的上面…
多态是个什么样子
class Instrument{
public void paly(Note m){
System.out.println("Instrument play()");
}
}
class Wind extends Instrument{
public void paly(Note m){
System.out.println("Wind play()");
}
}
public class Music {
public static void tune(Instrument i){
i.paly(Note.C_SHARP);
}
public static void main(String[] args) {
Wind flute = new Wind();
tune(flute);
}
}
这里针对tune方法,传入的类型是Instrument,而实际执行的时候传入的是Wind,不但没有报错,而且还得到了正确的结果. 这是哪位天使大姐发力了吗? 显然不是的. 这就是多态. 也就是说,我们只需要在基类中定义一个方法,在子类中如果该方法被重写了,那么,在调用相关方法的时候,编译器会自动的去调用子类中重写的方法. 这就是传说中的多态.
没有多态的时候如何实现上面的效果
如果没有多态的特性,我们想达到上面的效果也是有办法的.可以使用方法重载. 比如,可以在music类中定义多个tune方法.public static void tune(wind i)
,public static void tune(Stringed i)
,public static void tune(Brass i)
等等等. 这样就可以通过方法重载来实现类似这样的效果. 但是,作为一个正常的人类,不觉得这玩意儿有点反人类吗…
多态的原理
为什么会有多态这种现象,这就要聊到程序的方法运行绑定问题了. 绑定就是将一个方法调用与方法主题关联起来的动作. 像C语言这样的程序设计语言都是用一种叫做前期绑定的方法.也就是说在程序执行之前就要做绑定. 一般都是由编译器和连接器实现的. 也就是说程序在运行的时候已经固定了方法的入参类型了. 如果有错的话,就报错呗.
后来啊 ,改革开放过后(这只是一句口头禅,没有具体时间意义),出现了一种叫做后期绑定或者叫动态绑定又或者叫运行时绑定的技术. 也就是说,存在一种在运行的时候去判断对象类型的机制. 编译器一直不知道对象的类型,但是方法调用机制能找到正确的方法体,并加以调用. 不同语言的实现不一样.
比如,在子类中的那个基类的子集里的方法调用的指针被更改到了子类中重写的方法. 那在执行tune方法的时候,先讲Wind类型向上转型成Instrument类型,再调用的时候,就会直接去调用Wind类型中定义的方法体了. C++就是这样实现的,每个对象都会维护一张虚表,虚表中的虚函数指针默认初始化为自身(黑色箭头所指)然后通过一个virtual关键字来指定该函数为虚函数,如果出现上述这种调用方式的时候,编译器会讲该虚函数指针指向子类中重写的方法(红色箭头所指). (这是个人的理解啊,我比较菜,还没有看到哪个权威的文档上说java是这样实现的…)
java中static和final的方法(private的方法默认都是final的,所以也算),其他的方法都是动态绑定的. 又说到final方法了,final的方法会被关闭动态绑定. 这样的话,编译器在效率上可能会高一点. 但是不好说. 所以这不能作为使用final修饰方法的一个理由. 还是那句话,通过使用final修饰方法来优化程序是不靠谱的. final使用的唯一考量就是设计需要.
多态带来的可扩展性
有了多态机制之后,带来了一个好处,那就是在使用一个类的时候,可以只与基类的接口通信,这样也可以达到我们想要的结果. 那么程序的可扩展性就大大增强了. 多态带来的思想是,将改变的事物(方法实现)和没有改变的事物(方法签名)分离开来.
哪些方法是可以被覆盖的
首先,private的方法肯定是不可以的. private是被封装在类内部的,无法在类的外部调用. 在实例中都没有办法直接调用,那就没有多态可谈了.
其次,final的方法肯定也是不行的,final的方法不允许被重写.
关于private有一个问题,如果在子类中有一个跟基类的某个private方法名字一毛一样的方法,这算什么?什么也不算,不构成重写,只是看作是基类中的一个普通的方法而已.
最后,static的方法,static方法属于类本身. 不存在重写的问题. 也就不存在多态了. 在上面的原理中已经讲的很清楚了,多态发生在实例方法的调用中,对于静态方法是不构成多态的.
简单来说,构成多态需要两个条件.
- 有继承关系
- 子类中重写父类的方法.
构造器和多态
构造器是static的方法,虽然是隐式的.
class Meal{
Meal(){System.out.println("Meal()");}
}
class Bread{
Bread(){System.out.println("Bread()");}
}
class Lettuce{
Lettuce(){System.out.println("Lettuce()");}
}
class Cheese{
Cheese(){System.out.println("Cheese()");}
}
class Lunch extends Meal{
Lunch(){System.out.println("Lunch()");}
}
class ProtableLunch extends Lunch{
public ProtableLunch() {
System.out.println("ProtableLunch()");
}
}
public class Sandwich extends ProtableLunch{
private Bread b = new Bread();
private Cheese c = new Cheese();
private Lettuce l = new Lettuce();
public Sandwich(){System.out.println("Sandwich()");}
public static void main(String[] args) {
new Sandwich();
}
}
一个小例子来完善一下类的初始化过程.
- 调用基类的构造器,这个过程会一直反复递归下去,显示从根基类开始,一层层的往下找.
- 按照声明的顺序初始化成员.
- 调用导出类的构造器主体
继承和清理动作
前面说过,垃圾清理可能不会被调用而且仅仅清理堆上的实例对象. 有时候如果希望更多更及时的清理就需要我们自己书写一个自定义的清理方法. 那么在继承的过程中. 千万不要忘了,如果在子类中重写了这个清理方法的话,在方法内一定要通过super来调用基类的清理方法. 不然,这个方法被覆盖了,基类的清理方法中定义的动作就得不到执行.
还有一点就是,清理过程和初始化过程是相反的. 会先调用自身的清理方法,再调用基类的清理方法.
还有一个问题,如果成员对象中存在于其他一个或者多个对象共享的情况,需要显式的使用引用计数器来跟踪仍旧访问共享对象的对象的数量. 不能随意清理.
来吧,一码解千愁
class Shared{
private int refcount =0;
private static long counter = 0;
private final long id = counter++;
public Shared(){
System.out.println("Ceeating "+this);
}
public void dispose(){
if(--refcount==0){
System.out.println("dispose "+this);
}
}
public void addRef(){
refcount++;
}
public String toString(){
return "shared "+id;
}
}
class Composing{
private Shared shared;
private static long counter = 0;
private final long id = counter++;
public Composing(Shared shared){
System.out.println("Creating "+ this);
this.shared = shared;
this.shared.addRef();
}
protected void disposed(){
System.out.println("disposing "+this);
shared.dispose();
}
public String toString(){
return "Composing "+id;
}
}
public class ReferenceCounting {
public static void main(String[] args) {
Shared shared = new Shared();
Composing[] composing = {new Composing(shared),new Composing(shared),new Composing(shared),new Composing(shared)};
for(Composing c:composing){
c.disposed();
}
}
}/*out:
Ceeating shared 0
Creating Composing 0
Creating Composing 1
Creating Composing 2
Creating Composing 3
disposing Composing 0
disposing Composing 1
disposing Composing 2
disposing Composing 3
dispose shared 0
*/
这个例子里使用了引用计数器. 只有当引用计数器归零的时候,才清除shared对象.
构造器内部的多态方法的行为
按道理来说,这种代码是不应该存在的. 构造器用来创建和初始化对象. 而多态的行为是属于实例方法的. 也就是说多态的正常应用场景应该是在对象构建完成之后. 那么,如果在构造器中去调用一个实例方法,在构造的工程中应用多态会发生什么呢?
class Glyph{
void draw(){
System.out.println("Glyph.draw()");
}
Glyph(){
System.out.println("Glyph start");
System.out.println("Glyph() before draw()");
draw();
System.out.println("Glyph() after draw()");
System.out.println("Glyph end");
}
}
class RoundGlyph extends Glyph{
private int radius =1;
public RoundGlyph(int r) {
System.out.println("RoundGlyph start");
radius = r;
System.out.println("RoundGlyph.RoundGlyph().radius = "+ radius);
System.out.println("RoundGlyph end");
}
void draw(){
System.out.println("RoundGlyph.draw().radius = "+radius);
}
}
public class PolyConstructors {
public static void main(String[] args) {
new RoundGlyph(5);
}
}/*out:
Glyph start
Glyph() before draw()
RoundGlyph.draw().radius = 0
Glyph() after draw()
Glyph end
RoundGlyph start
RoundGlyph.RoundGlyph().radius = 5
RoundGlyph end
*/
首先在基类的构造方法中调用了draw()方法,那么在new RoundGlyph实例的时候,RoundGlyph的构造器会先执行基类的构造器. 所以Glyph()被调用了.而在Glyph()里调用的drwa()方法,那么由于多态,会执行RoundGlyph类中的draw,而这个时候,RoundGlyph实例并没有构造完成. 所以得到的值与期望值不一样. 那为什么这里会是0呢?
记得之前在初始化那一节的时候说过初始化的顺序:
- 分配内存空间并初始化为二进制的0
- 调用基类的构造器
- 按照声明顺序调用成员的初始化方法
- 调用子类的构造器.
那么,在Glyph()被调用的时候,第一步的初始化已经完成了. 所以,我们看到的是个0;这么做有个好处,在任何方法(包括构造方法)执行前,所有东西都已经被初始化为”空”了. 至少保证是有初始化状态的,如果出了问题,无论是报异常还是结果出错都相对来说都比较方便定位问题.
上面这个例子可能很多人都不会想到,因为大家一直都默认一条规定:构造器中使用尽可能简单的方法完成初始化,让对象进入正常状态就可以了. 如果有可能,避免在构造器中调用其他方法.
协变返回类型
之前说的多态都只解决了一个问题,对于子类中被覆盖的基类方法,入参类型不同可以构成多态. 那多态中的方法的返回值会有什么改变吗?
class Grain{
public String toString(){
return "Grain";
}
}
class Wheat extends Grain{
public String toString(){
return "Wheat";
}
}
class Mill{
Grain process(){
return new Grain();
}
}
class WheatMill extends Mill{
Wheat process(){
return new Wheat();
}
}
public class ConvarianReturn {
public static void main(String[] args) {
Mill m = new Mill();
Grain g = new Grain();
System.out.println(g);
m = new WheatMill();
g = m.process();
System.out.println(g);
}
}/*out:
Grain
Wheat
*/
在java SE1.5之前,重写基类方法的时候 返回值必须跟基类中方法的一致. 1.5之后,放宽了这个限制. 返回值可以是基类中被覆盖方法的返回值的子类. 也就是说可以是更具体的类型了.
使用继承进行设计
之前已经说过,在设计的时候,组合的方式是首选的考量. 组合更加灵活,而且还有多态的特性可以为它动态的选择类型.而继承必须在编译是就知道确切的类型. 一般来说的准则是用继承来表达行为见的差异,并且用字段来表达状态上的变化.
状态模式
class Leg{
public void walk(){}
}
class SoberLeg extends Leg{
public void walk(){
System.out.println("walk in line");
}
}
class DrunkenLeg extends Leg{
public void walk(){
System.out.println("zzz...");
}
}
class Person{
private Leg legs= new SoberLeg();
public void change() {legs = new DrunkenLeg();}
public void walkWithLegs(){legs.walk();}
}
public class PersonTest {
public static void main(String[] args) {
Person p = new Person();
p.walkWithLegs();
p.change();
p.walkWithLegs();
}
}
这个例子的设计不太合理,但是能表示那个意思. 就是使用组合的时候,完全可以在运行时,把一个对象的引用指向另外一个对象,然后时其行为发生改变.
纯继承,扩展和向下转型
最理想的继承方式应该是,子类和基类有一毛一样的接口. 也就是说是一种纯粹的”is-a”的关系. 也可以认为这是一种纯替代.
在这种状态下多态可以处理一切的事情.
但是理想永远是人们的一种美好的可望不可及的期待,java中继承的关键字是extends,这也说明,继承更多时候需要的是扩展基类接口. 也就是说子类是基类的一个超集. 这种关系可以说是一种 “is-like-a”的关系. 在这种关系里,向上转型的话,那子类中扩展的方法是无法使用的.同时带来了向下转型的风险. 使用”()”的强转方式依旧可以向下转型. java在运行时会做类型检查,如果不是我们期望的类型,会报ClassCastException.