1.8 单根继承结构
是否所有的类最终都继承自单一的基类。
在Java中(事实上还包括除C++以外的所有OOP语言),答案是yes,这个终极基类的名字就是Object。
事实证明, 单根继承结构带来了很多好处。
在单根继承结构中的所有对象都具有一个共用接口,所以它们归根到底都是相同的基本类型。
另一种(C++所提供的)结构是无法确保所有对象都属于同一个基本类型。
单根继承结构保证所有对象都具备某些功能。
因此你知道,在你的系统中你可以在每个对象上执行某些基本操作。
所有对象都可以很容易地在【堆】上创建,而【参数传递】也得到了极大的简化。
单根继承结构使【垃圾回收器】的实现变得容易得多,而垃圾回收器正是Java对C++的重要改进之一。
这对于系统级操作(如异常处理)显得尤其重要,并且给编程带来了更大的灵活性。
1.9 容器
对于面向对象设计中的大多数问题而言,这个问题的解决方案似乎过于轻率:创建另一种对象类型。
这种新的对象类型持有对其他对象的引用。
当然,你可以用在大多数语言中都有的数组类型来实现相同的功能。
但是这个通常被称为容器(也称为集合,不过Java类库以不同的含义使用“集合”这个术语,所以本书将使用“容器”,这个词)的新对象,
在任何需要时都可扩充自己以容纳你置于其中的所有东西。
因此不需要知道将来会把多少个对象置于容器中,只需要创建一个容器对象,然后让它处理所有细节。
幸运的是,好的OOP语言都有一组容器,它们作为开发包的一部分。
Java)中,具有满足不同需要的各种类型的容器,
例如List(用于存储序列),Map(也被称为关联数组,用来建立对象之间的关联),Set(每种对象类型只持有一个),以及诸如队列、树、堆栈等更多的构件。
设计的观点来看,真正需要的只是一个可以被操作,从而解决问题的序列。
如果单一类型的容器可以满足所有需要,那么就没有理由设计不同种类的序列了。
然而还是需要对容器有所选择,这有两个原因。
第一,不同容器提供了不同类型的接口和外部行为。
堆栈相比于队列就具备不同的接口和行为,也不同于集合和列表的接口和行为。
它们之中的某种容器提供的解决方案可能比其他容器要灵活得多。
第二,不同的容器对于某些操作具有不同的效率。
最好的例子就是两种List的比较: ArrayList和 LinkedList 。
我们可以在一开始使用LinkedList构建程序,而在优化系统性能时改用ArrayList。
接口List所带来的抽象,把在容器之间进行转换时对代码产生的影响降到最小限度。
1.9.1 参数化类型
在Java SE5出现之前,容器存储的对象都只具有Java中的通用类型:Object。
这使得容器很容易被复用。当把它从容其中取回时,就获取了一个对Object对象的引用,而不是对置入时的那个类型的对象的引用。它丢失其身份。
所以,怎样才能将它变回先前置入容器中时的具有实用接口的对象呢?
向下转型。
向下转型和运行时的检查需要额外的程序运行时间,也需要程序员付出更多的心血。
那么创建这样的容器,它知道自己所保存的对象的类型,从而不需要向下转型以及消除犯错误的可能,这样不是更有意义吗?
这种解决方案被称为参数化类型机制。
参数化类型就是一个编译器可以自动定制作用于特定类型上的类。
Java SE5的重大变化之一就是增加了参数化类型,在Java中它称为范型。
一对尖括号,中间包含类型信息,通过这些特征就可以识别对范型的使用。
例如,可以用下面这样的语句来创建一个存储Shape的ArrayList:
ArrayList shapes = new ArrayList();
在OOP中,自C++面世以来就已变得非常瞩目的一个问题就是,是否所有的类最终都继承自单一的基类。
在Java中(事实上还包括除C++以外的所有OOP语言),答案是yes,这个终极基类的名字就是Object。
事实证明, 单根继承结构带来了很多好处。
在单根继承结构中的所有对象都具有一个共用接口,所以它们归根到底都是相同的基本类型。
另一种(C++所提供的)结构是无法确保所有对象都属于同一个基本类型。
从向后兼容的角度看,这么做能够更好地适应C模型,而且受限较少,
但是当要进行完全的面向对象程序设计时,则必须构建自己的继承体系,使得它可以提供其他OOP语言内置的便利。
并且在所获得的任何新类库中,总会用到一些不兼容的接口,需要花力气(有可能要通过多重继承)来使新接口融入你的设计之中。
这么做来换取C++额外的灵活性是否值得呢?
如果需要的话——如果在C上面投资巨大,这么做就很有价值。
如果是刚刚从头开始,那么像Java这样的选择通常会有更高的生产率。
单根继承结构保证所有对象都具备某些功能。
因此你知道,在你的系统中你可以在每个对象上执行某些基本操作。
所有对象都可以很容易地在堆上创建,而参数传递也得到了极大的简化。
单根继承结构使垃圾回收器的实现变得容易得多,而垃圾回收器正是Java对C++的重要改进之一。
由于所有对象都保证具有其类型信息,因此不会因无法确定对象的类型而陷入僵局。
这对于系统级操作(如异常处理)显得尤其重要,并且给编程带来了更大的灵活性。
通常说来,如果不知道在解决某个特定问题时需要多少个对象,或者它们将存活多久,那么就不可能知道如何存储这些对象。
如何才能知道需要多少空间来创建这些对象呢?
答案是你不可能知道,因为这类信息只有在运行时才能获得。
对于面向对象设计中的大多数问题而言,这个问题的解决方案似乎过于轻率:创建另一种对象类型。
这种新的对象类型持有对其他对象的引用。
当然,你可以用在大多数语言中都有的数组类型来实现相同的功能。
但是这个通常被称为容器(也称为集合,不过Java类库以不同的含义使用“集合”这个术语,所以本书将使用“容器”,这个词)的新对象,
在任何需要时都可扩充自己以容纳你置于其中的所有东西。
因此不需要知道将来会把多少个对象置于容器中,只需要创建一个容器对象,然后让它处理所有细节。
幸运的是,好的OOP语言都有一组容器,它们作为开发包的一部分。
在C++中,容器是标准C++类库的一部分,经常被称为标准模板类库(Standard Template Library STL)。
Object Pascal 在其可视化构件库(Visual Component Libraryt VCL)中有容器;Smalltalk提供了一个非常完备的容器集;
Java在其标准类库中也包含有大量的容器。
在某些类库中,一两个通用容器足够满足所有的需要,但是在其他类库(例如Java)中,具有满足不同需要的各种类型的容器,
例如List(用于存储序列),Map(也被称为关联数组,用来建立对象之间的关联),Set(每种对象类型只持有一个),以及诸如队列、树、堆栈等更多的构件。
从设计的观点来看,真正需要的只是一个可以被操作,从而解决问题的序列。
如果单一类型的容器可以满足所有需要,那么就没有理由设计不同种类的序列了。
然而还是需要对容器有所选择,这有两个原因。
第一,不同容器提供了不同类型的接口和外部行为。
堆栈相比于队列就具备不同的接口和行为,也不同于集合和列表的接口和行为。
它们之中的某种容器提供的解决方案可能比其他容器要灵活得多。
第二,不同的容器对于某些操作具有不同的效率。
最好的例子就是两种List的比较: ArrayList和 LinkedList 。
它们都是具有相同接口和外部行为的简单的序列,但是它们对某些操作所花费的代价却有天壤之别。
在ArrayList中,随机访问元素是一个花费固定时间的操作;
但是,对LinkedList来说,随机选取元素需要在列表中移动,这种代价是高昂的,访问越靠近表尾的元素, 花费的时间越长。
而另一方面,如果想在序列中间插入一个元素,LinkedList的开销却比ArrayList要小。
上述操作以及其他操作的效率,依序列底层结构的不同而存在很大的差异。
我们可以在一开始使用LinkedList构建程序,而在优化系统性能时改用ArrayList。
接口List所带来的抽象,把在容器之间进行转换时对代码产生的影响降到最小限度。
在Java SE5出现之前,容器存储的对象都只具有Java中的通用类型:Object。
单根继承结构意味着所有东西都是Object类型。
所以可以存储Object的容器可以存储任何东西。
这使得容器很容易被复用。
要使用这样的容器,只需在其中置入对象引用,稍后还可以将它们取回。
但是由于容器只存储Object,所以当将对象引用置入容器时,它必须被向上转型为Object,因此它会丢失其身份。
当把它取回时,就获取了一个对Object对象的引用,而不是对置入时的那个类型的对象的引用。
所以,怎样才能将它变回先前置入容器中时的具有实用接口的对象呢?
这里再度用到了转型,但这一次不是向继承结构的上层转型为一个更泛化的类型,而是向下转型为更具体的类型。
这种转型的方式称为向下转型。
我们知道,向上转型是安全的,例如Circle是一种Shape类型;但是不知道某个Object是Circle还是Shape,所以除非确切知道所要处理的对象的类型,否则向下转型几乎是不安全的。
然而向下转型并非彻底是危险的,因为如果向下转型为错误的类型,就会得到被称为异常的运行时错误,稍后会介绍什么是异常。
尽管如此,当从容器中取出对象引用时,还是必须要以某种方式记住这些对象究竟是什么类型,这样才能执行正确的向下转型。
向下转型和运行时的检查需要额外的程序运行时间,也需要程序员付出更多的心血。
那么创建这样的容器,它知道自己所保存的对象的类型,从而不需要向下转型以及消除犯错误的可能,这样不是更有意义吗?
这种解决方案被称为参数化类型机制。
参数化类型就是一个编译器可以自动定制作用于特定类型上的类。
例如,通过使用参数化类型,编译器可以定制一个只接纳和取出Shape对象的容器。
Java SE5的重大变化之一就是增加了参数化类型,在Java中它称为范型。
一对尖括号,中间包含类型信息,通过这些特征就可以识别对范型的使用。
例如,可以用下面这样的语句来创建一个存储Shape的ArrayList:
ArrayList shapes = new ArrayList();
为了利用范型的优点,很多标准类库构件都已经进行了修改。
就像我们将要看到的那样,范型对本书中的许多代码都产生了重要的影响。