8.1 不该初始化的class
这个结构有什么不对?
这个class结构不算太差。如此设计已经能够维持最少的重复程序代码,且有需要特地实现的方法也已经被覆盖过。从多态的角度来看,我们也做到了适应性,所以任何Animal的子型,包括编写程序时无法想象的种类,也能够传递给取用Animal类型的方法来执行。我们已经加上了所有Animal的共同协议到父类上,现在可以开始创建新的Lion等对象。
我们可以写出这样的指令:
Wolf aWolf = new Wolf();
也可以这样:
Animal aHippo = new Hippo();
但是这样会很奇怪:
Animal anim = new Animal();
实例变量的值会是……?
有些类不应该被初始化!
创建出Wolf对象或Hippo对象或Tiger对象是很合理的,但是Animal对象呢?它应该长什么样子?什么颜色、大小、几条腿?
尝试创建出Animal类型的对象就好像出人意料之外,在传送组合的过程中发生了一点问题。
那要如何处理这个问题呢?我们一定要有Animal这个类来继承和产生多态。但是要限制只有它的子类才能够被初始化。我们要的是Lion、Hippo对象,而不是Animal对象。
幸好,有个方法能够防止类被初始化。换句话说,就是让这个类不能被“new”出来。通过标记类为抽象类的,编译器就知道不管在哪里,这个类就是不能创建任何类型的实例。
你还是可以用这种抽象的类型作为引用类型。这也就是当初为何要有抽象类型的目的。
当你设计好继承结构时,你必须要决定哪些类是抽象的,而哪些是具体的。具体的类是实际可以被初始化为对象的。
设计抽象的类很简单——在类的声明前面加上抽象类关键词abstract就好:
abstract public class Canine extends Animal {
public void roam() { }
}
8.2 抽象类
编译器不会让你初始化抽象类
抽象类代表没有人能够创建出该类的实例。你还是可以使用抽象类来声明为引用类型给多态使用,却不用担心哪个创建该类型的对象,编译器会确保这件事。
抽象类除了被继承过之外,是没有用途、没有值、没有目的(除了抽象的class可以有static的成员之外,见第10章)
抽象与具体
不是抽象的类就被称为具体类。在Animal的继承树下,如果我们创建出Animal、Canine与Feline的抽象,则Hippo、Wolf等就是具体的类。
查阅Java API你就会发现其中有很多的抽象类,特别是GUI的函数库中更多。GUI的组件类是按钮、滚动条等与GUI有关类的父类。你只会对组件下的具体子类作初始化动作。
抽象或具体?
你怎么知道某个类应该是抽象的?饮料或许是抽象的。那大雕参茸或威士忌是否也应该是抽象的?在继承层次中从哪个点开始才算是具体的?
你会把“提神饮料”设计成具体,还是说它也是个抽象?看起来“保力达 B”才会是具体的。你认为呢?
观察上面的Animal继承层次,这些抽象或具体的决定是否合适呢?你会修改这个层次吗?
8.3 抽象方法
抽象的方法
除了类之外,你也可以将方法标记为abstract的。抽象的类代表此类必须要被extend过,抽象的方法代表此方法一定要被覆盖过。你或许认为抽象类中的某些行为在没有特定的运行时不会有任何的意义。也就是说,没有任何通用的实现是可行的。想象一下通用的eat()方法会有什么结果?
抽象的方法没有实体!因为你已经知道编写出抽象方法的程序代码没有意义,所以不会含有方法。
如果你声明出一个抽象的方法,就必须将类也标记为抽象的。你不能在非抽象类中拥有抽象方法。
就算只有一个抽象的方法,此类也必须标记为抽象的。
你必须实现所有抽象的方法
实现抽象的方法就如同覆盖过方法一样
抽象的方法没有内容,它只是为了标记出多态而存在。这表示在继承树结构下的第一个具体类必须要实现出所有的抽象方法。
然而你还是可以通过抽象机制将实现的负担转给下层。例如说将Animal与Canine都标记为abstract,则Canine就无需实现出Animal的抽象方法。但具体的类,例如说Dog,就得实现出Animal和Canine的抽象方法。
记得抽象类可以带有抽象和非抽象的方法,因此Canine也可以实现Animal的抽象方法,让Dog不必实现这个部分。如果Canine没有对Animal的抽象类表示出任何意见,就表示Dog得自己实现出Animal的抽象方法。
当我们谈到“你必须实现所有抽象的方法”时,表示说你必须写出内容。你必须以相同的方法鉴名(名称与参数)和相容的返回类型创建出非抽象的方法。Java很注重你的具体子类有没有实现这些方法。
8.4 多态的引用
多态的使用
假设我们不知道有ArrayList这种类而想要自行编写维护list的类以保存Dog对象。在第一轮我们只会写出add()方法。我们使用大小为5的简单Dog数组(Dog[])来保存新加入的Dog对象。当Dog对象超过5个时,你还是可以调用add()方法,但是什么事情也不会发生。如果没有越界,add()会把Dog装到可用的数组位置中,然后递增可用索引(nextlndex)
糟了,也要写出Cat用的
我们有几个选项:
(1)另外创建一个单独的MyCatList类来处理Cat对象,这不好。
(2)创建一个单独的DogAndCatList类,用addCat(Cat c)与addDog(Dog d)来同时处理两个不同的数组实例,这也不好。
(3)编写一个不同的AnimalList类让它处理Animal所有的子类。这应该是最好的办法,所以我们就这么处理,以更通用的Animal来取代个别的子类。程序变更得关键部分有特别标出来(逻辑还是一样,只是把Dog换成Animal)
public class MyAnimalList {
private Animal[] animals = new Animal[5];
private int nextIndex = 0;
public void add(Animal a) {
if (nextIndex < animals.length) {
animals[nextIndex] = a;
System.out.println("Animal added at " + nextIndex);
nextIndex++;
}
}
}
public class AnimalTestDrive {
public static void main(String[] args) {
MyAnimalList list = new MyAnimalList();
Dog a = new Dog();
Cat c = new Cat();
list.add(a);
list.add(c);
}
}
8.5 对象之母:Object
非Animal呢?何不写个万用类?
你知道这要怎么做。我们可以修改数组的类型,并且调整add()方法的参数,以处理Animal之上的类。那便是更通用、更抽象的一种类。但是真的有这种类吗?我们设计的Animal并没有父类不是吗?
事实上是有的。
还记得ArrayList的方法吗?它们是通过对象这个类型来操纵所有类型的对象。
在Java中的所有类都是从Object这个继承出来的。
Object这个类是所有类的源头;它是所有对象的父类。
如果Java中没有共同的父类,那将无法让Java的开发人员创建出可以处理自定义类型的类,也就是说无法写出像ArrayList这样可以处理各种类的类。
就算你不知道,但实际上所有的类都是从对象给继承出来的。你可以把自己写的类想象成是这样声明的:
public class Dog extends Object ( )
但是Dog本来是从Canine给extends出来的啊!没关系,编译器会知道改成让Canine去继承对象。
事实上是Animal去继承对象。
没有直接继承过其他类的类会是隐含的继承对象。
所以就算Dog或Canine没有直接extend对象,还是会通过Animal来继承对象。
终极对象有什么?
如果你是Java,那你会想要让每个对象都带有什么行为?嗯……来个可以判断某对象是否与其他对象相等的方法如何?再加上一个可以说明它是什么类的方法怎样?或许还会需要一个产生对象哈希代码的方法?你可以运用哈希表上的对象(我们会在第17章和附录B中讨论哈希表)。
你知道怎样吗?对象的确有上面所说的方法。那还不是全部的方法,但目前我们只关心这几个。
8.6 取出数组元素
使用Object类型的多态引用是会付出代价的
在你开始以Object类型使用所有超适用性参数和返回类型之前,你应该要考虑到使用Object类型作为引用的一些问题。注意此处的讨论不涉及制作出Object类型的实例,这是在说以Object类型作为引用的其他类型。
当你把对象装进ArrayList<Dog>时,它会被当作Dog来输入与输出:
但若你把它声明成ArrayList<Object>时会怎样?如果你打算创建出一个可以保存任何一种对象的
ArrayList时,你会如此的声明:
如果是这样,当你尝试要把Dog对象取出并赋值给Dog的引用时会发生什么事?
任何从ArrayList<Object>取出的东西都会被当作Object类型的引用而不管它原来是什么
从ArrayList<Object>取出的Object都会被当作是Object这个类的实例。编译器无法将此对象识别为Object以外的事物。
当Dog不再是Dog时
问题在于把所有东西都以多态来当作是Object会让对象看起来失去了真正的本质(但不是永久性的)。Dog似乎失去了犬性。让我们来看一下当我们传入一个Dog给会返回同一个Dog对象的类型引用的方法时会有什么反应。
8.7 编译器对引用类型的检查
Object不会吠
我们已经知道当一个对象被声明为Ob-ject类型的对象所引用时,它无法再赋值给原来类型的变量。我们也知道这会发生在返回类型被声明为Object类型的时候,例如前面所提过的ArrayList<Object>。但这意味着什么呢?使用Object引用变量来引用Dog对象会是个问题吗?让我们试着对被编译器认为是Object的Dog调用Dog才有的方法看看:
编译器是根据引用类型来判断有哪些method可以调用,而不是根据Object确实的类型。
就算你知道对象有这个功能,编译器还是会把它当作一般的Object来看待。编译器只管引用的类型,而不是对象的类型。
8.8 探索内部对象
探索内部Object
对象会带有从父类继承下来的所有东西。这代表每个对象,不论实际类型,也会是个Object的实例。所以Java中的每个对象除了真正的类型外,也可以当作是Object来处理。当你执行new Snowboard)命令时,除了在堆上会有一个Snow-board对象外,此对象也包含了一个Object在里面。
8.9 多态引用
“多态”意味着“很多形式”
你可以把Snowboard当作Snowboard或者Object
如果引用是个遥控器,则当你在继承树往下走时,会发现遥控器的按钮越来越多。Object的遥控器只有几个按钮而已,但Snowboard的遥控器就会包含有来自Object和自己定义的按钮。越接近具体的类会有越多的按钮。
当然这也不是绝对的,子类也有可能不会加入任何新的方法,而只是覆盖过一些方法罢了。重点在于如果对象的类型是Snowboard,而引用它的却是Object,则它不能调用Snowboard的方法。
当你把对象装进ArrayList<Object>时,不管它原来是什么,你只能把它当作是Object。从ArrayList<Object>取出引用时,引用的类型只会是Object。这代表你只会取得Object的遥控器。
8.10 对象引用类型转换
转换回原来的类型
它还是个Dog对象,但如果你想要调用Dog特有的方法,就必须要将类型声明为Dog。如果你真的确定它是个Dog,那么你就可以从Object中拷贝出一个Dog引用,并且赋值给Dog引用变量。
Object o = al.get(index);
Dog d = (Dog) o;
d.roam();
如果不能确定它是Dog,你可以使用instanceof这个运算符来检查。若是类型转换错了,你会在执行期遇到ClassCastException异常并且终止。
if (o instanceof Dog) {
Dog d = (Dog) o;
}
现在你知道Java是多么注重引用变量的类型。
你只能在引用变量的类确实有该方法才能调用它。把类的公有方法当作是合约的内容,合约是你对其他程序的承诺协议。
在编写类时,大多数情况下你一定会显露出一些方法给类以外的程序使用。要让方法显露就代表你会让方法能够存取得到,通常这会通过标记成公有来完成。
想象这样的情境:你在编写一个小型会计总账系统给“猪标服装社”使用。你发现有个称为
Account的类已经写好并且符合你的需求(上一次帮“宏昌水电行”开发程序时写出来的),因此就把它拿过来用。
它有一个debit()和credit()方法可以用来执行会计的借贷项目,还有getBlance()方法可以计算账户。
所以你可以声明一个变量a引用到Account的实例,然后通过圆点运算符调用a.debit()或a.credit()等。因为类的合约是这么保证的,所以你在这个类的实例上面一定能找到这些方法。
万一想要修改合约呢?
好吧,假如你是个Dog(但是千万别去闻另外一个Dog的屁股,这样很不礼貌),你会发现不只有一份合约,你还继承了所有父类传递下来的方法。
类Canine中的所有元素是你合约中的一部分。类Animal中的所有元素是你合约中的一部分。类Object中的所有元素是你合约中的一部分。
根据IS-A测试,你就会是Canine、Animal和Object。
如果有人要编写类似的程序,你大可把定义好的class交给他使用。
但是,如果他还要加上亲热或耍宝等宠物特有的功能要怎么办呢?
现在假设你是设计Dog类的程序设计师。没问题吧?你可以直接把beFriendly()和play()这两个方法加进Dog这个类中。这样做不会让其他用到Dog的程序产生问题,因为你没有更改到其他现有的方法。你觉得这样的做法(把Pet的方法直接加到Dog上)有没有缺点?
如果你是Dog类的程序设计师,且必须修改Dog类以让它能够执行Pet的动作,那你会怎么办?我们知道直接加入Pet的方法是可行的,并且这也不会对其他程序有影响。
但若Cat也要有Pet的功能怎么办?先不管Java的功能,想象一下你要怎样让Animal可以选择性地带有Pet的行为又不会强迫让狮子老虎都表现成宠物?
有哪些方法可以在PetShop程序中重用现有的类?
方法一:
采用最简单的做法,把宠物方法加进Animal类中
优点:
所有的动物马上就可以继承宠物的行为。不需要改变所有子类的程序代码,而新增加的动物也会取得同样的行为。
缺点:
你什么时候看过宠物店贩卖河马?又是什么时候看到饲主跟河马亲热,带河马去公园散步?
并且我们也知道狗跟猫表现亲热的方式不太一样啊。
方法二:
采用方法一,但是把宠物的方法设定成抽象的,强迫每个动物子类覆盖它们
优点:
这样就可以让非宠物类的动物在覆盖这些方法时,作出合理的动作,或者是什么也不作。
缺点:
所有具体的动物都得实现宠物的行为,这样很浪费时间。
并且这种合约不太理想,不论有没有实质的行为,非宠物也得声明出有宠物行为的外观,对狮子老虎的尊严是个打击。
最重要的是这会让Animal的定义变得有些局限性,反而让其他类型的程序更难以重复利用。
方法三:
把方法加到需要的地方
优点:
不必担心如何跟河马亲热。只有宠物才会有宠物的行为。
缺点:
首先,这样就会失去了物该有的合约保证。你无法确定宠物可以执行的是doFriendly()还是beFriendly()。
其次,多态将无法起作用,因为Animal不会有共同的宠物行为,你得针对个别宠物设计程序。
所以我们真正需要的方法是:
(1)一种可以让宠物行为只应用在宠物身上的方法。
(2)一种确保所有宠物的类都有相同的方法定义的方法。
(3)一种是可以运用到多态的方法。
8.11 多重继承的麻烦
这种“多重继承”可能会很差。其实Java不支持这种方式,因为多重继承会有称为“致命方块”的问题“致命方块”(因为这个形状看起来就像扑克牌的方块)
允许致命方块的程序语言会产生某种很糟糕的复杂性问题,因为你必须要有某种规则来处理可能出现的模糊性。额外的规则意味着你必须同时学习这些规则与观察适用这些规则的特殊状况。因此Java基于简单化的原则而不允许这种致命方块的出现。好吧,问题还是没有解决……
8.12 使用接口
接口是我们的救星!
Java有个解决方案,使用接口。此处所讨论的不是GUI的接口,也不是“沟通管道”或“存取途径”的接口,我们说的是Java的interface关键词。
此接口可以用来解决多重继承的问题却又不会产生致命方块这种问题。
接口解决致命方块的办法很简单:把全部的方法设为抽象的!如此一来,子类就得要实现此方法,因此Java虚拟机在执行期间就不会搞不清楚要用哪一个继承版本。
接口的定义:
public interface Pet {...}
接口的实现:
public class Dog extends Canine implements Pet {...}
设计与实现Pet接口
public interface Pet {
public abstract void beFriendly();
public abstract void play();
}
public class Dog extends Canine implements Pet{
public void beFriendly() {...}
public void play() {...}
public void roam() {...}
public void eat() {...}
}
不同继承树的类也可以实现相同的接口
当你把一个类当作多态类型运用时,相同的类型必定来自同一个继承树,而且必须是该多态类型的子类。定义为Canine类型的参数可以接受Wolf与Dog,但无法忍受Cat或Hippo。
但当你用接口来作为多态类型时,对象就可以来自任何地方了。唯一的条件就是该对象必须是来自有实现此接口的类。允许不同继承树的类实现共同的接口对Java API来说是非常重要的。如果你想要将对象的状态保存在文件中,只要去实现Serializable这个接口就行。打算让对象的方法以单独的线程来执行吗?没问题,实现Runnable。有概念了吧。后面的章节会有关于Serializable与Runnable的讨论,现在只要先掌握住这个概念就行。
更棒的是类可以实现多个接口!
通过继承结构,Dog对象IS-A Canine、IS-A Animal、IS-A Object。但Dog IS-A Pet是通过接口实现的机制达成的,并同时也能够实现其他的接口:
public class Dog extends Animal implements Pet, Saveable, paintable{...}
瑰是红的,自我感觉是良好的,extend只能有一个,implement可以有好多个。类来自单亲家庭(superclass),但可以扮演多重角色(implement)。
要如何判断应该是设计类、子类、抽象类或接口呢?
如果新的类无法对其他的类通过IS-A测试时,就设计不继承其他类的类。
只有在需要某类的特殊化版本时,以覆盖或增加新的方法来继承现有的类。
当你需要定义一群子类的模板,又不想让程序员初始化此模板时,设计出抽象的类给它们用
如果想要定义出类可以扮演的角色,使用接口。
调用父类的方法