对java集合框架的理解

来源:互联网 发布:linux svn命令 用户名 编辑:程序博客网 时间:2024/05/22 15:11

我们常说要继承的话,到底是写个抽象类还是接口,它们区别在于:如果子类确实是父类的一种,应该使用抽象类,描述是“is-a”的关系,而接口则表示一种行为,描述的是“like-a”的关系。但在Java类库里,其实许多原则由于各种原因被打破了,比如在Collection框架里,List/Set都是Collection的一种,为什么不把Collection定义为抽象类呢?而ArrayList/LinkedList也都是List的一种,为什么不把List定义为抽象类呢?这就是原则和实际的折衷。作为Java类库而言,不仅要考虑面向对象的一些原则,也要考虑扩展性和语言本身的限制。能不能把Collection接口去掉,用AbstractCollection作为顶层?作为类库而言是不可以的,因为Java是单继承的,如果把AbstractCollection作为顶层,那么当用户自定义的类既要继承自己的父类,又要具备集合的属性,那么就做不到了(可以自定义集合接口,但就无法与Collection相互转化)。因此,Java集合框架采取的是类库广泛使用的接口+抽象类的形式,以同时获得接口和抽象类的好处,所以我们看到ArrayList extends AbstractList implements List(AbstractList本身就是实现List的,这里再写出implements List是为了使ArrayList的类结构更为清晰)。

另外我们再看Set接口,它的方法基本和Collection方法一模一样,为什么要再写一遍?一方面是作为类库而言要增加详细注释,虽然是同名的方法但实现的约束不同,比如Set的add方法是不会保存重复值的,另一方面是为了从Set接口本身能很清楚地看到它所提供的功能(比如size()方法,和Collection是完全一个含义,也重新定义了一遍),这是从类库易读性来考虑,对于我们自己编写的类,基本就不需要这样。

说多了,回到集合框架本身。

Iterable基本是个标识接口,同时约定了所有线性集合(数组、队列、栈这种一维的都属于线性集合,Map就属于二维,不要求遍历)必须是可以遍历的(集合要给出遍历结构),同时提供了配套的Iterator顶级接口,实现hasNext()、next()和remove()方法来完成遍历功能。为什么这里要定义remove接口方法却不定义add/set方法?个人觉得这可能是考虑在类库的使用过程中remove的频率更高,而add的方法频率要低,set的使用场景就更少了。

ListIterator相比Iterator就多提供了很多功能,包括上面提到的add/set,还有获得索引的nextIndex、previousIndex、以及往回迭代的hasPrevious()/previous()。给针对线性表的操作者更多的便利,事实上在AbstractList里就提供了iterator()和listIterator()两种方法来提供给开发者更多选择。相应的,在HashMap里头,也提供了实现Iterator接口的HashIterator内部抽象类,而在Apache Commons Collections下甚至单独写出MapIterator extends Iterator,由此可见,作为类库的设计者,在Iterator和ListIterator/HashMapIterator上是做了便捷性/易用性以及使用场景上的权衡的。

ArrayList内部结构是个数组,默认是10,在创建ArrayList对象时此数组是空的(Object[] EMPTY_ELEMENTDATA = {})只有当add的时候才扩容(如果扩容容量小于DEFAULT_CAPACITY,也就是10,就一次性扩容到10)。其扩容的机制是:当前数组容量已经无法放入更多元素的时候,增加原有数组的一半

LinkedList我们知道内部结构是个线性链表,首先看它继承的不是AbstractList,而是继承自AbstractSequentialList,这是AbstractList的子类,实现了线性链表的骨架方法,如get/set,均是通过ListIterator迭代器来遍历实现。为什么要创造出AbstractSequentialList这个类?因为线性的不只有链表,但线性的都只有通过迭代器才能找到元素,与之对应的是随机读取——也就是数组,因此在AbstractSequentialList的类注释里明确说明:如果是随机读取的,则使用AbstractList更合适(AbstractList并没有提供随机读取的实现,类注释的意思只是说如要随机读取,则AbstractSequentialList没有任何帮助,不如实现AbstractList更准确)。事实上,为了表明集合是否可以根据索引随机读取,Collection框架专门定义了一个空接口RandomAccess,以标识该类是否可随机读,ArrayList、Vector都实现了这个接口,而没有实现这个接口的,则是不可以通过下标索引来寻址的。

LinkedList有比ArrayList在接口上有更丰富的功能,比如addFirst()、addLast()、push()、pop(),、indexOf(),同时它的listIterator()也要比iterator()更常用一些。我们以前常说对于经常删除、增加的集合,使用LinkedList比ArrayList效率要高,这是容易被误解的,LinkedList的寻址相比数组来说非常地慢,如果在频繁增/删之前需要寻址定位,那么仍然比ArrayList要慢很多,数十倍地慢,所以使用它的时候要谨慎,不能耍小聪明。LinkedList根据索引寻址的get(int index)方法,使用的是简单的“二分法”,即如果index小于size的一半,则从前往后迭代;大于size一半则从后往前迭代。这也是没有办法的事情,LinkedList是需要保证插入顺序的,所以不能做任何排序,也就不能使用任何如冒泡、快速排序之类的算法。有没有不需要保证插入顺序从而能够快速寻址的集合呢?TreeSet/HashSet可以快速寻址,但不能有重复值;TreeMap/HashMap同样是不能有重复值;Collection框架并没有给出能有重复值同时又能允许排序的List,应该是他们认为ArrayList就可以满足这种场景了,但类库中有个类IdentityHashMap,它的hash()方法用的是System.identityHashCode()而不是HashMap所用的key.hashCode。System.identityHashCode()意思是不管对象是否实现了hashCode,都取Object的hashCode也就是对象的内存地址来作为key,这样即使两个对象hashCode相等,也会被重复插入(在该类的注释中说到了它的一些使用场景,有兴趣的可以仔细看下)。

我们知道通常的集合都是非线程安全的,表现在多个线程同时增/删时,集合大小会不可预测,同时Iterator尽量保证在迭代过程中操作是安全的(不保证准确,但尽量保证不会有越界问题),即当某线程迭代读取集合时,如有其他线程修改此集合的结构(扩大/缩小),则会抛出ConcurrentModificationException。那么它是如何实现的呢?在集合中都会维护一个内部计数器modCount,如果有影响集合结构的操作(增加、删除、合并等,而修改不是),modCount都会自增1。在对集合迭代时,都会检查当前迭代时的操作计数器副本expectedModCount(迭代前初始化为和modCount相等)和modCount是否相等

从整体来说,感觉Set设计地不是太好,其大多数功能和List很像,仅非重复这个频率并不高的场景不足以单独列这一套接口,而其实现上又基本上完全依托于Map。如果开发者真有这种场景,完全可以自行用HashMap来代替。

集合框架中还有两个很有用的辅助类,分别是Collections和Arrays,这两个就不多介绍了。Collections提供了一系列synchronized集合、unmodified集合以及很少用的Checked集合(类型检查的),以及toArray(toArray(T[] a)更好用,因为能指定返回数组的元素类型)、binarySearch(快速查找算法,需要参数列表元素能排序,否则结果就不准确)。而Arrays提供了一些如sort、merge、binarySearch、copyOf、asList这样有效的方法,注意这里的asList返回的Arrays内部实现的一个ArrayList,有些方法不支持,比如add、remove,除了set之外基本上就是一个只读列表,如果需要可add/remove,还是需要使用集合的相应构造函数或者Collections的copy方法)。


0 0