浅谈Java多态

多态,英语Polymorphism,由希腊语的两个单词polys(意为many, much)和morphē(意为form, shape)组成。从英文单词也可知道polymorphism的意思是“有着多样的形态”。多态表示的是同一个事物具有的不同形态。

引子

在日常使用的语言中,我们随时使用到多态,也就是一字多义。举“洗”(wash)为例,“洗”可以表达多种不同含义的“洗”。洗衣服、洗澡、洗车中的”洗“实际上都不一样,都是不尽相同的动作。但是我们无需专门为了这些情景中的”洗“专门定义一个字或词。例如不必为”洗车“的”洗“而专门造一个字。

通过消除文字之间的耦合,极大地减少了语言的文字数量,提高了语言的简洁性、可读性。消除文字之间的耦合是指自然语言中的文字可以单独拿出来看待,比如”洗“这个字,单独拿出来看我们也知道是什么意思,而不是要从”洗车“整个词理解才能知道”洗“是什么意思。如果字与字之间的耦合度很高,只要我改变了一整段话的某一个字,就有可能要改掉整段话中的所有字了,会牵一发而动全身。比如说”我在室外洗自行车“。如果“洗”和“车”的耦合度很高,例如为不同的车“洗”都有专门的字,有为单车的“洗”,摩托车的“洗”,轿车的“洗”。这样只要我把”自行车“改为”轿车“,就要把自行车的“洗”换为轿车的“洗”了。我们希望不管是洗什么车,都是同一个洗,甚至是不管是洗什么物体,都是同一个“洗”。

而在面向对象的程序设计中,多态就是指同一个接口在不同的导出类中具有不同的行为表现方式,其意义与自然语言中的多态十分相似。

继承与多态

在OOP中,没有继承就没有多态(严格上这里的多态是指动态多态)。
要理解多态,必须结合面向对象中的继承来看,它并不是一个可以单独隔离来看的概念。

继承在程序设计中最主要并不是为了复用父类的代码,组合也可以完成代码的复用,而继承更多是表现出一种类与类之间的关系,这种关系就是子类是父类的一种类型,也就是经常提到的"is-a"关系。而这种关系正是多态存在的前提。
由于导出类复用了父类的接口(具有相同的方法),同一个消息可以发送给这些不同的导出类,使得相同的接口具有不同的行为表现。

借用《Java编程思想》的简单例子

class Instrument {
    public void play(Note n) {
        System.out.println("Instrument.play()");
    }
}

class Wind extends Instrument {
    public void play(Note n) {
        System.out.println("Wind()");
    }
}

class Violin extends Instrument {
    public void play(Note n) {
        System.out.println("Violin()");
    }
}

public class Music {
    public static void tune(Instrument i) {
        i.play(Note.MIDDLE_C);
    }
    
    public static void main(String[] args) {
        Wind flute = new Wind();
        tune(flute);
        
        Violin violin = new Violin();
        tune(violin);
    }
}

在上面的例子中,Wind类和Violin类是Instrument类的导出类,有其独特的play方法实现。Music.tune()方法中调用的是Instrument类的play方法。只需要给tune方法传入Instrument类或其导出类,Java就会根据Instrument类的实际类型使用对应的play方法。同一个play方法,根据对象的类型具有不同的实现。
综合来看,在OOP中,多态的“同一个东西”就是指有同一个父类的同一个方法,而“不同的形态”是说这些子类的方法可以有自己不同的实现。
继承是多态的前提,并且是其实现的条件。

类型解耦

程序设计语言中多态的作用与自然语言的非常相似。
多态的本质在于消除了类型之间的耦合。简而言之,即一个类的代码改变尽量少影响另外一个类。如同上文阐述的自然语言中字与字之间的解耦。不希望一个类的改变导致另外一个类的改变,从而使得整个代码都大幅度的的改动。
使用在上一节中的代码例子,就是希望Music类中的tune方法是一个不受具体乐器而改变的方法,不想为了每一种具体的乐器都特地写一个tune方法,如tune(Violin),tune(Wind)等等,只需要一个tune(Instrument)即可。
通过类型的解耦,使得改变的事物与不变的事物区别开来,不管新增还是减少乐器,都是使用Music.tune方法。
而之所以可以解耦,原因在于将what与how区别出来。Music.tune表示的是what,仅仅是一个抽象的概念,正如“洗”本身是一个抽象的“洗”。而具体的how,则由更细节的子类来表达,正如“洗车”中的“洗”。
通过多态,程序将变得更可扩展,代码也变得更加的简练。

后期绑定

在程序设计
多态是如何做到区别不同的子类型,调用正确的方法呢?

public static void tune(Instrument i) {
    i.play(Note.MIDDLE_C);
}

在tune方法中,它只接受一个Instrument类的引用。但是实际上编译器如何知道这个Instrument引用指向的具体对象呢?是指向Violin对象还是Wind对象呢?实际上Java编译器无法得知,只能是在运行时得知。
实际上这个过程称为绑定,也就是将方法和一个方法主体(对象)关联起来。多态的实现依赖于后期绑定,即在运行时根据对象的类型进行绑定。后期绑定的“后期”与“前期”是一个相对的概念,区别在于是运行前还是运行时。

并非所有的都是多态

并非所有的东西都能是多态。正如在自然语言中,并非所有的字都会有多义。例如“人”,人的本意只能表达人类这种动物,并不会用来表示其他的动物或者事物,除非是后来的引申义。而往往谓词,可以有多义,如上文提及的“洗”,是一个动词。

在程序设计语言中,多态当然也有限制——多态只能是针对类的非static和final方法。换句话说,就是类的static和final方法以及类的域不能多态。private方法实际上是final方法,因此private方法也不能实现多态。
类域的多态并不是“多态”。域表示的是类的状态数据,与自然语言中的体词类似,状态数据不可能有多个,例如boolean类型的成员变量只能是true或者false。如果子类的域和父类的域值发生了改变,那不是多义,而是值发生了变化。
final方法表示的是不可覆写,自然就无法做到每个子类有不同的实现了。
static方法表示的该方法属于类,而非对象。多态的根据具体子类调用不同的方法变得毫无意义,因为向上转型后调用的总会是基类的方法。例如:

class Super {
    public static staticMethod() {
        System.out.println("Super static method");
    }
}

class Sub extends Super {
    public static staticMethod() {
        System.out.println("Sub static method");
    }
}

public class StaticMethodPolymorphismTest {
    public static void main(String[] args) {
        Super super = new Sub();
        super.staticMethod();
    }
}

这段代码的输出例子是"Super Static method"而不是"Sub static method"。原因很简单,static方法是属于类的,所以调用staticMethod方法肯定是调用Super类,而非Sub类。顺带一提,在实践中,不建议使用对象实例来调用static方法,而是直接使用类来调用静态方法,可以减少混淆,如:

Super.staticMethod();

构造器中的多态陷阱

值得一提的是,如果在多态中使用多态,很可能会造成一些意想不到的问题。这是因为在构造器初始化的时候,导出类的数据还没有构造完毕,如果多态的方法使用了导出类的数据,会造成意想不到的问题。
借用《Java编程思想》的简单例子。

class Glyph {
    void draw() {
        System.out.println("Glyph.draw");
    }

    public Glyph() {
        System.out.println("Glyph before draw()");
        draw();
        System.out.println("Glyph after draw()");
    }
}

class RoundGlyph extends Glyph {
    private int radius = 1;

    void draw() {
        System.out.println("RoundGlyph.draw(), radiu = " + radius);
    }

    public RoundGlyph(int radius) {
        this.radius = radius;
        System.out.println("RoundGlyph.RoundGlyph(), radius = " + radius);
    }
}

public class PolyConstructors {
    public static void main(String[] args) {
        new RoundGlyph(5);
    }
}

输出结果是:
Glyph before draw()
RoundGlyph.draw(), radiu = 0
Glyph after draw()
RoundGlyph.RoundGlyph(), radius = 5

在调用RoundGlyph构造器时,会首先隐式地调用Glyph构造器。在Glyph方法中会调用draw方法,而由于后期绑定,Java会调用RoundGlyph的draw方法。RoundGlyph的draw方法会使用到radius成员变量,而由于此时radius成员变量值只是初始化的零值,所以就打印出来0了。
所以多态并不建议在构造器中使用,我们甚至建议在构造器中尽可能简单地初始化对象,唯一安全使用的就是final方法。

结束

从根本上来说,OOP中的多态消除了类型之间的耦合,使得“变”与“不变”区别开来,提高了程序的可扩展性,使得代码更可读和更可维护,是面向对象中的基本特性。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容