2 大话设计模式 | 【Java溢彩加强版】 业的吗?” “是的,今年大四。不过老实说,在学校里真的没学到什么东西。所谓‘公司一 日,校园一年。’来这一定会比学校学的东西要多。” “哈,夸张了,应该是‘公司一日抵自学一月。’在实践当中,自然会学得快一 些。不过话说回来,一些基本的东西还是要知道的。Java语言学过吧?对面向对象又了解 多少?” “Java是学过的,什么变量、常量、判断、循环,我都知道。面向对象,好像也学 过的。什么类呀、方法呀的,太久了,当时就为了应付考试了,具体如何用,根本不记 得。你要不给我讲讲吧。” “啊,都不记得了,不就等于白学了吗?不过没关系,今天我正好有空,我们争取 来个快速入门吧。” “太好了,遥哥,不对,还是得叫蔡老师,先谢谢了。” 0.2 类与实例 “先问问你,对象是什么?类是什么?”小菜问道。 “准确定义不知道。类大概就是对东西分类的意思。”史熙答得很勉强。 “啊,看来你是实实在在的菜鸟呀。一切事物皆为对象,即所有的东西都是对象, 对象就是可以看到、感觉到、听到、触摸到、尝到或闻到的东西。准确地说,对象是 一个自包含的实体,用一组可识别的特性和行为来标识。面向对象编程,英文叫Object- Oriented Programming,其实就是针对对象来进行编程的意思。” gray elephant with white background gray elephant with white background “至于类,待会儿再讲,先从最简单的开始,你 可以用Java编写一个小程序,最终将实现一个‘动物 运动会’的软件小例子。” “动物运动会?有意思。” “首先实现这样一个功能,用Java实现一声猫 叫,我们暂时无法模拟出真实声音,那就在控制台显 喵 棕色编织篮中的白色和灰色猫 3 第0章 | 楔子 培训实习生—面向对象基础 示叫声‘喵’就可以了。” “这个非常简单。” “好了,是这个意思吗?” 史熙问道。 “是,程序是编出来了,现在的问题就是,如果我们需要在另一个按钮中来让小猫 叫一声,或者需要小猫多叫几声,怎么办?” “那就多写几个‘System.out.println("喵");’呀。” “那不就重复了吗?可不可以想个办法?” “我知道你的意思,写个函数就可以解决了。其他需要猫叫的地方都可以调用” “很好,现在问题是,万一别的地方,我指程序的其他地方也需要猫叫shout(),如 何处理?” “这个我好像学过的,在shout()方法前面加一个public,别的场合就可以访问了。” “对,没错,这样是可以达到访问的目的了,但是你觉得这个shout(猫叫)放在这个 Test.java的代码中合适吗?这就好比,居委会的公用电视放在你家,而别人家都没有,于 是街坊邻居都来你家看电视。你喜欢这样吗?” “这样好呀,邻居关系好了。不过这确实不是办法,公用电视应该放在居委会。” “所以说,这猫叫的函数应该放在一个更合适的地方,这就是‘类’。类就是具有 相同的属性和功能的对象的抽象的集合,我们来看代码。” “这里‘class’是表示定义类的关键字,‘Cat’就是类的名称,‘shout’就是类的 方法。” “哈,定义类这玩意还是很简单的嘛。” “这里有两点要注意 (1)类名称首字母记着要大写。多个单词则各个首字母大写; (2)对外公开的方法需要用‘public’修饰符。” “明白,那怎么应用这个类呢?” “很简单,只要将类实例化一下就可以了。” “什么叫实例化?” “实例,就是一个真实的对象。比如我们都是‘人’,而你和我其实就是‘人’类 的实例了。而实例化就是创建对象的过程,使用new关键字来创建。” “注意,‘Cat cat = new Cat();’其实做了两件事。” “Cat实例化后,等同于出生了一只小猫cat,此时就可以让小猫cat.shout()了。在任 何需要小猫叫的地方都可以实例化它。” “明白,这下调用它确实就方便很多了。” 0.3 构造方法 “下面,我们希望出生的小猫应该有个姓名,比如叫‘咪咪’,当咪咪叫的时候, 最好是能说‘我的名字叫咪咪,喵’。此时就需要考虑用构造方法。” “构造方法?这是做什么用的?” “构造方法,又叫构造函数,其实就是对类 进行初始化。构造方法与类同名,无返回值,也 不需要void,在new的时候调用。” “那就是说,在类创建时,就是调用构造方 法的时候了?” “是呀,在‘Cat cat=new Cat();’中,new 我叫咪咪 后面的Cat()其实就是构造方法。” “不对呀,在类当中没有写过构造方法Cat(),怎么可以调用呢?” “问得好,实际情况是这样,所有类都有构造方法,如果你不编码则系统默认生成 空的构造方法,若你有定义的构造方法,那么默认的构造方法就会失效了。也就是说, 由于你没有在Cat类中定义过构造方法,所以Java语言会生成一个空的构造方法Cat()。当 然,这个空的方法什么也不做,只是为了让你能顺利地实例化而已。” “那不是很好吗?我们还需要构造方法做什么呢?” “刚才不是说过吗,构造方法是为了对类进行初始化。比如我们希望每个小猫一诞 生就有姓名,那么就应该写一个有参数的构造方法。” “这样一来,我们在客户端要生成小猫时,就必须要给小猫起名字了。” 结果显示: 我的名字叫咪咪 喵 0.4 方法重载 “但是,遥哥,如果我事先没有起好小猫的名字,难道这个实例就创建不了了 吗?” 史熙又有疑问。 “是的,有些父母刚生下孩子时,姓名没有起好也是很正常的事。就目前的代码,你如 果写‘Cat cat = new Cat();’是会直接报‘Cat方法没有采用0个参数的重载’的错误,原因就 是必须要给小猫起名字。如果当真需要不起名字也要生出小猫来,可以用‘方法重载’。” “方法重载?好像也学过,具体如何说?” “方法重载提供了创建同名的多个方法的能力,但这些方法需使用不同的参数类 型。注意并不是只有构造方法可以重载,普通方法也是可以重载的。” “哦,这样的话,如果写‘Cat cat = new Cat();’的话,就不会报错了。而猫叫时会是 ‘我的名字叫无名 喵’。” “对的,注意方法重载时,两个方法必须要方法名相同,但参数类型或个数必须要 有所不同,否则重载就没有意义了。你觉得方法重载的好处是什么?”小菜问道。 “哈,我想应该是方法重载可在不改变原方法的基础上,新增功能。” “说得很好,方法重载算是提供了函数可扩展的能力。比如刚才这个例子,有的小 猫起好名字了,就用带string参数的构造方法,有的没有名字,就用不带参数的,这样就 达到了扩展的目的。” “如果我需要分清楚猫的姓和名,还可以再重载一个public Cat(string firstName, string lastName),对吧?” “对的。非常好。下面,我们觉得小猫叫的次数太少,希望是我让它叫几声,它就 叫几声,如何做?” “那是不是在构造方法里再加一个叫的次数?” “那样当然是可以,但叫几声并不是必须要实例化的时候就声明的,我们可以之后 再规定叫几声,所以这时应该考虑用‘属性’。” 0.5 属性与修饰符 “属性是一个方法或一对方法,即属性适合于以字段的方式使用方法调用的场合。 这里还需要解释一下字段的意思,字段是存储类要满足其设计所需要的数据,字段是与 类相关的变量。比如刚才的Cat类中的‘private string name = "";’name其实就是一 个字段,它通常是私有的类变量。那么属性是什么样呢?我们现在增加一个猫叫次数 ShoutNum的属性。” “刚才没有强调public和private的区别,它们都是修饰符,public表示它所修饰的类 成员可以允许其他任何类来访问,俗称公有的。而private表示只允许同一个类中的成员 访问,其他类包括它的子类无法访问,俗称私有的。如果在类中的成员没有加修饰符, 则被认为是private的。修饰符还有其他三个,以后再讲。通常字段都是private,即私有 的变量,而属性都是public,即公有的变量。那么在这里shoutNum就是私有的字段,而 ShoutNum就是公有的对外属性。由于是对外的,所以属性的名称一般首字母大写,而字 段则一般首字母小写或前加‘_’。” “属性的get和set是什么意思?” “属性有两个方法get和set。get返回与声明的属性相同的数据类型,表示的意思是调 用时可以得到内部字段的值或引用;set有一个参数,用关键字value表示,它的作用是调 用属性时可以给内部的字段或引用赋值。” “那又何必呢?我把字段的修饰符改为public,不就可以做到对变量的既读又写 了吗?” “是的,如果仅仅是可读可写,那与声明了public的字段没什么区别。但是对于对外 界公开的数据,我们通常希望能做更多的控制,这就好像我们的房子,我们并不希望房 子是全透明的,那样你在家里的所有活动全部都被看得清清楚楚,毫无隐私可言。通常 我们的房子有门有窗,但更多的是不透明的墙。这门和窗其实就是public,而房子内的东 西,其实就是private。而对于这个房子来说,门窗是可以控制的,我们并不是让所有的人 都可以从门随意进出,也不希望蚊子苍蝇来回出入。这就是属性的作用了,如果你把字 段声明为public,那就意味着不设防的门窗,任何时候,调用者都可以读取或写入,这是 非常糟糕的一件事。如果把对外的数据写成属性,那情况就会好很多。” “我明白了,这就好比给窗子安装了纱窗,只让阳光和空气进入,而蚊子苍蝇就隔 离。多了层控制就多了层保护。” “说得很好。我们还没有做完,由于有了‘叫声次数’的属性,于是我们的shout方 法就需要改进了。” “此时调用的时候只需要给属性赋值就可以了。” 结果显示: 我的名字叫咪咪 喵 喵 喵 喵 喵 “如果我们不给属性赋值,小猫会叫‘喵’吗?” “当然会,应该是三声吧,因为字段shoutNum的初始值是3。” “很好。另外需要强调的是,变量私有的叫字段,公有的是属性,那么对于方法而 言,同样也就有私有方法和公有方法了,一般不需要对外界公开的方法都应该设置其修 饰符为private(私有)。这才有利于‘封装’” 0.6 封装 “现在我们可以讲面向对象的三大特性之一‘封装’了。每个对象都包含它能进行 操作所需要的所有信息,这个特性称为封装,因此对象不必依赖其他对象来完成自己的 操作。这样方法和属性包装在类中,通过类的实例来实现。” “是不是刚才提炼出Cat类,其实就是在做封装?” “是呀,封装有很多好处。第一,良好的封装能够减少耦合,至少我们让Cat和 Form1的耦合分离了。第二,类内部的实现可以自由地修改,这也是显而易见的,我们已 经对Cat做了很大的改动。第三,类具有清晰的对外接口,这其实指的就是定义为public 的ShoutNum属性和shout方法。” “封装的好处很好理解,比如刚才 举的例子。我们的房子就是一个类的实 例,室内的装饰与摆设只能被室内的居 住者欣赏与使用,如果没有四面墙的遮 挡,室内的所有活动在外人面前一览无 遗。由于有了封装,房屋内的所有摆设 都可以随意地改变而不用影响他人。然 而,如果没有门窗,一个包裹得严严实实的黑箱子,即使它的空间再宽阔,也没有实用 价值。房屋的门窗,就是封装对象暴露在外的属性和方法,专门供人进出,以及流通空 气、带来阳光。” “现在我需要增加一个狗叫的功能,就是加一个按钮‘狗叫’,单击后会弹出‘我 的名字叫XX 汪 汪 汪’如何做?” “那简单呀,仿造Cat加一个Dog类。然后再用类似代码调用就好了。” 房子, 水池, 室内设计, 结构, 3D, 设计, 风格, 极简主义者, 内部的, 模块化, 住房, 预制 结果显示: 我的名字叫旺财 汪 汪 汪 汪 汪 汪 汪 汪 “这下就OK了,小狗旺财也会叫了。” “很好,但你有没有发现,Cat和Dog有非常类似的代码?” “是呀,90%的代码是一样的,不过这些代码都是必需的,也没什么办法去 除呀。” “当然可以想办法,代码有大量重复不会是什么好事情。我们要用到面向对象的第 二大特性‘继承’。” 0.7 继承 “我们还是先离开软件编程, 来想想我们的动物常识,猫和狗都 是什么?”小菜问道。 “都是给人添麻烦的东西。 除了吃喝拉撒睡,什么也不干的家 伙。”史熙调皮地答道。 “拜托,正经一些。猫和狗都 是动物,准确地说,他们都是哺乳 动物。哺乳动物有什么特征?” “哦,这个小时候学过,哺乳动物是胎生、哺乳、恒温的动物。” “OK,因为猫和狗是哺乳动物,所以猫和狗就同样具备胎生、哺乳、恒温的特征。 所以我们可以这样说,由于猫和狗是哺乳动物,所以猫和狗与哺乳动物是继承关系。” “哦,原来继承就是这个意思。” “是的,回到编程上,对象的继承代表了一种‘is-a’的关系,如果两个对象A和 B,可以描述为‘B是A’,则表明B可以继承A。‘猫是哺乳动物’,就说明了猫与哺 乳动物之间是继承与被继承的关系。实际上,继承者还可以理解为是对被继承者的特殊 宠物, 可爱的, 猫, 狗, 可爱的墙纸, 粉色的, 动物, 朋友们, 粉红色的动物, 粉红色的墙纸 化,因为它除了具备被继承者的特性外,还具备自己独有的个性。例如,猫就可能拥有 抓老鼠、爬树等‘哺乳动物’对象所不具备的属性。因而在继承关系中,继承者可以完 全替换被继承者,反之则不成立。所以,我们在描述继承的‘is-a’关系时,是不能相互 颠倒的。说‘哺乳动物是猫’显然有些莫名其妙。继承定义了类如何相互关联,共享特 性。继承的工作方式是,定义父类和子类,或叫作基类和派生类,其中子类继承父类的 所有特性。子类不但继承了父类的所有特性,还可以定义新的特性。” “‘is-a’这个比较好理解。” “学习继承最好是记住三句话,如果子类继承于父类,第一,子类拥有父类非 private的属性和功能;第二,子类具有自己的属性和功能,即子类可以扩展父类没有的属 性和功能;第三,子类还可以以自己的方式实现父类的功能(方法重写)。” “这里有些不理解,什么叫非private,难道除了public还有别的修饰符吗?” “当然有,刚才讲了private和public,现在再讲一个protected修饰符。protected表示继 承时子类可以对基类有完全访问权,也就是说,用protected修饰的类成员,对子类公开, 但不对其他类公开。所以子类继承于父类,则子类就拥有了父类的除private外的属性和功 能,注意除这三个修饰符外还有两个,由于和目前所讲的内容无关,就留给你自己去查 MSDN看吧。” “那方法重写是什么意思?” “这个留到后面讲多态的时候去说,现在我们来看看怎么做。对比观察Cat和 Dog类。” “我们会发现大部分代码都是相同的,所以我们现在建立一个父类,动物Animal 类,显然猫和狗都是动物。我们把相同的代码尽量放到动物类中。” “然后我们需要写Cat和Dog的代码。让它们继 承Animal。这样重复的部分都可以不用写了,不过在 Java中,子类从它的父类中继承的成员有方法、属 性等,但对于构造方法,有一些特殊,它不能被继 承,只能被调用。对于调用父类的成员,可以用base 关键字。” “此时客户端的代码完全一样,没有受到影响,但重复的代码却因此减少了。”小 菜说。 “差不太多嘛,子类还是有些复杂,没简单到哪去?”史熙说道。 “如果现在需要加牛、羊、猪等多个类似的类,按你以前的写法就需要再复制三 遍,也就是有五个类。如果我们需要改动起始的叫声次数,也就是让shoutNum由3改为 5,你需要改几个类?” “我懂你的意思了,那需要改5个类,现在有了Animal,就只要改一个类就行了,继 承可以使得重复减少。” “狗拿耗子,那是多管闲事了。看来不能让狗继承猫,那样很容易造成麻烦。” “继承是有缺点的,那就是父类变,则子类不得不变。让狗去继承于猫,显然不是 什么好的设计。另外,继承会破坏包装,父类实现细节暴露给子类,这其实是增大了两 个类之间的耦合性。” “什么叫耦合性?” “严格定义你自己去查吧,简单理解就是藕断丝连,两个类尽管分开,但如果关系 密切,一方的变化都会影响到另一方,这就是耦合性高的表现,继承显然是一种类与类 之间强耦合的关系。” “明白,你说了这么多,那什么时候用继承才是合理的呢?” “我最先不是说过吗?当两个类之间具备‘is-a’的关系时,就可以考虑用继承了, 因为这表示一个类是另一个类的特殊种类,而当两个类之间是‘has-a’的关系时,表示 某个角色具有某一项责任,此时不适合用继承。比如人有两只手,手不能继承人;再比 如飞机场有飞机,飞机也不能去继承飞机场。” “OK,也就是说,只有合理的应用继承才能发挥好的作用。” 0.8 多态 “下面我们再来增加需求,如果我们要举办一个动物运动会,来参加的有各种各样 的动物,其中有一项是‘叫声比赛’。界面就是放两个按钮,一个是‘动物报名’,就 是确定动物的种类和报名的顺序;另一个是‘叫声比赛’,就是报名的动物挨个地叫出 声音来比赛。注意来报名的都是什么动物,我们并不知道。可能是猫、可能是狗,也可 能是其他的动物,当然它们都需要会叫才行。” “有点复杂,我除了会加两个按钮外,不知道如何做了。” “先分析一下,来报名的都是动物,参加叫声比赛必须会叫。这说明什么?” “说明都有叫的方法,哦,也就是Animal类中有shout方法。” “是呀,所谓的‘动物报名’,其实就是建立一个动物对象数组,让不同的动物对 象加入其中。所谓的‘叫声比赛’,其实就是遍历这个数组来让动物们‘shout()’就可 以了。” “哦,我大概明白你的意思了,那看看我写的代码,我觉得应该是类似的样子,但 问题是我们不知道是哪个动物来报名,最终叫的时候到底是猫在叫还是狗在叫呢?” “是呀,就之前讲到的知识,是不足以解决这个问题的,所以我们引入面向对象的 第三大特性—多态。” “啊,多态,多态是我大学里听得很多,但一直都不懂的东西,实在不明白它是什么意思。” 同样是鸟,同样展开翅膀的动作,但老鹰、鸵鸟和企鹅之间,是完全不同的作用。 老鹰展开翅膀用来更高更远地飞翔,鸵鸟用来更快更稳地奔跑,而企鹅则是更急更流畅 地游泳。这就是生物多态性表现。在面向对象中,“多态表示不同的对象可以执行相同 的动作,但要通过它们自己的实现代码来执行。 看定义显然不太明白,我再来举个例子。我们的国粹‘京剧’以前都是子承父业, 代代相传的艺术。假设有这样一对父子,父亲是非常有名的京剧艺术家,儿子长大成 人,模仿父亲的戏也惟妙惟肖。有一天,父亲突然发高烧,上不了台表演,而票都早就 卖出,退票显然会大大影响声誉。怎么办呢?由于京戏都是需要化妆才可以上台的,于 是就决定让儿子代父亲上台表演。” “化妆后谁认识谁呀,只要唱得好就可以糊弄过去了。” “是呀,这里面有几点注意,第一,子类以父类的身份出现,儿子代表老子表演, 化妆后就是以父亲身份出现了。第二,子类在工作时以自己的方式来实现,儿子模仿得 再好,那也是模仿,儿子只能用自己理解的表现方式去模仿父亲的作品;第三,子类以 父类的身份出现时,子类特有的属性和方法不可以使用,儿子经过多年学习,其实已经 有了自己的创作,自己的绝活,但在此时,代表父亲表演时,绝活是不能表现出来的。 当然,如果父亲还有别的儿子会表演,也可以在此时代表父亲上场,道理也是一样的。 这就是多态。” “听听好像都懂,怎么用呢?” “是呀,怎么用呢,我们还需要了解一些概念,方 法重写。子类可以选择使用override关键字,将父类实现 替换为它自己的实现,这就是方法重写Override,或者 叫作方法覆写。我们来看一下例子。” “由于Cat和Dog都有shout的方法,只是叫的声音 不同,所以我们可以让Animal有一个shout的方法,然后 Cat和Dog去重写这个shout,用的时候,就可以用猫或狗 代替Animal叫唤,来达到多态的目的。” “再回到你刚才写的客户端代码上。” 结果显示,先单击“动物报名”,然后“叫声比赛”,将有五个对话列出。 我的名字叫小花 喵 喵 喵 我的名字叫阿毛 汪 汪 汪 我的名字叫小黑 汪 汪 汪 我的名字叫娇娇 喵 喵 喵 我的名字叫咪咪 喵 喵 喵 “我明白了,Animal相当于京剧表演的老爸,Cat和Dog相当于儿子,儿子代表父亲 表演shout,但Cat叫出来的是‘喵’,Dog叫出来的是‘汪’,这就是所谓的不同的对象 可以执行相同的动作,但要通过它们自己的实现代码来执行。” “说得好,是这个意思,不过一定要注意了,这个对象的声明必须是父类,而不 是子类,实例化的对象是子类,这才能实现多态。多态的原理是当方法被调用时,无论 对象是否被转换为其父类,都只有位于对象继承链最末端的方法实现会被调用。也就是 说,虚方法是按照其运行时类型而非编译时类型进行动态绑定调用的。[AMNFP]” “不过老实说,即使这样,我也还是不太理解这样做有多大的好处。多态被称为面 向对象三大特性,我感觉不到它有和封装、继承同样的作用。” “慢慢来,要深刻理解并会合理利用多态,不去研究设计模式是很难做到的。也可 以反过来说,没有学过设计模式,那么对多态乃至对面向对象的理解多半都是肤浅和片 面的。我相信会有那种天才,可以听一知十,刚学的东西就可以灵活自如地应用,甚至 要造汽车,他都能再去发明轮子。但对于绝大多数程序员,还是需要踏踏实实地学习基 本的东西,并在不断的实践中成长,最终成为高手。” “蔡老师,受教了。下面我们做什么?” 0.9 重构 “现在又来了小牛和小羊来报名,需要参加‘叫声比赛’,你如何做?” “这个简单了,我现在再实现牛Cattle和羊Sheep的类,让它们都继承Animal就可 以了。” “等等,你有没有发现,猫狗牛羊四个类,除了构造方法之外,还有重复的地方?” “是呀,我发现了,shout里除了四种动物叫的声音不同外,几乎没有任何差异。” “这有什么坏处?” “重复呀,如果你有需求说,把‘我的名字叫XXX’改成‘我叫XXX’,我就得更 改四个类的代码了。” “非常好,所以这里有重复,我们还是应该要改造它。” “我先把重复的这个shout的方法体放到Animal类中。” “这样如何能行,动物叫什么声音呢?叫‘喵’?叫‘汪’?都不行,动物是个抽 象的概念,它是不会有叫的声音的。” “别急,我们把叫的声音部分改成另一个方法getShoutSound不就行了!” “此时的子类就极其简单了。除了叫声和构造方法不同,所有的重复都转移到了父 类,真是漂亮之极。” “有点疑问,这样改动,子类,比如Cat就没有Shout方法了,外面如何调 用呢?” “唉,你把继承的基本忘记了?继承的第一条是什么?” “哈,是子类拥有所有父类非private的属性和方法。对的对的,由于子类继承父 类,所以是public的Shout方法是一定可以为所有子类所用的。”史熙高兴地说,“我渐 渐能感受到面向对象编程的魅力了,的确是非常简洁。由于不重复,所以需求的更改都 不会影响到其他类。” “这里其实就是在用一个设计模式,叫模板方法。(详见第10章)” “啊,原来就是设计模式呀,Very Good,太棒了,哈,我竟然学会了设计 模式。” “疯了?发什么神经呀。”小菜同样微笑道,“这才是知道了皮毛,得意什么,还 早着呢。” 0.10 抽象类 “我们再来观察,你会发现, Animal类其实根本就不可能实例化的, 你想呀,说一只猫长什么样,可以想 象,说new Animal(); 即实例化一个动 物。一个动物长什么样?” “不知道,动物是一个抽象的名 词,没有具体对象与之对应。” “是呀,所以我们完全可以考虑把实例化没有任何意义的父类,改成抽象类,同样 地,对于Animal类的getShoutSound方法,其实方法体没有任何意义,所以可以将修饰符 改为abstract,使之成为抽象方法。Java允许把类和方法声明为abstract,即抽象类和抽象 方法。” “这样一来,Animal就成了抽象类。抽象类需要注意几点,第一,抽象类不能实例 野生动物, 哺乳动物, 猴, 灵长类动物, 猿, 婴儿, 猩猩, 婆罗洲, 动物, 荒野, 自然, 可爱的 动物长什么样? 化,刚才就说过,‘动物’实例化是没有意义的;第二,抽象方法是必须被子类重写的 方法,不重写的话,它的存在又有什么意义呢?其实抽象方法可以被看成是没有实现体 的虚方法;第三,如果类中包含抽象方法,那么类就必须定义为抽象类,不论是否还包 含其他一般方法。” “这么说的话,一开始就可以把Animal类设成抽象类了,根本没有必要存在虚方法 的父类。是这样吧?”史熙问道。 “的确是这样,我们应该考虑让抽象类拥有尽可能多的共同代码,拥有尽可能少的 数据[J&DP]。” “那到底什么时候应该用抽象类呢?” “抽象类通常代表一个抽象概念,它提供一个继承的出发点,当设计一个新的抽象 类时,一定是用来继承的,所以,在一个以继承关系形成的等级结构里面,树叶节点应 当是具体类,而树枝节点均应当是抽象类[J&DP]。也就是说,具体类不是用来继承的。我 们作为编程设计者,应该要努力做到这一点。比如,若猫、狗、牛、羊是最后一级,那 么它们就是具体类,但如果还有更下面一级的金丝猫继承于猫、哈巴狗继承于狗,就需 要考虑把猫和狗改成抽象类了,当然这也是需要具体情况具体分析的。” “这个应该可以理解。” “OK,我们继续下面的需求实现。” 0.11 接口 “在动物运动会里还有一项非常特殊的比赛是为了给有特异功能的动物展示其特殊 才能的。” “哈,有特异功能?有意思。不知是什么动物?” “多的是呀,可以来比赛的比如有机器猫叮、石猴孙悟空、肥猪猪八戒,再比如 蜘蛛人、蝙蝠侠等。” “啊,这都是什么动物呀,根本就是人们虚构之物。” “让猫狗比赛叫声难道就不是虚构?你当它们会愿意相互攀比?其实我的目的只 是为了让两个动物尽量不相干而已。现在叮会从肚皮的口袋里变出东西,而孙悟空可 以拔根毫毛变出东西,且有七十二般变化,八戒有三十六般变化。它们各属于猫、猴、 猪,现在需要让它们比赛谁变东西的本领大。你来分析一下如何做?” “‘变出东西’应该是叮、孙悟空、猪八戒的行为方法,要想用多态,就得让 猫、猴、猪有‘变出东西’的能力,而为了更具有普遍意义,干脆让动物具有‘变出东 西’的行为,这样就可以使用多态了。” “哈哈,史熙呀,你犯了几乎所有学面向对象的人都会犯的错误,‘变出东西’ 它是动物的方法吗?如果是,那是不是所有的动物都必须具备‘变出东西’的能 力呢?” “这个,确实不是,这其实只是三个特殊动物具备的方法。那应该如何做?” “这时候我们就需要新的知识了,那就是接口interface。通常我们理解的接口可能更 多的是像电脑、音响等设备的硬件接口,比如用来传输电力、音视频、数据等接线的插 口。而今天我们要提的,是面向对象编程里的接口概念。” “猴子的类Monkey和孙悟空的类StoneMonkey与上面非常类似,在此省略。此时我 们的客户端,可以加一个‘变出东西’按钮,并实现下面的代码。” 结果显示: 我的名字叫叮 喵,喵,喵,我有万能 的口袋,我可变出各种各样的东西! 我的名字叫孙悟空 俺老孙来也,俺老 孙来也,俺老孙来也,我会七十二变, 可变出各种各样的东西! “哦,我明白了,其实这和抽象类是很类似的,由于我现在要让两个完全不相干的 对象,叮和孙悟空来做同样的事情‘变出东西’,所以我不得不让他们去实现做这件 ‘变出东西’的接口,这样的话,当我调用接口的‘变出东西’的方法时,程序就会根 据我实现接口的对象来做出反应,如果是叮,就是用万能口袋,如果是孙悟空,就是 七十二变,利用了多态性完成了两个不同的对象本不可能完成的任务。” “说得非常好,同样是飞,鸟用翅膀飞,飞机用引擎加机翼飞,而超人呢?举起两 手,握紧拳头就能飞,它们是完全不同的对象,但是,如果硬要把它们放在一起的话, 用一个飞行行为的接口,比如命名为IFly的接口来处理就是非常好的办法。” 0.12 集合 “下面我们再来看看,客户端的代码中,‘动物报名’用的是Animal类的对象数 组,你设置了数组的长度为5,也就是说,最多只能有五个动物可以报名参加‘叫声比 赛’,多了就不行了。这显然是非常不合理的,应该考虑改进。你能说说数组的优缺 点吗?” “数组的优点,比如说数组在内存中连续存储,因此可以快速而容易地从头到尾遍 历元素,可以快速修改元素等。缺点嘛,应该是创建时必须要指定数组变量的大小,还 有在两个元素之间添加元素也比较困难。” “说得不错,的确是这样,这就可能使得数组长度设置过大,造成内存空间浪费, 长度设置过小造成溢出。所以Java提供了用于数据存储和检索的专用类,这些类统称集 合。这些类提供对堆栈、队列、列表和哈希表的支持。大多数集合类实现相同的接口。 我们现在介绍当中最常用的一种,ArrayList。” “集合?它和数组有什么区别?” “别急,首先ArrayList是程序包java.util.ArrayList下的一部分,它是使用大小可按需 动态增加的数组实现Collection接口。” “哦,没学接口前不太懂,现在知道了,你的意思是说,Collection接口定义了很多 集合用的方法,ArrayList对这些方法做了具体的实现?” “对的,ArrayList的容量是ArrayList可以保存的元素数。ArrayList的默认初始容量为 0。随着元素添加到ArrayList中,容量会根据需要通过重新分配自动增加。使用整数索引 可以访问此集合中的元素。此集合中的索引从零开始。” “是不是可以这样理解,数组的容量是固定的,而 ArrayList 的容量可根据需要自动 扩充?” “是的,由于实现了Collection,所以ArrayList 提供添加、插入或移除某一范围元素 的方法。下面我们来看看如何做。” “如果有动物报完名后,由于某种原因(如政治、宗教、兴奋剂、健康等)放弃比 赛,此时应该需要将其从名单中移除。例如,在报了名后,两只小狗需要退出比赛。我 们查了一下它们的报名索引次序为1和2(从0开始计算),所以可以应用集合的remove方 法,它的作用是移除指定索引处的集合项。” “我明白怎么做了。” “哈,你太着急,集合与数组的不同就在于此,程序在执行RemoveAt(1)的时候,也 就是叫‘阿毛’的Dog被移除了集合,此时‘小黑’的索引次序还是原来的2吗?” “哦,我明白了,等于整个后序对象都向前移一位了。应该是这样才对。也就是 说,集合的变化是影响全局的,它始终都保证元素的连续性。” “总结一下,集合ArrayList相比数组有什么好处?” “主要就是它可以根据使用大小按需动态增加,不用受事先设置其大小的控制。还 有就是可以随意地添加、插入或移除某一范围元素,比数组要方便。” “对,这是ArrayList的优势,但它也有不足,ArrayList不管你是什么对象都是接受 的,因为在它眼里所有元素都是Object,这就使得如果你‘arrayAnimal.add(123);’或 者‘arrayAnimal.add("HelloWorld");’在编译时都是没有问题的,但在执行时,‘for (Animal item :arrayAnimal)’需要明确集合中的元素是Animal类型,而123是整型, HelloWorld是字符串型,这就会在运行到此处时报错,显然,这是典型的类型不匹配错 误,换句话说,ArrayList不是类型安全的。还有就是ArrayList对于存放值类型的数据,比 如int、string型(string是一种拥有值类型特点的特殊引用类型)或者结构struct的数据, 用ArrayList就意味着都需要将值类型装箱为Object对象,使用集合元素时,还需要执行拆 箱操作,这就带来了很大的性能损耗。” 盒子, 可爱, 猫 “等等,我不太懂,装箱和拆箱是什么意思?” “所谓装箱就是把值类型打包到Object引用类型的一个实例中。比如整型变量 i 被 ‘装箱’并赋值给对象o。” “所谓拆箱就是指从对象中提取值类型。此例中对象 o 拆箱并将其赋值给整型变 量 i。” “相对于简单的赋值而言,装箱和拆箱过程需要进行大量的计算。对值类型进行装 箱时,必须分配并构造一个全新的对象。其次,拆箱所需的强制转换也需要进行大量的 计算[MSDN]。总之,装箱拆箱是耗资源和时间的。而ArrayList集合在使用值类型数据时, 其实就是在不断地做装箱和拆箱的工作,这显然是非常糟糕的事情。” “啊,那从这点上来看,它还不如数组来得好了,因为数组事先就指定了数据类型, 就不会有类型安全的问题,也不存在装箱和拆箱的事情了。看来它们各有利弊呀。” “说得非常对,Java在5.0版之前的确也没什么好办法,但5.0出来后,就推出了新的 技术来解决这个问题,那就是泛型。” 0.13 泛型 “泛型是具有占位符(类型参数)的类、结构、接口和方法,这些占位符是类、结 构、接口和方法所存储或使用的一个或多个类型的占位符。泛型集合类可以将类型参数 用作它所存储的对象的类型的占位符;类型参数作为其字段的类型和其方法的参数类型 出现。我读给你的是泛型定义的原话,听起有些抽象,我们直接来看例子。在Java 5.0后 有ArrayList 类的泛型等效类,该类使用大小可按需动态增加的数组实现Collection泛型接 口。其实用法上关键就是在ArrayList后面加‘’,这个‘T’就是你需要指定的集合 的数据或对象类型。” “此时,如果你再写‘arrayAnimal.add(123);’或者‘arrayAnimal.add ("HelloWorld");’结果将是?” “哈,编译就报错,因为add的参数必须是要Animal或者Animal的子类型才行。” “我是这样想的,其实ArrayList和ArrayList在功能上是一样的,不同点就在于, 它在声明和实例化时都需要指定其内部项的数据或对象类型,这就避免了刚才讲的类型 安全问题和装箱拆箱的性能问题了。强,够强,怎么想到的,真是厉害。” “是呀,也就是说,我们一开始就明确了集合这个‘箱子’只能装啥,这个就不需 要再考虑混乱的问题了。不过显然Java语言的设计者也并不是一开始就明白这一点,也是 通过实践和用户的反馈才在Java 5.0版中改进过来的。巨人也会有走弯路的时候,何况我 们常人。通常情况下,都建议使用泛型集合,因为这样可以获得类型安全的直接优点而 不需要从基集合类型派生并实现类型特定的成员。此外,如果集合元素为值类型,泛型 集合类型的性能通常优于对应的非泛型集合类型(并优于从非泛型基集合类型派生的类 型),因为使用泛型时不必对元素进行装箱。” “当然是泛型好呀,它可是集早期的ArrayList集合和Array数组优点于一身的好东 西,有了它,早期的ArrayList就显得太老土了。” “至于泛型的知识还有很多,这里就不细讲了,你自己去找资料研究吧。” “好的好的,其实已经有些明白是怎么回事了。我自己去研究吧。” 0.14 客套 “要讲的东西太多了,我们的‘动物运动会’程序也只写了个开头,以后有的是机 会。” 小菜看了看表说,“现在都过了中午,食堂都快没菜了,走,我们先吃饭去吧。” “好的,今天真的太感谢了,我觉得这半天的收获远远比上一个月课,看几本砖头 书来得效果好呀。”史熙兴奋地说。 “哪里哪里,今天讲的都只是皮毛,要学习的内容还多着呢,不过话说回来,上 午讲的这个未完成的‘动物运动会’的例子尽管简单,但却涵盖了面向对象的最重要的 知识,你好好去体会一下。有机会我再跟你讲讲设计模式,你对封装、继承、多态的理 解就会更深入一些,学无止境,你需要不断地练习实践才可能真正成为优秀的软件工 程师。” “嗯,我觉得我对编程有了很大的兴趣,面向对象的编程方式确实非常有意思。” “师傅领进门,修行在个人,今后就看你的了,好好加油。不过现在我们还是先去 为肚皮加点油哦。” “对对对,走,我们去吃饭去。” 几个月后。研发总监对小菜的培训工作非常满意,准备提升小菜为技术培训经理, 今后培训新员工都可以交给他做。小菜欣喜之余,也不觉感慨,如果不是两年前表哥大 鸟的帮助,自己也不能有今天的成长。那段时间关于设计模式的学习经历,真是一段值 得书写和回味的时光。