Java集合体系结构分析与比较

By | 03月15日
Advertisement

1. Java集合框架图

Java平台提供了一个全新的集合框架。“集合框架”主要由一组用来操作对象的接口组成。不同接口描述一组不同数据类型。

Java集合框架图如下:

集合接口:6个接口(短虚线表示),表示不同集合类型,是集合框架的基础。

抽象类:5个抽象类(长虚线表示),对集合接口的部分实现。可扩展为自定义集合类。

实现类:8个实现类(实线表示),对接口的具体实现。

在很大程度上,一旦您理解了接口,您就理解了框架。虽然您总要创建接口特定的实现,但访问实际集合的方法应该限制在接口方法的使用上;因此,允许您更改基本的数据结构而不必改变其它代码。

Java集合的顶层接口是Collection,Collection 接口是一组允许重复的对象。Java集合框架主要由以下三个接口组成:

(1) Set 接口继承Collection,但不允许重复,使用自己内部的一个排列机制。

(2) List 接口继承Collection,允许重复,以元素安插的次序来放置元素,不会重新排列。

(3) Map接口是一组成对的键-值对象,即所持有的是key-value pairs。Map中不能有重复的key,拥有自己的内部排列机制。

容器中的元素类型都为Object,从容器取得元素时,必须把它转换成原来的类型。简化后的集合框架图如下:

集合接口:6个接口(短虚线表示),表示不同集合类型,是集合框架的基础。

抽象类:5个抽象类(长虚线表示),对集合接口的部分实现。可扩展为自定义集合类。

实现类:8个实现类(实线表示),对接口的具体实现。

在很大程度上,一旦您理解了接口,您就理解了框架。虽然您总要创建接口特定的实现,但访问实际集合的方法应该限制在接口方法的使用上;因此,允许您更改基本的数据结构而不必改变其它代码。

Java集合的顶层接口是Collection,Collection 接口是一组允许重复的对象。Java集合框架主要由以下三个接口组成:

(1) Set 接口继承Collection,但不允许重复,使用自己内部的一个排列机制。

(2) List 接口继承Collection,允许重复,以元素安插的次序来放置元素,不会重新排列。

(3) Map接口是一组成对的键-值对象,即所持有的是key-value pairs。Map中不能有重复的key,拥有自己的内部排列机制。

容器中的元素类型都为Object,从容器取得元素时,必须把它转换成原来的类型。简化后的集合框架图如下:

Java集合体系结构分析与比较

2. 接口Collection

用于表示任何对象或元素组,想要尽可能以常规方式处理一组元素时,就使用这一接口。

(1)单元素添加、删除操作:

booleanadd(Object o):将对象添加给集合

booleanremove(Object o): 如果集合中有与o相匹配的对象,则删除对象o

(2)查询操作:

intsize():返回当前集合中元素的数量

booleanisEmpty():判断集合中是否有任何元素

booleancontains(Object o):查找集合中是否含有对象o

Iteratoriterator():返回一个迭代器,用来访问集合中的各个元素

(3)组操作:作用于元素组或整个集合

booleancontainsAll(Collection c): 查找集合中是否含有集合c中所有元素

booleanaddAll(Collection c) : 将集合c中所有元素添加给该集合

voidclear(): 删除集合中所有元素

voidremoveAll(Collection c) : 从集合中删除集合c中的所有元素

voidretainAll(Collection c) : 从集合中删除集合c中不包含的元素

(4)Collection转换为Object数组:

Object[]toArray():返回一个内含集合所有元素的array

Object[]toArray(Object[]a):返回一个内含集合所有元素的array。运行期返回的array和参数a的型别相同,需要转换为正确型别。

此外,您还可以把集合转换成其它任何其它的对象数组。但是,您不能直接把集合转换成基本数据类型的数组,因为集合必须持有对象。

斜体接口方法是可选的。因为一个接口实现必须实现所有接口方法,调用程序就需要一种途径来知道一个可选的方法是不是不受支持。如果调用一种可选方法时,一个UnsupportedOperationException被抛出,则操作失败,因为方法不受支持。此异常类继承RuntimeException类,避免了将所有集合操作放入try-catch块。

Collection不提供get()方法。如果要遍历Collectin中的元素,就必须用Iterator。

2.1 抽象类AbstractCollection

AbstractCollection类提供具体“集合框架”类的基本功能。虽然您可以自行实现Collection接口的所有方法,但是,除了iterator()和size()方法在恰当的子类中实现以外,其它所有方法都由AbstractCollection类来提供实现。如果子类不覆盖某些方法,可选的如add()之类的方法将抛出异常。

2.2 接口Iterator

Collection接口的iterator()方法返回一个Iterator。Iterator接口方法能以迭代方式逐个访问集合中各个元素,并安全的从Collection中除去适当的元素。

(1)boolean hasNext(): 判断是否存在另一个可访问的元素

Objectnext(): 返回要访问的下一个元素。如果到达集合结尾,则抛出NoSuchElementException异常。

(2)void remove():删除上次访问返回的对象。本方法必须紧跟在一个元素的访问后执行。如果上次访问后集合已被修改,方法将抛出IllegalStateException。

Iterator中删除操作对底层Collection也有影响。

迭代器是故障快速修复(fail-fast)的。这意味着,当另一个线程修改底层集合的时候,如果您正在用Iterator遍历集合,那么,Iterator就会抛出ConcurrentModificationException(一种RuntimeException异常)异常并立刻失败。

在遍历Iterator时不能对底层Collection执行remove()操作。

3. 接口List

List接口继承了Collection接口以定义一个允许重复项的有序集合。该接口不但能够对列表的一部分进行处理,还添加了面向位置的操作。

(1) 面向位置的操作包括插入某个元素或Collection的功能,还包括获取、除去或更改元素的功能。在List中搜索元素可以从列表的头部或尾部开始,如果找到元素,还将报告元素所在的位置:

voidadd(int index, Object element): 在指定位置index上添加元素element

booleanaddAll(int index, Collection c): 将集合c的所有元素添加到指定位置index

Objectget(int index): 返回List中指定位置的元素

intindexOf(Object o): 返回第一个出现元素o的位置,否则返回-1

intlastIndexOf(Object o):返回最后一个出现元素o的位置,否则返回-1

Objectremove(int index) :删除指定位置上的元素

Objectset(int index, Object element):用元素element取代位置index上的元素,并且返回旧的元素

(2)List 接口不但以位置序列迭代的遍历整个列表,还能处理集合的子集:

ListIteratorlistIterator() : 返回一个列表迭代器,用来访问列表中的元素

ListIteratorlistIterator(int index) : 返回一个列表迭代器,用来从指定位置index开始访问列表中的元素

ListsubList(int fromIndex, inttoIndex):返回从指定位置fromIndex(包含)到toIndex(不包含)范围中各个元素的列表视图

对子列表的更改(如add()、remove()和set()调用)对底层List也有影响。

3.1 接口ListIterator

ListIterator接口继承Iterator接口以支持添加或更改底层集合中的元素,还支持双向访问。ListIterator没有当前位置,光标位于调用previous和next方法返回的值之间。一个长度为n的列表,有n+1个有效索引值:

(1)void add(Object o): 将对象o添加到当前位置的前面

voidset(Object o):用对象o替代next或previous方法访问的上一个元素。如果上次调用后列表结构被修改了,那么将抛出IllegalStateException异常。

(2) boolean hasPrevious(): 判断向后迭代时是否有元素可访问

Objectprevious():返回上一个对象

intnextIndex(): 返回下次调用next方法时将返回的元素的索引

intpreviousIndex(): 返回下次调用previous方法时将返回的元素的索引

3.2 抽象类AbstractList和AbstractSequentialList

有两个抽象的List实现类:AbstractList和AbstractSequentialList。像AbstractSet类一样,它们覆盖了equals()和hashCode()方法以确保两个相等的集合返回相同的哈希码。若两个列表大小相等且包含顺序相同的相同元素,则这两个列表相等。这里的hashCode()实现在List接口定义中指定,而在这里实现。

除了equals()和hashCode(),AbstractList和AbstractSequentialList实现了其余List方法的一部分。因为数据的随机访问和顺序访问是分别实现的,使得具体列表实现的创建更为容易。需要定义的一套方法取决于您希望支持的行为。您永远不必亲自提供的是iterator方法的实现。

3.3 类LinkedList、ArrayList和Vector

在“集合框架”中有两种常规的List实现:ArrayList和LinkedList。使用两种List实现的哪一种取决于您特定的需要。如果要支持随机访问,而不必在除尾部的任何位置插入或除去元素,那么,ArrayList提供了可选的集合。但如果,您要频繁的从列表的中间位置添加和除去元素,而只要顺序的访问列表元素,那么,LinkedList实现更好。

ArrayList和LinkedList都实现Cloneable接口,都提供了两个构造函数,一个无参的,一个接受另一个Collection

3.1.1 类LinkedList

LinkedList类添加了一些处理列表两端元素的方法。

(1) void addFirst(Object o): 将对象o添加到列表的开头

voidaddLast(Object o):将对象o添加到列表的结尾

(2) Object getFirst(): 返回列表开头的元素

ObjectgetLast(): 返回列表结尾的元素

(3) Object removeFirst(): 删除并且返回列表开头的元素

ObjectremoveLast():删除并且返回列表结尾的元素

(4) LinkedList(): 构建一个空的链接列表

LinkedList(Collectionc): 构建一个链接列表,并且添加集合c的所有元素

使用这些新方法,您就可以轻松的把LinkedList当作一个堆栈、队列或其它面向端点的数据结构。

3.1.2 类ArrayList

ArrayList类封装了一个动态再分配的Object[]数组。每个ArrayList对象有一个capacity。这个capacity表示存储列表中元素的数组的容量。当元素添加到ArrayList时,它的capacity在常量时间内自动增加。

在向一个ArrayList对象添加大量元素的程序中,可使用ensureCapacity方法增加capacity。这可以减少增加重分配的数量。

(1) void ensureCapacity(int minCapacity): 将ArrayList对象容量增加minCapacity

(2) void trimToSize(): 整理ArrayList对象容量为列表当前大小。程序可使用这个操作减少ArrayList对象存储空间。

3.1.3 类Vector

Vector类似于ArrayList。从API的角度来看这两个类非常相似。Vector是同步的,这个类中的一些方法保证了Vector中的对象是线程安全的。而ArrayList则是异步的,因此ArrayList中的对象并不是线程安全的。

3.1.4 三者之间的区别

3.1.4.1 LinkedList与ArrayList的区别

ArrayList:支持随机访问,不必在除尾部的任何位置插入或除去元素。

LinkedList:频繁的从列表的中间位置添加和除去元素,而只要顺序的访问列表元素。

3.1.4.2 Vector与ArrayList的区别

(1)同步性

Vector是同步的。这个类中的一些方法保证了Vector中的对象是线程安全的。而ArrayList则是异步的,因此ArrayList中的对象并不是线程安全的。因为同步的要求会影响执行的效率,所以如果你不需要线程安全的集合那么使用ArrayList是一个很好的选择,这样可以避免由于同步带来的不必要的性能开销。

(2)数据增长

从内部实现机制来讲ArrayList和Vector都是使用数组(Array)来控制集合中的对象。当你向这两种类型中增加元素的时候,如果元素的数目超出了内部数组目前的长度它们都需要扩展内部数组的长度,Vector缺省情况下自动增长原来一倍的数组长度,ArrayList是原来的50%,所以最后你获得的这个集合所占的空间总是比你实际需要的要大。所以如果你要在集合中保存大量的数据那么使用Vector有一些优势,因为你可以通过设置集合的初始化大小来避免不必要的资源开销。

(3)使用模式

在ArrayList和Vector中,从一个指定的位置(通过索引)查找数据或是在集合的末尾增加、移除一个元素所花费的时间是一样的,这个时间我们用O(1)表示。但是,如果在集合的其他位置增加或移除元素那么花费的时间会呈线形增长:O(n-i),其中n代表集合中元素的个数,i代表元素增加或移除元素的索引位置。为什么会这样呢?以为在进行上述操作的时候集合中第i和第i个元素之后的所有元素都要执行位移的操作。这一切意味着什么呢?

这意味着,你只是查找特定位置的元素或只在集合的末端增加、移除元素,那么使用Vector或ArrayList都可以。如果是其他操作,你最好选择其他的集合操作类。比如,LinkList集合类在增加或移除集合中任何位置的元素所花费的时间都是一样的—O(1),但它在索引一个元素的使用却比较慢-O(i),其中i是索引的位置。使用ArrayList也很容易,因为你可以简单的使用索引来代替创建iterator对象的操作。LinkList也会为每个插入的元素创建对象,所有你要明白它也会带来额外的开销。

最后,在《PracticalJava》一书中PeterHaggar建议使用一个简单的数组(Array)来代替Vector或ArrayList。尤其是对于执行效率要求高的程序更应如此。因为使用数组(Array)避免了同步、额外的方法调用和不必要的重新分配空间的操作。

4. 接口Set

Set接口继承Collection接口,而且它不允许集合中存在重复项,每个具体的Set实现类依赖添加的对象的equals()方法来检查独一性,因此加入Set的Object必须定义equals()方法以确保对象的唯一性。Set接口没有引入新方法,所以Set就是一个Collection,只不过其行为不同。

4.1 Hash表

Hash表是一种数据结构,用来查找对象。Hash表为每个对象计算出一个整数,称为HashCode(哈希码)。Hash表是个链接式列表的阵列。每个列表称为一个buckets(哈希表元)。对象位置的计算 index= HashCode % buckets (HashCode为对象哈希码,buckets为哈希表元总数)。

当你添加元素时,有时你会遇到已经填充了元素的哈希表元,这种情况称为HashCollisions(哈希冲突)。这时,你必须判断该元素是否已经存在于该哈希表中。

如果哈希码是合理地随机分布的,并且哈希表元的数量足够大,那么哈希冲突的数量就会减少。同时,你也可以通过设定一个初始的哈希表元数量来更好地控制哈希表的运行。初始哈希表元的数量为 buckets= size * 150% + 1 (size为预期元素的数量)。

如果哈希表中的元素放得太满,就必须进行rehashing(再哈希)。再哈希使哈希表元数增倍,并将原有的对象重新导入新的哈希表元中,而原始的哈希表元被删除。loadfactor(加载因子)决定何时要对哈希表进行再哈希。在Java编程语言中,加载因子默认值为0.75,默认哈希表元为101。

4.2 接口Comparable与Comparator

在“集合框架”中有两种比较接口:Comparable接口和Comparator接口。像String和Integer等Java内建类实现Comparable接口以提供一定排序方式,但这样只能实现该接口一次。对于那些没有实现Comparable接口的类、或者自定义的类,您可以通过Comparator接口来定义您自己的比较方式。

4.2.1 接口Comparable

在java.lang包中,Comparable接口适用于一个类有自然顺序的时候。假定对象集合是同一类型,该接口允许您把集合排序成自然顺序。

(1)int compareTo(Object o):比较当前实例对象与对象o,如果位于对象o之前,返回负值,如果两个对象在排序中位置相同,则返回0,如果位于对象o后面,则返回正值

在Java2SDK版本1.4中有二十四个类实现Comparable接口。下表展示了8种基本类型的自然排序。虽然一些类共享同一种自然排序,但只有相互可比的类才能排序。



排序


BigDecimal,BigInteger,Byte,Double,

Float,Integer,Long,Short


按数字大小排序


Character


按Unicode值的数字大小排序


String


按字符串中字符Unicode值排序

利用Comparable接口创建您自己的类的排序顺序,只是实现compareTo()方法的问题。通常就是依赖几个数据成员的自然排序。同时类也应该覆盖equals()和hashCode()以确保两个相等的对象返回同一个哈希码。

4.2.2 接口Comparator

若一个类不能用于实现java.lang.Comparable,或者您不喜欢缺省的Comparable行为并想提供自己的排序顺序(可能多种排序方式),你可以实现Comparator接口,从而定义一个比较器。

(1)intcompare(Object o1, Object o2):对两个对象o1和o2进行比较,如果o1位于o2的前面,则返回负值,如果在排序顺序中认为o1和o2是相同的,返回0,如果o1位于o2的后面,则返回正值

与Comparable相似,0返回值不表示元素相等。一个0返回值只是表示两个对象排在同一位置。由Comparator用户决定如何处理。如果两个不相等的元素比较的结果为零,您首先应该确信那就是您要的结果,然后记录行为。

(2)booleanequals(Object obj): 指示对象obj是否和比较器相等。

该方法覆写Object的equals()方法,检查的是Comparator实现的等同性,不是处于比较状态下的对象。

4.3 接口SortedSet

“集合框架”提供了个特殊的Set接口:SortedSet,它保持元素的有序顺序。SortedSet接口为集的视图(子集)和它的两端(即头和尾)提供了访问方法。当您处理列表的子集时,更改视图会反映到源集。此外,更改源集也会反映在子集上。发生这种情况的原因在于视图由两端的元素而不是下标元素指定,所以如果您想要一个特殊的高端元素

4.4 类HashSet、TreeSet和LinkedHashSet

4.4.2 类HashSet

为快速查找而设计的Set。存入HashSet的对象必须定义hashCode()。生成自己的类时,Set需要维护元素的存储顺序,因此要实现Comparable接口并定义compareTo()方法。

HashSet是一个只有key的HashMap。

4.4.3 类TreeSet

保持次序的Set,底层为树结构。使用它可以从Set中提取有序的序列。

4.4.4 类LinkedHashSet

LinkedHashSet扩展HashSet,具有HashSet的查询速度,且内部使用链表维护元素的顺序(插入的次序)。于是在使用迭代器遍历Set时,迭代器按照元素的插入顺序来访问各个元素。它提供了一个可以快速访问各个元素的有序集合。同时,它也增加了实现的代价,因为哈希表元中的各个元素是通过双重链接式列表链接在一起的。

(1) LinkedHashSet():构建一个空的链接式哈希集

(2)LinkedHashSet(Collection c): 构建一个链接式哈希集,并且添加集合c中所有元素

(3)LinkedHashSet(int initialCapacity): 构建一个拥有特定容量的空链接式哈希集

(4)LinkedHashSet(int initialCapacity, float loadFactor):构建一个拥有特定容量和加载因子的空链接式哈希集。LoadFactor是0.0至1.0之间的一个数。

4.4.5 三者之间的区别

(1) HashSet采用散列函数对元素进行排序,这是专门为快速查询而设计的。

(2) TreeSet采用红黑树的数据结构进行排序元素。

(3) LinkedHashSet内部使用散列以加快查询速度,同时使用链表维护元素的次序,使得看起来元素是以插入的顺序保存的。

5. 接口Map

Map接口不是Collection接口的继承。Map接口用于维护键/值对(key/valuepairs)。该接口描述了从不重复的键到值的映射。

(1) 添加、删除操作:

Objectput(Object key, Object value):将互相关联的一个关键字与一个值放入该映像。如果该关键字已经存在,那么与此关键字相关的新值将取代旧值。方法返回关键字的旧值,如果关键字原先并不存在,则返回null

Objectremove(Object key): 从映像中删除与key相关的映射

voidputAll(Map t): 将来自特定映像的所有元素添加给该映像

voidclear(): 从映像中删除所有映射

键和值都可以为null。但是,您不能把Map作为一个键或值添加给自身。

(2) 查询操作:

Objectget(Object key):获得与关键字key相关的值,并且返回与关键字key相关的对象,如果没有在该映像中找到该关键字,则返回null

booleancontainsKey(Object key): 判断映像中是否存在关键字key

booleancontainsValue(Object value): 判断映像中是否存在值value

intsize(): 返回当前映像中映射的数量

booleanisEmpty():判断映像中是否有任何映射

(3) 视图操作:处理映像中键/值对组

SetkeySet(): 返回映像中所有关键字的视图集

因为映射中键的集合必须是唯一的,您用Set支持。你还可以从视图中删除元素,同时,关键字和它相关的值将从源映像中被删除,但是你不能添加任何元素。

Collectionvalues():返回映像中所有值的视图集

因为映射中值的集合不是唯一的,您用Collection支持。你还可以从视图中删除元素,同时,值和它的关键字将从源映像中被删除,但是你不能添加任何元素。

SetentrySet(): 返回Map.Entry对象的视图集,即映像中的关键字/值对

因为映射是唯一的,您用Set支持。你还可以从视图中删除元素,同时,这些元素将从源映像中被删除,但是你不能添加任何元素。

5.1 接口Map.Entry

Map的entrySet()方法返回一个实现Map.Entry接口的对象集合。集合中每个对象都是底层Map中一个特定的键/值对。

通过这个集合的迭代器,您可以获得每一个条目(唯一获取方式)的键或值,并对值进行更改。当条目通过迭代器返回后,除非是迭代器自身的remove()方法或者迭代器返回的条目的setValue()方法,其余对源Map外部的修改都会导致此条目集变得无效,同时产生条目行为未定义。

(1) Object getKey(): 返回条目的关键字

(2) Object getValue(): 返回条目的值

(3) Object setValue(Object value): 将相关映像中的值改为value,并且返回旧值

5.2 接口SortedMap

“集合框架”提供了个特殊的Map接口:SortedMap,它用来保持键的有序顺序。

SortedMap接口为映像的视图(子集),包括两个端点提供了访问方法。除了排序是作用于映射的键以外,处理SortedMap和处理SortedSet一样。

添加到SortedMap实现类的元素必须实现Comparable接口,否则您必须给它的构造函数提供一个Comparator接口的实现。TreeMap类是它的唯一一份实现。

因为对于映射来说,每个键只能对应一个值,如果在添加一个键/值对时比较两个键产生了0返回值(通过Comparable的compareTo()方法或通过Comparator的compare()方法),那么,原始键对应值被新的值替代。如果两个元素相等,那还好。但如果不相等,那么您就应该修改比较方法,让比较方法和equals()的效果一致。

(1)Comparator comparator():返回对关键字进行排序时使用的比较器,如果使用Comparable接口的compareTo()方法对关键字进行比较,则返回null

(2)Object firstKey(): 返回映像中第一个(最低)关键字

(3)Object lastKey(): 返回映像中最后一个(最高)关键字

(4)SortedMap subMap(Object fromKey, Object toKey):返回从fromKey(包括)至toKey(不包括)范围内元素的SortedMap视图(子集)

(5)SortedMap headMap(Object toKey): 返回SortedMap的一个视图,其内各元素的key皆小于toKey

(6)SortedSet tailMap(Object fromKey):返回SortedMap的一个视图,其内各元素的key皆大于或等于fromKey

5.3 抽象类AbstractMap

和其它抽象集合实现相似,AbstractMap类覆盖了equals()和hashCode()方法以确保两个相等映射返回相同的哈希码。如果两个映射大小相等、包含同样的键且每个键在这两个映射中对应的值都相同,则这两个映射相等。映射的哈希码是映射元素哈希码的总和,其中每个元素是Map.Entry接口的一个实现。因此,不论映射内部顺序如何,两个相等映射会报告相同的哈希码。

5.4 类HashMap

Map基于散列表的实现,取代了Hashtable。插入和查询label/value的开销是固定的,并且可以通过构造器设置容量和负载因子,以调整容器的性能。

使用散列的目的:想要使用一个对象来查找另一个对象。使用TreeSet或TreeMap也能实现此目的。另外,还可以自己实现一个Map,此时,必须提供Map.entrySet()方法来生成Map.Entry对象的Set。

使用散列的价值:速度,散列使得查询可以快速进行。散列将label保存在数组中方便快速查询,因为存储一组元素最快的数据结构是数组,用它来表示label的信息(后面有信息的描述),而不是label本身。通过label对象计算得到一个数字,作为数组的下标,这个数字就是散列码(即前面所述的信息)。该散列码具体是通过定义在基类Object中,可能由程序员自定义的类覆盖的hashCode()方法,即散列函数生成。为了解决数组容量带来的限制,可以使不同的label生成相同的下标,保存在一个链表list中,每一个链表就是数组的一个元素。查询label时就可以通过对list中的信息进行查找,当散列函数比较好,数组的每个位置中的list长度较短,则可以快速查找到数组元素list中的某个位置,提高了整体速度。

散列表中的slot通常称为bucket,为了使散列分步均匀,bucket的值一般取质数。但事实证明,质数实际上并不是散列bucket的理想容量,近来Java散列实现都使用2的幂,具体如何验证以后再续。

为了优化HashMap空间的使用,您可以调优初始容量和负载因子。

(1) HashMap(): 构建一个空的哈希映像

(2) HashMap(Map m): 构建一个哈希映像,并且添加映像m的所有映射

(3) HashMap(int initialCapacity): 构建一个拥有特定容量的空的哈希映像

(4) HashMap(int initialCapacity, float loadFactor):构建一个拥有特定容量和加载因子的空的哈希映像

5.4.1 hashCode()

当使用标准库中的类Integer作为HashMap的label时,程序能够正常运行,但是使用自己创建的类作为HashMap的label时,通常犯一个错误。

在HashMap中通过label查找value时,实际上是计算label对象地址的散列码来确定value的。一般情况下,我们是使用基类Object的方法hashCode()来生成散列码,它默认是使用对象的地址来计算的,因此由第一个对象newApple(5)和第二个对象newApple(5)生成的散列码是不同的,不能完成正确的查找。通常,我们可以编写自己的hashCode()方法来覆盖基类的原始方法,但与此同时,我们必须同时实现equals()方法来判断当前的label是否与表中存在的label相同。

正确的equals()方法满足五个条件:

(1) 自反性。对于任意的x,x.equals(x)一定返回true。

(2) 对称性。对于任意的x和y,如果y.equals(x)返回true,则x.equals(y)也返回true。

(3) 传递性。对于任意的x、y、z,如果有x.equals(y)返回true,y.equals(z)返回true,则x.equals(z)一定返回true。

(4) 一致性。对于任意的x和y,如果对象中用于等价比较的信息没有改变,那么无论调用x.equals(y)多少次,返回的结果应该保持一致,要么一直是true,要么一直是false。

(5)对任何不是null的x,x.equals(null)一定返回false。

Equals()比较的是对象的地址,如果要使用自己的类作为HashMap的label,必须同时重载hashCode()和equals()方法。

5.4.2 HashMap的性能因子

容量(capacity):散列表中bucket的数量。

初始化容量(initialcapacity): 创建散列表时bucket的数量。可以在构造方法中指定HashMap和HashSet的初始化容量。

尺寸(size):散列表中记录的数量。(数组的元素个数,非list中元素总和)

负载因子(loadfactor):尺寸/容量。负载因子为0,表示空的散列表,0.5表示半满的散列表。轻负载的散列表具有冲突少,适宜插入与查询的特点,但是使用迭代器遍历会比较慢。较高的负载会减少所需空间大小。当负载达到指定值时,容器会自动成倍地增加容量,并将原有的对象重新分配,存入新的bucket中,这个过程称为“重散列”。

5.4.3 重写hashCode()的关键

(1) 对同一个对象调用hashCode()都应该生成同样的值。

(2) hashCode()方法不要依赖于对象中易变的数据,当数据发生变化时,hashCode()就会生成一个不同的散列码,即产生了一个不同的label。

(3) hashCode()不应依赖于具有唯一性的对象信息,例如对象地址。

(4) 散列码应该更关心速度,而不是唯一性,因为散列码不必是唯一的。

(5) 好的hashCode()应该产生分步均匀的散列码。

5.4.4 HashMap的深度分析

HashMap可谓JDK的一大实用工具,把各个Object映射起来,实现了“键--值”对应的快速存取。但实际里面做了些什么呢?

在这之前,先介绍一下负载因子和容量的属性。大家都知道其实一个HashMap的实际容量等于因子*容量,其默认值是16×0.75=12;这个很重要,对效率很一定影响!当存入HashMap的对象超过这个容量时,HashMap就会重新构造存取表。这就是一个大问题,我后面慢慢介绍,反正,如果你已经知道你大概要存放多少个对象,最好设为该实际容量的能接受的数字。

两个关键的方法,put和get:

先有这样一个概念,HashMap是声明了Map,Cloneable,Serializable 接口,和继承了AbstractMap类,里面的Iterator其实主要都是其内部类HashIterator和其他几个iterator类实现,当然还有一个很重要的继承了Map.Entry的Entry内部类,由于大家都有源代码,大家有兴趣可以看看这部分,我主要想说明的是Entry内部类。它包含了hash,value,key和next这四个属性,很重要。put的源码如下

publicObject put(Object key, Object value) {

//这个就是判断键值是否为空,如果为空,它会返回一个staticObject 作为键值

//这就是为什么HashMap允许空键值的原因

Objectk = maskNull(key);

/*

hash通过key这个Object的hashcode进行hash,然后通过indexFor获得在Objecttable的索引值。

table?不要惊讶,其实HashMap也神不到哪里去,它就是用table来放的。最牛的就是用hash能正确的返回索引。

*/

inthash = hash(k);

inti = indexFor(hash, table.length);

/*

不知道大家有没有留意put其实是一个有返回的方法,它会把相同键值的put覆盖掉并返回旧的值!如下方法彻底说明了HashMap的结构,其实就是一个表加上在相应位置的Entry的链表:

for(Entry e = table[i]; e != null; e = e.next) {

  if(e.hash == hash && eq(k, e.key)) {

Objectoldvalue = e.value;

e.value= value; //把新的值赋予给对应键值。

e.recordAccess(this);//空方法,留待实现

returnoldvalue; //返回相同键值的对应的旧的值。

  }

}

modCount++;//结构性更改的次数

addEntry(hash,k, value, i); //添加新元素,关键所在!

returnnull; //没有相同的键值返回

}

我们把关键的方法拿出来分析:

voidaddEntry(int hash, Object key, Object value, int bucketIndex) {

/*

因为hash的算法有可能令不同的键值有相同的hash码并有相同的table索引,如:key=“33”和key=Objectg的hash都是-8901334,那它经过indexfor之后的索引一定都为i,这样在new的时候这个Entry的next就会指向这个原本的table[i],再有下一个也如此,形成一个链表,和put的循环对定e.next获得旧的值。到这里,HashMap的结构,大家也十分明白了吧?

*/

table[bucketIndex]= new Entry(hash, key, value, table[bucketIndex]);

if(size++ >= threshold) //这个threshold就是能实际容纳的量

resize(2* table.length); //超出这个容量就会将Objecttable重构

}

所谓的重构也不神,就是建一个两倍大的table(我在别的论坛上看到有人说是两倍加1,把我骗了),然后再一个个indexfor进去!注意!!这就是效率!!如果你能让你的HashMap不需要重构那么多次,效率会大大提高!

说到这里也差不多了,get比put简单得多,大家,了解put,get也差不了多少了。对于collections我是认为,它是适合广泛的,但不完全适合特有的,如果大家的程序需要特殊的用途,自己写吧,其实很简单。(作者是这样跟我说的,他还建议我用LinkedHashMap,我看了源码以后发现,LinkHashMap其实就是继承HashMap的,然后override相应的方法,有兴趣的同人,自己looklook)建个Objecttable,写相应的算法,就ok啦。

举个例子吧,像Vector,list啊什么的其实都很简单,最多就多了的同步的声明,其实如果要实现像Vector那种,插入,删除不多的,可以用一个Objecttable来实现,按索引存取,添加等。

如果插入,删除比较多的,可以建两个Objecttable,然后每个元素用含有next结构的,一个table存,如果要插入到i,但是i已经有元素,用next连起来,然后size++,并在另一个table记录其位置。

5.5 类HashTable

5.5.1 HashTable和HashMap的区别

HashTable的应用非常广泛,HashMap是新框架中用来代替HashTable的类,也就是说建议使用HashMap,不要使用HashTable。可能你觉得HashTable很好用,为什么不用呢?这里简单分析他们的区别。

(1) HashTable的方法是同步的,HashMap未经同步,所以在多线程场合要手动同步HashMap。但HashMap的同步问题可通过Collections的一个静态方法得到解决:MapCollections.synchronizedMap(Map m),这个方法返回一个同步的Map,这个Map封装了底层的HashMap的所有方法,使得底层的HashMap即使是在多线程的环境中也是安全的。

(2) HashTable不允许null值(key和value都不可以),HashMap允许null值(key和value都可以)。

(3) HashTable有一个contains(Objectvalue),功能和containsValue(Objectvalue)功能一样。

(4) HashTable使用Enumeration,HashMap使用Iterator。

(5) HashTable中hash数组默认大小是11,增加的方式是old*2+1。HashMap中hash数组的默认大小是16,而且一定是2的指数。

(6) 哈希值的使用不同,HashTable直接使用对象的hashCode,代码是这样的:

inthash = key.hashCode();

intindex = (hash & 0x7FFFFFFF) % tab.length;

而HashMap重新计算hash值,而且用与代替求模:

inthash = hash(k);

inti = indexFor(hash, table.length);

staticint hash(Object x) {

inth = x.hashCode();

h+= ~(h << 9);

h^= (h >>> 14);

h+= (h << 4);

h^= (h >>> 10);

returnh;

}

staticint indexFor(int h, int length) {

returnh & (length-1);

}

5.6 类TreeMap

查看label或label/value时,元素会被排序,其次序由Comparable或Comparator决定,因此查询所得到的结果是经过排序的。另外,它是唯一带有subMap()方法的Map具体类,即返回一个子树。它也是SortedMap接口的唯一实现,subMap()方法也是从该接口继承的。

TreeMap没有调优选项,因为该树总处于平衡状态。

(1)TreeMap():构建一个空的映像树

(2)TreeMap(Map m): 构建一个映像树,并且添加映像m中所有元素

(3)TreeMap(Comparator c): 构建一个映像树,并且使用特定的比较器对关键字进行排序

(4) TreeMap(SortedMap s): 构建一个映像树,添加映像树s中所有映射,并且使用与有序映像s相同的比较器排序

5.6.1 使用TreeMap进行中文排序

最近工作遇到需要按一个model中不同的列进行排序的问题,查了一下JDKAPI文档,发现,java中可以排序的工具类和接口共有五个SortedMap、SortedSetTreeMap、TreeSet和Collections,由于我要排序的是一系列model,所以,最后使用了TreeMap对象,而且TreeMap到最后的处理比较自由,可以直接返回TreeMap对象,也可以返回model的一个Collection对象。其它几个类的用法其实都是大同小异,如果java基础较好,看一下API文档很容易明白,只是Collection中需要显式调用sort()方法而已。

packageChineseSort;

importjava.util.Collection;

importjava.util.Iterator;

importjava.util.SortedMap;

importjava.util.TreeMap;

publicclass TestSort {

publicstatic void main(String[] args) {

TreeMapmap = new TreeMap();

for(inti = 0; i < 10; i ++) {

String s = "" + (int)(Math.random() * 1000);

map.put(s,s);

}

map.put("abcd","abcd");

map.put("Abc","Abc");

map.put("bbb","bbb");

map.put("BBBB","BBBB");

map.put("北京","北京");

map.put("中国","中国");

map.put("上海","上海");

map.put("厦门","厦门");

map.put("香港","香港");

map.put("碑海","碑海");

Collectioncol = map.values();

Iteratorit = col.iterator();

while(it.hasNext()){

System.out.println(it.next());

}

}

}

代码就不多作解释了,一看就明白,开始放进去10个整数随机数,然后是英文,然后是中文。运行结果如下:

132

205

287

295

399

410

411

464

670

73

Abc

BBBB

abcd

bbb

上海

中国

北京

厦门

碑海

香港

注意,这里的数字排序正常,而英文排序是区分大小写的,这个也是正常的,因为ASCII码中小写字母比大写字母靠后,中文排序则明显的不正确,碑和北明显应该在一起的,而且应该在最前面。这个主要是java中使用中文编码GB2312或者JBK时,char型转换成int型得过程出现了比较大的偏差,很多文章介绍过了,大家可以去网上找一下,这里不多说了,直接寻找解决方案。

Java中之所以出现偏差,主要是compare方法的问题,所以这里自己实现Comparator接口,而国际化的问题,使用Collator类来解决。这里先解决中文问题,代码如下:

importjava.text.CollationKey;

importjava.text.Collator;

importjava.util.Comparator;

publicclass CollatorComparator implements Comparator {

Collatorcollator = Collator.getInstance();

publicint compare(Object element1, Object element2){

CollationKeykey1 = collator.getCollationKey(element1.toString());

CollationKeykey2 = collator.getCollationKey(element2.toString());

returnkey1.compareTo(key2);

}

}

同时修改我们前面完成的TestSort类,找到:TreeMapmap = new TreeMap();

修改为:

CollatorComparatorcomparator = new CollatorComparator();

TreeMapmap = new TreeMap(comparator);

再次运行该类,运行结果如下:

325

62

653

72

730

757

874

895

909

921

Abc

abcd

bbb

BBBB

碑海

北京

上海

厦门

香港

中国

此时可以看到中文的排序已经完成正常。如果想不让英文区分大小写,则修改CollatorComparator类,找到

element1.toString()

修改为:

element1.toString().toLowerCase()

当然你改成转换成大写的也无所谓了,当然element2.toString()也要同时修改为element2.toString().toLowerCase()。再次运行结果如下:

207

353

656

659

770

789

857

861

931

984

Abc

abcd

bbb

BBBB

碑海

北京

上海

厦门

香港

中国

现在可以看到,排序已经完全符合我们的要求了。如果要反向排序也很容易,遍历的时候倒过来,或者你写两个Comparator的实现类,正向的排序就像我们前面所写的,反向排序就将return key1.compareTo(key2);修改成return -key1.compareTo(key2);,加了个负号,这里你可以直接加个符号看看效果,结果我就不写了,肯定中国是NumberOne。我还真没找到TreeMap里直接反向的方法,谁看到了告诉我。

最后一些要说明的,这里我就不再写实现的代码了,就是我们要实现的是根据model中的一个列进行排序,而我们测试代码只是简单的一些值,这个容易,遍历所有model,把要排序的列值取出来作为TreeMap的key,然后model放进去作为value就行了,这个很简单,如果想写成稍微通用点的,就使用反射机制,把取值方法封装一下就行了,然后把model对象和方法名扔进去就行了。至于value值重复的问题,也好办,只要value相同只要不是多列同时作为排序的键,那么他们之间的前后顺序无所谓,判断一下当前Map中是否含有该key值,存在,则新的key做成value+longtime就行了,就是加个时间戳(感觉用时间戳比较方便,其它的能区分的办法也行啦)。至于多列的排序,其实也容易,按照列的前后顺序firstvalue+secondvalue+......组成key放到TreeMap里照样OK。

5.7 类LinkedHashMap

LinkedHashMap扩展HashMap,以插入顺序将关键字/值对添加进链接哈希映像中。象LinkedHashSet一样,LinkedHashMap内部也采用双重链接式列表。

(1) LinkedHashMap():构建一个空链接哈希映像

(2) LinkedHashMap(Mapm): 构建一个链接哈希映像,并且添加映像m中所有映射

(3)LinkedHashMap(int initialCapacity): 构建一个拥有特定容量的空的链接哈希映像

(4)LinkedHashMap(int initialCapacity, float loadFactor):构建一个拥有特定容量和加载因子的空的链接哈希映像

(5)LinkedHashMap(int initialCapacity, float loadFactor,

5.8 类WeakHashMap

WeakKey映射,允许释放映射所指向的对象。当映射之外没有引用指向某个label时,此label可以被垃圾收集器回收。

5.9 类IdentityHashMap

使用==代替equals()对label进行比较的散列映射。

6. 数组

数组是Java语言内置的类型,除此之外,Java有多种保存对象引用的方式。Java类库提供了一套相当完整的容器类,使用这些类的方法可以保存和操纵对象。

6.1 数组的基本特性

数组与其它种类的容器(List/Set/Map)之间的区别在于效率、确定的类型和保存基本类型数据的能力。数组是一种高效的存储和随机访问对象引用序列的方式,使用数组可以快速的访问数组中的元素。但是当创建一个数组对象(注意和对象数组的区别)后,数组的大小也就固定了,当数组空间不足的时候就再创建一个新的数组,把旧的数组中所有的引用复制到新的数组中。

Java中的数组和容器都需要进行边界检查,如果越界就会得到一个RuntimeException异常。这点和C++中有所不同,C++中vector的操作符[]不会做边界检查,这在速度上会有一定的提高,Java的数组和容器会因为时刻存在的边界检查带来一些性能上的开销。

Java中通用的容器类不会以具体的类型来处理对象,容器中的对象都是以Object类型处理的,这是Java中所有类的基类。另外,数组可以保存基本类型,而容器不能,它只能保存任意的Java对象。

一般情况下,考虑到效率与类型检查,应该尽可能考虑使用数组。如果要解决一般化的问题,数组可能会受到一些限制,这时可以使用Java提供的容器类。

6.2 操作数组的实用功能

在java.util.Arrays类中,有许多static静态方法,提供了操作数组的一些基本功能:

equals()方法----用于比较两个数组是否相等,相等的条件是两个数组的元素个数必须相等,并且对应位置的元素也相等。

Fill()方法----用以某个值填充整个数组,这个方法有点笨。

asList()方法----接受任意的数组为参数,将其转变为List容器。

binarySearch()方法----用于在已经排序的数组中查找元素,需要注意的是必须是已经排序过的数组。当Arrays.binarySearch()找到了查找目标时,该方法将返回一个等于或大于0的值,否则将返回一个负值,表示在该数组目前的排序状态下此目标元素所应该插入的位置。负值的计算公式是“-x-1”。X指的是第一个大于查找对象的元素在数组中的位置,如果数组中所有的元素都小于要查找的对象,则x=a.size()。如果数组中包含重复的元素,则无法保证找到的是哪一个元素,如果需要对没有重复元素的数组排序,可以使用TreeSet或者LinkedHashSet。另外,如果使用Comparator排序了某个对象数组,在使用该方法时必须提供同样的Comparator类型的参数。需要注意的是,基本类型数组无法使用Comparator进行排序。

Sort()方法----对数组进行升序排序。

在Java标准类库中,另有static方法System.arraycopy()用来复制数组,它针对所有类型做了重载。

6.3 数组的排序

在Java1.0和1.1两个版本中,类库缺少基本的算法操作,包括排序的操作,Java2对此进行了改善。在进行排序的操作时,需要根据对象的实际类型执行比较操作,如果为每种不同的类型各自编写一个不同的排序方法,将会使得代码很难被复用。一般的程序设计目标应是“将保持不变的事物与会发改变的事物相分离”。在这里,不变的是通用的排序算法,变化的是各种对象相互比较的方式。

Java有两种方式来实现比较的功能,一种是实现java.lang.Comparable接口,该接口只有一个compareTo()方法,并以一个Object类为参数,如果当前对象小于参数则返回负值,如果相等返回零,如果当前对象大于参数则返回正值。另一种比较方法是采用策略(strategy)设计模式,将会发生变化的代码封装在它自己的类(策略对象)中,再将策略对象交给保持不变的代码中,后者使用此策略实现它的算法。因此,可以为不同的比较方式生成不同的对象,将它们用在同样的排序程序中。在此情况下,通过定义一个实现了Comparator接口的类而创建了一个策略,这个策略类有compare()和equals()两个方法,一般情况下实现compare()方法即可。

使用上述两种方法即可对任意基本类型的数组进行排序,也可以对任意的对象数组进行排序。再提示一遍,基本类型数组无法使用Comparator进行排序。

Java标准类库中的排序算法针对排序的类型进行了优化——针对基本类型设计了“快速排序”,针对对象设计的“稳定归并排序”。一般不用担心其性能。

Similar Posts:

  • JAVA安全体系结构分析

    http://www.blogjava.net/Archangelsy/articles/142841.html JAVA安全体系结构分析 下图显示了 JAVA 安全体系结构的标准组件.在图的下半部分,是 JAVA2 安全体系结构的核心和 JAVA 加密体系结构( JCA , Java Cryptography Architecture ),两者构成 JAVA2 平台所带的 JAVA2 安全平台.在图的上半部分,是独立于 JAVA2 平台而又与 JAVA2 平台的不同方面相关的 JAVA 安全扩

  • Java 集合体系详解——List体系有序集合

    引言 面向对象语言对事物的体现必然是以对象的形式,Java工程师为了方便多多个对象的操作,就对对象进行存储,集合就是存储对象的一种方式,他们的底层都是基于不同的数据结构.当然集合和数组一样都是容器,数组也是可以存储对象的,但是数组长度一经初始化长度就是固定的,而集合长度是可变的,数组只能用于存储相同类型的对象,而集合可以存储不同类型的对象,数据多了用对象封装,对象多了用集合存. 一Java 集合类体系结构 Java的所有的集合体系都是实现了Collection接口,所有通用的 Collectio

  • java_集合体系之总体框架——01

    java_集合体系之总体框架--01 摘要:相对于数组的不能扩展.集合为我们提供的大量以不同数据结构:集合.链表.队列.栈.数组等存储数据的类.和操作这些数据的方法.本体系试图通过分析这些类的结构.源码.使用方法.之间的区别.以及使用场合等来深入了解java集合体系. 一:总体框架体系图 二:简单说明 从总体结构图中可以看出java的集合体系可以分为三大块: 1)Collection: collection本身是一个被高度抽象化出来的接口.用于定义一部分集合类所应该共有的属性和行为.Collec

  • 牛刀小试 - 浅析Java集合框架的使用

    基本概述 Java中的集合框架与数组类似,都是用于存储多个同一类型数据的容器. 但是对于数组的使用,会因为数组本身的特性会导致一些使用限制,例如: 数组要求在构造时,就必须确定数组的长度.所以如果想要存放的数据个数不确定,数组就无法使用. 于是促使了集合框架的诞生,与数组相比,集合框架最大特点在于: 集合框架下的容器类只能存放对象类型数据:而数组支持对基本类型数据的存放. 任何集合框架下的容器类,其长度都是可变的.所以不必担心其长度的指定问题. 集合框架下的不同容器了底层采用了不同的数据结构实现

  • JAVA——集合(List延伸)

    Collection List: 元素是有序的,元素可以重复.因为该集合体系又索引. ArrayList(jdk1.2以后) 底层的数据结构使用的是数组结构.特点:查询速度很快,但是增删稍慢.线程不同步. LinkedList 底层使用的是链表的数据结构.特点:增删速度很快,查询稍慢. Vector(jdk1.0以后) 底层是数组数据结构.线程同步,被ArrayList替代了. Set: 元素是无序的,元素不可以重复. List: 特有方法.凡是可以操作角标的方法都是该体系特有的方法. 增 ad

  • 黑马程序员-Java集合框架小结

    ------- android培训.java培训.期待与您交流! ---------- 什么是集合 面向对象语言对事物的体现都是以对象的形式存在,所以为了方便对多个对象的操作,就对对象进行存储,集合就是存储对象最常用的一种方式. 集合有什么的特点? 集合只用于存储对象,集合长度是可变的,集合可以存储不同类型的对象. 怎么用?什么时候用? 集合体系比较庞大. Collection 首先我们要知道集合与数组是不同: 其中集合是可变长度的,集合中用size(); 而数组是固定长度的,数组中是用leng

  • arcims体系结构分析

    arcims体系结构分析:1. arcims的体系结构 当你安装完arcims软件以后,已经包括了上图中的ArcIMS应用服务器,ArcIMS空间服务器两个中间件.当然还有Monitor, Tasker:工具软件admin,author:各种连接器servlet,javaconnector的代码等. 大家可以看到,arcXML在应用服务器和空间服务器之间传递.所以空间服务器是这些中间件中压力最大的. 表现层:html,applet,ocx控件.也可以是c/s的任何桌面程序. 业务逻辑层:arci

  • 黑马程序员-Java集合

    ------Java培训.Android培训.iOS培训..Net培训.期待与您交流! ------- 一.Collection Collection是集合框架中的常用接口.其下有两个子接口:List(列表),Set(集). 所属关系: Collection |--List//元素是有序的,元素可以重复.因为该集合体系有索引. |--Set//元素是无序的,元素不可以重复. (一).Collection接口中的常见操作 1.添加元素 add(Objectobj); //add方法的参数类型是Ob

  • java_集合体系之HashMap详解、源码及示例——09

    java_集合体系之HashMap详解.源码及示例--09 一:HashMap结构图 简单说明: 1.上图中虚线且无依赖字样.说明是直接实现的接口 2.虚线但是有依赖字样.说明此类依赖与接口.但不是直接实现接口 3.实线是继承关系.类继承类.接口继承接口 4.继承AbstractMap.以键值对的形式存储.操作元素 5.实现Serielazable接口.允许使用ObjectInputStream/ObjectOutputStream读取/写入 6.实现Cloneable接口.允许克隆HashMa

  • [置顶] java学习之路 之 Java集合框架

    Java集合框架 一方面, 面向对象语言对事物的体现都是以对象的形式,为了方便对多个对象的操作,就要对对象进行存储.另一方面,使用数组存储对象方面具有一些弊端,而Java 集合就像一种容器,可以动态地把多个对象的引用放入容器中.Java 集合类可以用于存储数量不等的多个对象.可以将它简单地看作是一个可变长度的Object数组. Java 集合类可以用于存储数量不等的多个对象.可以将它简单地看作是一个可变长度的Object数组. Java 集合可分为 Collection 和 Map 两种体系 C

Tags: