您的位置:首页 > 编程语言 > Java开发

Java核心技术卷I:基础知识(原书第8版):13.2 具体的集合

2014-10-24 09:19 585 查看
铁文整理

13.2 具体的集合

这里并不打算更加详细地介绍所有的接口,但我们认为先介绍一下Java类库提供的具体数据结构还是很有用途的。透彻地介绍了人们想使用的类之后,再回过头研究一些抽象的概念,看一看集合框架组织这些类的方式,表13-1展示了Java类库中的集合,并简要描述了每个集合类的用途(鉴于简单起见,省略了将在第十四章中介绍的线程安全集合)。在表13-1中,除了以Map结尾的类之外,其他类都实现了Collection接口。而以Map结尾的类实现了Map接口。有关Map接口的内容将在稍后介绍。

表13-1 Java库中的具体集合

集合类型

描述

ArrayList

一种可以动态增长和缩减的索引序列

LinkedList

一种可以在任何位置进行高效地插入和删除操作的有序序列

ArrayDeque

一种用循环数组实现的双端队列

HashSet

一种没有重复元素的无序集合

TreeSet

一种有序集

EnumSet

一种包含枚举类型值的集

LinkedHashSet

一种可以记住元索插入次序的集

PriorityQueue

一种允许高效删除最小元素的集合

HashMap

一种存储键/值关联的数据结构

TreeMap

一种键值有序排列的映射表

EnumMap

一种键值槭卞枚举类型的映射表

LinkedHasbMap

一种可以记住键/值项添加次序的映射表

WeakHashMap

一种其值无用武之地后可以被垃圾回收器回收的映射表

IdentityHashMap

一种用==,而不是用equals比较键值的映射表

13.2.1
链表

在本书中,有很多示例已经使用了数组以及动态的ArrayList类。然而,数组和数组列表都有一个重大的缺陷。这就是从数组的中间位置删除一个元素要付出很大的代价,其原因是数组中处于被删除元素之后的所有元素都要向数组的前端移动(见图13-4),在数组中间的位置上插入一个元素也是如此。

另外一个大家非常熟悉的数据结构——链表(linked list)解决了这个问题,尽管数组在连续的存储位置上存放对象引用,但链表却将每个对象存放在独立的节点中。每个节点还存放着序列中下一个节点的引用。在Java程序设计语言中,所有链表实际上都是双向链接的——即每个节点还存放着指向前驱节点的引用(见图13-5)。

从链表中间删除一个元素是一个很轻松的操作,即需要对被删除元素附近的节点更新一下即可(见图13-6)。

你也许曾经在数据结构课程中学习过如何实现链表的操作。在链表中添加或删除元素时,绕来绕去的指针可能已经给人们留下了极坏的印象。如果真是如此的话,就会为Java集合类库提供一个类LinkedList而感到拍手称快。

在下面的代码示例中,先添加3个元素,然后再将第2个元素删除:

List<String> staff = new LinkedList<String>();
// LinkedList implements List
staff.add("Amy");
staff.add("Bob");
staff.add("Carl");
Iterator iter = staff.iterator();
String first = iter.next();
// visit first element
String second = iter.next();
// visit second element
iter.remove(); // remove last visited element

但是,链表与泛型集合之间有一个重要的区别。链表是一个有序集合。每个对象的位置十分重要。LinkedList.add方法将对象添加到链表的尾部。但是,常常需要将元素添加到链表的中间。由于迭代器是描述集合中位置的,所以这种依赖于位置的add方法将由迭代器负责。只有对自然有序的集合使用迭代器添加元素才有实际意义。例如,下一节将要讨论的集(set)类型,其中的元素完全无序。因此,在Iterator接口中就没有add方法。相反地,集合类库提供了子接口ListIterator,其中包含add方法:

interface ListIterator<E>
extends Iterator<E> {
void add(E element);
}
与Collection.add不同,这个方法不返回boolean类型的值,它假定添加操作总会改变链表。另外,ListIterator接口有两个方法,可以用来反向遍历链表。

E previous();
boolean hasPrevious();

与next方法一样,previous方法返回越过的对象。

LinkedList类的listIterator方法返回一个实现了ListIterator接口的迭代器对象。

ListIterator<String> iter = staff.listIterator();

add方法在迭代器位置之前添加一个新对象。例如,下面的代码将越过链表中的第一个元素,并在第二个元素之前添加“Juliet”(见图13-7):

List<String> staff = new LinkedList<String>();
staff.add("Amy");
staff.add("Bob");
staff.add("Carl");
ListIterator iter = staff.listIterator();
iter.next(); // skip pass first element
iter.add("Juliet");

如果多次调用add方法,将按照提供的次序把元素添加到链表中。它们被依次添加到迭代器当前位置之前。

当用一个刚刚由iterator方法返回,并且指向链表表头的迭代器调用add操作时,新添加的元素将变成列表的新表头。当迭代器越过链表的最后一个元素时(即hasNext返回false),添加的元素将变成列表的新表尾。如果链表有n个元素,有n+1个位置可以添加新元素。这些位置与迭代器的n+1个可能的位置相对应。例如,如果链表包含3个元素,A、B、C,就有4个位置(标有|)可以插入新元素:

|ABC

A|BC

AB|C

ASC|

注释:在用“光标”类比时要格外小心。remove操作与BACKSPACE键的工作方式不太一样。在调用next之后,remove方法确实与BACKSPACE键一样删除了迭代器左侧的元素。但是,如果调用previous就会将右侧的元素删除掉,并且不能在同一行中调用两次remove。add方法只依赖于迭代器的位置,而remove方法依赖于迭代器的状态。

最后需要说明,set方法用一个新元素取代调用next或previous方法返回的上一个元素。例如,下面的代码将用一个新值取代链表的第一个元素:

Listlterator<String> iter = list.listIterator();
String oldValue = iter.next();
// returns first element
iter.set(newValue); // sets first element to newValue

可以想像,如果在某个迭代器修改集合时,另一个迭代器对其进行遍历,一定会出现混乱的状况。例如,一个迭代器指向另一个迭代器刚刚删除的元素前面,现在这个迭代器就是无效的,并且不应该再使用。链表迭代器的设计使它能够检测到这种修改。如果迭代器发现它的集合被另一个迭代器修改了,或是被该集合自身的方法修改了,就会抛出一个ConcurrentModificationException异常。例如,看一看下面这段代码:

List<String> list = ...;
ListIterator<String> iterl = list.listIterator();
ListIterator<String> iter2 = list.listIterator();
iter1.next();
iter1.remove();
iter2.next(); // Throws ConcurrentModificationException

由于iter2检测出这个链表被从外部修改了,所以对iter2的调用抛出了一个ConcurrentModificationException异常。

为了避免发生并发修改的异常,请遵循下述简单规则:可以根据需要给容器附加许多的迭代器,但是这些迭代器只能读取列表。另外,再单独附加一个既能读又能写的迭代器。

有一种简单的方法可以检测到并发修改的问题。集合可以跟踪改写操作(诸如添加或删除元素)的次数。每个迭代器都维护一个独立的计数值。在每个迭代器方法的开始处检査自己改写操作的计数值是否与集合的改写操作计数值一致。如果不—致,抛出一个ConcurrentModificationException。

注释:对于并发修改列表的检测有一个奇怪的例外。链表只负责跟踪对列表的结构性修改,例如,添加元素、刪除元素。set操作不被视为结构性修改。可以将多个迭代器附加给一个链表,所有的迭代器都调用set方法对现有节点的内容进行修改。在本章后面所介绍的Collection类的许多算法都需要使用这个功能。

现在已经介绍了LinkedList类的各种基本方法。可以使用LinkedList类从前后两个方向遍历链表中的元素,并可以添加、删除元素。

在上一节已经看到,Collection接口中声明了许多用于对链表操作的有用方法。其中大部方法都是在LinkedList类的超类AbstractCollection中实现的。例如,toString方法调用了所有元素的toString,并产生了一个很长的格式为[A,B,C]的字符串。这为调试工作提供了便利。可以使用contains方法检测某个元素是否出现在链表中。例如,如果链表中包含一个等于“Harry”的字符串,调用staff.contains("Harry")后将会返回true。

在Java类库中,还提供了许多在理论上存在一定争议的方法。链表不支持快速地随机访问。如果要査看链表中第n个元素,就必须从头开始,越过n-1个元素。没有捷径可走。鉴于这个原因,在程序需要采用整数索引访问元素时,程序员通常不选用链表。

尽管如此,LinkedList类还是提供了一个用来访问某个特定元素的get方法:

List<String> list = ...;
String obj = list.get(n);

当然,这个方法的效率并不太高。如果发现自己正在使用这个方法,说明有可能对于所要解决的问题使用了错误的数据结构。

绝对不应该使用这种让人误解的随机访问方法来遍历链表。下面这段代码的效率极低:

for (int i = 0; i < list.size(); i++)
doSomethingWith(list.get(i));

每次查找一个元素都要从列表的头部重新开始搜索。LinkedList对象根本不做任何缓存位置信息的操作。

注释:get方法做了微小的优化:如果索引大于size()/2就从列表尾端开始搜索元素。

列表迭代器接口还有一个方法,可以告之当前位置的索引。实际上,从概念上讲,由于Java迭代器指向两个元素之间的位置,所以可以同时产生两个索引:nextIndex方法返回下一次调用next方法时返回元素的整数索引,previousIndex方法返回下一次调用previous方法时返回元素的整数索引。当然,这个索引只比nextIndex返回的素引值小1。这两个方法的效率非常高,这是因为迭代器保持着当前位置的计数值。最后需要说一下,如果有一个整数索引n,list.listIterator(n)将返回一个迭代器,这个迭代器指向索引为n的元素前面的位置。也就是说,调用next与调用list.get(n)会产生同一个元素,只是获得这个迭代器的效率比较低。

如果链表中只有很少几个元素,就完全没有必要为get方法和set方法的开销而烦恼。但是,为什么要优先使用链表呢?使用链表的唯一理由是尽可能地减少在列表中间插入或删除元素所付出的代价。如果列表只有少数几个元素,就完全可以使用ArrayList。

我们建议避免使用以整数索引表示链表中位置的所有方法。如果需要对集合进行随机访问,就使用数组或ArrayList而不要使用链表。

例13-1中的程序使用的就是链表。它简单地创建了两个链表,将它们合并在一起,然后从第二个链表中每间隔一个元素刪除一个元素,最后测试removeAll方法。建议跟踪一下程序流程,并要特别注意迭代器。从这里会发现绘制一个下面这样的迭代器位置示意图是非常有用的:

|ACE |BDFC

A|CE |BDFC

AB|CE B|DFC

……

注意调用

System.out.println(a);

通过调用AbstractCollection类中的toString方法打印出链表a中的所有元素。

例13-1 LinkedListTest.java
import java.util.*;

/**
* This program demonstrates operations on linked lists.
*
* @version 1.10 2004-08-02
* @author Cay Horstmann
*/
public
class
LinkedListTest {
public
static void
main(String[] args) {
List<String> a = new LinkedList<String>();
a.add("Amy");
a.add("Carl");
a.add("Erica");

List<String> b = new LinkedList<String>();
b.add("Bob");
b.add("Doug");
b.add("Frances");
b.add("Gloria");

// merge the words from b into a

ListIterator<String> aIter = a.listIterator();
Iterator<String> bIter = b.iterator();

while (bIter.hasNext()) {
if (aIter.hasNext())
aIter.next();
aIter.add(bIter.next());
}

System.out.println(a);

// remove every second word from b

bIter = b.iterator();
while (bIter.hasNext()) {
bIter.next(); // skip one element
if (bIter.hasNext()) {
bIter.next();
// skip next element
bIter.remove();
// remove that element
}
}

System.out.println(b);

// bulk operation: remove all words in b from a

a.removeAll(b);

System.out.println(a);
}
}
API:java.util.List<E> 1.2

ListIterator<E> listIterator():返回一个列表迭代器,以便用来访问列表中的元素。

ListIterator<E> listIterator(int index):返回一个列表迭代器,以使用来访问列表中的元素,这个元素是第一次调用next返回的给定索引的元素。

void add(int i, E element):在给定位置添加一个元素。

void addAll(int i, Collection<? extends E> elements):将某个集合中的所有元素添加到给定位置。

E remove(int i):刪除给定位置的元素并返回这个元素。

E get(int i):获取给定位置的元素。

E set(int i, E element):用新元素取代给定位置的元素,并返回原来那个元素。

int indexOf(Object element):返回与指定元素相等的元素在列表中第一次出现的位置。如果没有这样的元素将返回-1。

int lastIndexOf(Object element):返回与指定元素相等的元素在列表中最后一次出现的位置,如果没有这样的元素将返回-1。

API:java.util.ListIterator<E> 1.2

void add(E newElement):在当前位置前添加一个元素。

void set(E newElement):用新元素取代next或previous上次访问的元素。如果在next或previous上次调用之后列表结构被修改了,将抛出一个IIlegalStateException异常。

boolean hasPrevious():当反向迭代列表时,还有可供访问的元素,返回true。

E previous():返回前一个对象。如果已经到达了列表的头部,就抛出一个NoSuchElementException异常。

int nextIndex():返回下一次调用next方法时将返回的元素索引。

int previousIndex():返回下一次调用previous方法时将返回的元素索引。

API:java. util .LinkedList<E> 1.2

LinkedList():构造一个空链表。

LinkedList(Conection<? extends E> elements):构造一个链表,并将集合中所有的元素添加到这个链表中。

void addFirst(E element)

void addLast(E element):将某个元素添加到列表的头部或尾部。

E getFirst()

E getLast():返回列表头部或尾部的元素。

E removeFirst()

E removeLast():删除并返回列表头部或尾部的元素。

13.2.2
数组列表

在上一节中,介绍了List接口和实现了这个接口的LinkedList类。List接口用于描述一个有序集合,并且集合中每个元素的位置十分重要。有两种访问元素的协议:一种是用迭代器,另一种是用get和set方法随机地访问每个元素。后者不适用于链表,但对数组却很有用。集合类库提供了一种大家熟悉的ArrayList类,这个类也实现了List接口,ArrayList封装了一个动态再分配的对象数组。

注释:对于一个经验丰富的Java程序员来说,在需要动态数组时,可能会使用Vector类。为什么要用ArrayList取代Vector呢?原因很简单:Vector类的所有方法都是同步的,可以由两个线程安全地访问一个Vector对象。但是,如果由一个线程访问Vector,代码要在同步操作上耗费大量的时间。这种情况还是很常见的,而ArrayList方法不是同步的,因此,建仪在不需要同步时使用ArrayList,而不要使用Vector。

13.2.3
散列集

链表和数组可以按照人们的意愿排列元素的次序。但是,如果想要査看某个指定的元素,却又忘记了它的位置,就需要访问所有元素,直到找到为止。如果集合中包含的元素很多,将会消耗很多时间,如果不在意元素的顺序,可以有几种能够快速査找元素的数据结构,其缺点是无法控制元素出现的次序,它们将按照有利于其操作目的的原则组织数据。

有一种众所周知的数据结构,可以快速地査找所需要的对象,这就是散列表。散列表为每个对象计算一个整数,称为散列码(hash table)。散列码是由对象的实例域产生的一个整数。更准确地说,具有不同数据域的对象将产生不同的散列码。表13-2列出了几个散列码的示例,它们是由String类的hashCode方法产生的。

如果自定义类,就要负责实现这个类的hashCode方法。有关hashCode方法的详细内容请参看第5章。注意,自己实现的hashCode方法应该与equa]s方法兼容,即如果a.equals(b)为true,a与b必须具有相同的散列码。

现在,最重要的问题是散列码要能够快速地计算出来,并且这个计算只与要散列的对象状态有关,与散列表中的其他对象无关。

在Java中,散列表用链表数组实现。每个列表被称为桶(bucket)(参看图13-8)。要想査找表中对象的位置,就要先计算它的散列码,然后与桶的总数取余,所得到的结果就是保存这个元素的桶的索引。例如,如果某个对象的散列码为76268,并且有128个桶,对象应该保存在第108号桶中(76268除以128余108)。或许会很幸运,在这个桶中没有其他元素,此时将元素直接插入到桶中就可以了。当然,有时候会遇到桶被占满的情况,这也是不可避免的。这种现象被称为散列冲突(hash
collision)。这时,需要用新对象与桶中的所有对象进行比较,査看这个对象是否已经存在。如果散列码是合理且随机分布的,桶的数目也足够大,需要比较的次数就会很少。

图13-8 散列表
如果想更多地控制散列表的运行性能,就要指定一个初始的桶数。桶数是指用于收集具有相同散列值的桶的数目。如果要插入到散列表中的元素太多,就会增加冲突的可能性,降低运行性能。

如果大致知道最终会有多少个元素要插入到散列表中,就可以设置桶数。通常,将桶数设置为预计元素个数的75%~150%。有些研究人员认为:尽管还没有确凿的证据,但最好将桶数设置为一个素数,以防键的集聚。标准类库使用的桶数是2的幂,默认值为16(为表大小提供的任何值都将被自动地转换为2的下一个幂)。

当然,并不是总能够知道需要存储多少个元素的,也有可能最初的估计过低。如果散列表太满,就需要再散列(rehashed)。如果要对散列表再散列,就需要创建一个桶数更多的表,并将所有元素插入到这个新表中,然后丢弃原来的表。装填因子(load factor)决定何时对散列表进行再散列。例如,如果装填因子为0.75(默认值),而表中超过75%的位置已经填入元素,这个表就会用双倍的桶数自动地进行再散列。对于大多数应用程序来说,装填因子为75%是比较合理的。

散列表可以用于实现几个重要的数据结构。其中最简单的是set类型。set是没有重复元素的元素集合。set的add方法首先在集中査找要添加的对象,如果不存在,就将这个对象添加进去。

Java集合类库提供了一个HashSet类,它实现了基于散列表的集。可以用add方法添加元素。contains方法已经被重新定义,用来快速地査看某个元素是否已经出现在集中。它只在某个桶中査找元素,而不必査看集合中的所有元素。

散列集迭代器将依次访问所有的桶。由于散列将元素分散在表的各个位置上,所以访问它们的顺序几乎是随机的。只有不关心集合中元素的顺序时才应该使用HashSet。

本节末尾的示例程序(例13-2)将从System.in读取单词,然后将它们添加到集中,最后,再打印出集中的所有单词。例如,可以将Alice in Wonderland(可以从http://www.gutenberg.net)的文本输入到这个程序中,并通过下列命令运行:

java SetTest < alice30.txt

这个程序将读取输入的所有单词,并且将它们添加到散列集中。然后遍历散列集中的不同单词,最后打印出单词的数量(Alice in Wonderland共有5909个不同的单词,包括开头的版权声明)。单词以随机的顺序出现。

警告:在更改集中的元素时要格外小心。如果元素的散列码发生了改变,元素在数据结构中的位置也会发生变化。

例13-2 SetTest.java
import java.util.*;

/**
* This program uses a set to print all unique words in System.in.
*
* @version 1.10 2003-08-02
* @author Cay Horstmann
*/
public
class
SetTest {
public
static void
main(String[] args) {
Set<String> words = new HashSet<String>();
// HashSet implements Set
long totalTime = 0;

Scanner in = new Scanner(System.in);
while (in.hasNext()) {
String word = in.next();
long callTime = System.currentTimeMillis();
words.add(word);
callTime = System.currentTimeMillis() - callTime;
totalTime += callTime;
}

Iterator<String> iter = words.iterator();
for (int i = 1; i <= 20&& iter.hasNext(); i++)
System.out.println(iter.next());
System.out.println(". . .");
System.out.println(words.size() +
" distinct words. " + totalTime +
" milliseconds.");
}
}
API:java.util.HashSet<E> 1.2

HashSet()

HashSet(Collection<? extends E> elements):构造一个散列集,并将集合中的所有元素添加到这个散列集中。

HashSet(int initialCapacity):构造一个空的具有指定容量(桶数)的散列集。

HashSet(int initialCapacity, float loadFactor):构造一个具有指定容量和装填因子(一个0.0~1.0之间的数值,确定散列表填充的百分比,当大于这个百分比时,散列表进行再散列)的空散列集。

API:java.lang.Object 1.0

int hashCode():返回这个对象的散列码。散列码可以是任何整数,包括正数或负数。equals和hashCode的定义必须兼容,即如果x.equals(y)为true,x.hashCode()必须等于y.hashCode()。

13.2.4
树集

TreeSet类与散列集十分类似,不过,它比散列集有所改进。树集是一个有序集合。可以以任意顺序将元素插入到集合中。在对集合进行遍历时,每个值将自动地按照排序后的顺序呈现。例如,假设插入3个字符串,然后访问添加的所有元素。

SortedSet<String> sorter = new TreeSet<String>();
// TreeSet implements SortedSet
sorter.add("Bob");
sorter.add("Amy");
sorter.add("Carl");
for (String s : sorter)
System.out.println(s);

这时,每个值将按照顺序打印出来:Amy Bob Carl。正如类名所示,排序是用树结构完成的(当前实现使用的是红黑树(red-black tree),有关红黑树的详细介绍请参看《Introduction
to Algorithms》,作者是Thomas Cormen、Charles Leiserson、Ronald Rivest和Clifford Stein。《算法导论》,已由机械工业出版社出版)。每次将一个元素添加到树中时,都被放置在正确的排序位置上,因此,迭代器总是以排好序的顺序访问每个元素。

将一个元素添加到树中要比添加到散列表中慢,但是,与将元素添加到数组或链表的正确位置上相比还是快很多的。如果树中包含n个元素,査找新元素的正确位置平均需要log2n次比较。例如,如果一棵树包含了1000个元素,添加一个新元素大约需要比较10次。

因此,将一个元素添加到TreeSet中要比添加到HashSet中慢。请参看表13-3。不过,TreeSet可以自动地对元素进行排序。

API:java.util.TreeSet<E> 1.2

TreeSet():构造一个空树集。

TreeSet(Collection<? extends E> elements):构造一个树集,并将集合中的所有元素添加到树集中。

13.2.5
对象的比较

TreeSet如何知道希望元素怎样排列呢?在默认情况时,树集假定插入的元素实现了Comparable接口。这个接口定义了一个方法:

public
interface
Comparable<T> {
int compareTo(T other);
}
如果a与b相等,调用a.compareTo(b)一定返回0;如果排序后a位于b之前,则返回负值;如果a位于b之后,则返回正值。具体返回什么值并不重要,关键是符号(>0、0或<0)。有些标准的Java平台类实现了Comparable接口,例如,String类,这个类的compareTo方法依据字典序(有时称为词典序)对字符串进行比较。

如果要插入自定义的对象,就必须通过实现Comparable接口自定义排列顺序。在Object类中,没有提供任何compareTo接口的默认实现。

例如,下面的代码展示了如何用部件编号对ltem对象进行排序:

class Item
implements Comparable<Item> {
public
int
compareTo(Item other) {
return partNumber - other.partNumber;
}
}

如果对两个正整数进行比较,就像上面示例中的部件编号,就可以直接地返回它们的差。如果第一项位于第二项的前面,就返回负值;如果部件编号相同就返回0;否则返回正值。

警告:只有整数在一个足够小的范围内,才可以使用这个技巧,如果x是一个较大的正整教。y是一个较大的负整数,x-y有可能会溢出。

然而,使用Comparable接口定义排列排序显然有其局限性。对于一个给定的类只能够实现这个接口一次。如果在一个集合中需要按照部件编号进行排序,在另一个集合中却要按照描述信息进行排序,该怎么办呢?另外,如果需要对一个类的对象进行排序,而这个类的创建者又没有费心实现Comparable接口,又该怎么办呢?

在这种情况下,可以通过将Comparator对象传递给TreeSet构造器来告诉树集使用不同的比较方法。Comparator接口声明了一个带有两个显式参数的compare方法:

public
interface
Comparator<T> {
int compare(T a, T b);
}

与compareTo方法一样,如果a位于b之前compare方法则返回负值,如果a和b相等则返回0,否则返回正值。

如果按照描述信息进行排序,就直接定义一个实现Comparator接口的类:

class ItemComparator
implements Comparator<Item> {
public
int
compare(Item a, Item b) {
String descrA = a.getDescription();
String descrB = b.getDescription();
return descrA.compareTo(descrB);
}
}

然后将这个类的对象传递给树集的构造器:

ItemComparator comp = new ItemComparator();
SortedSet<Item> sortByOescription = new TreeSet<Item>(comp);

如果构造了一棵带比较器的树,就可以在需要比较两个元素时使用这个对象。

注意,这个比较器没有任何数据。它只是比较方法的持有器。有时将这种对象称为函数对象。函数对象通常被定义为“瞬时的”,即匿名内部类的实例:

SortedSet<Item> sortByOescription = new TreeSet<Item>(
new Comparator<Item>() {
public
int
compare(Item a, Item b) {
String descrA = a.getDescription();
String descrB = b.getDescription();
return descrA.compareTo(descrB);
}
});

注释:实际上,Comparator<T>接口声明了两个方法:compare和equals。当然,每一个类都有一个equals方法;因此,为这个接口声明再添加一个equals方法似乎没有太大好处。API文档解释说,不需要覆盖equals方法,但这样做可能会在某些情况下提高性能。例如,如果从另一个集合添加元素,这个由使用相同比较器的另外一个集添加元素,TreeSet类中的addAll方法的效率会更高。

回头看一看表13-3可能会疑虑:是否总是应该用树集取代散列集。毕竟,添加一个元素所花费的时间看上去并不很长,而且元素是自动排序的。到底应该怎样做将取决于所要收集的数据。如果不需要对数据进行排序,就没有必要付出排序的开销。更重要的是,对于某些数据来说,对其排序要比散列函数更加困难。散列函数只是将对象适当地打乱存放,而比较却要精确地判别每个对象。

要想具体地了解它们之间的差异,还需要研究一个收集矩形集的任务。如果使用TreeSet就需要提供Comparator<Rectangle>。如何比较两个矩形呢?比较面积吗?这行不通。可能会有两个不同的矩形,它们的坐标不同,但面积却相同。树的排序必须是整体排序。也就是说,任意两个元素必须是可比的,并且只有在两个元素相等时结果才为0。确实,有一种矩形的排序(按照坐标的词典顺序排列)方式,但它的计算很牵强且很繁琐。相反地,Rectangle类已经定义了散列函数,它直接对坐标进行散列。

注释:从Java SE 6起,TreeSet类实现了NavigableSet接口。这个接口增加了几个便于定位元素以及反向遍历的方法。详细信息请参看API注释。

在例13-3的程序中创建了两个Item象的树集。第一个按照部件编号排序,这是Item对象的默认顺序。第二个通过使用一个定制的比较器来按照描述信息排序。

例13-3 TreeSetTest.java
/**
@version 1.10 2004-08-02
@author Cay Horstmann
*/

import java.util.*;

/**
* This program sorts a set of item by comparing their descriptions.
*/
public
class
TreeSetTest {
public
static void
main(String[] args) {
SortedSet<Item> parts = new TreeSet<Item>();
parts.add(new Item("Toaster", 1234));
parts.add(new Item("Widget", 4562));
parts.add(new Item("Modem", 9912));
System.out.println(parts);

SortedSet<Item> sortByDescription = new TreeSet<Item>(
new Comparator<Item>() {
public
int
compare(Item a, Item b) {
String descrA = a.getDescription();
String descrB = b.getDescription();
return descrA.compareTo(descrB);
}
});

sortByDescription.addAll(parts);
System.out.println(sortByDescription);
}
}

/**
* An item with a description and a part number.
*/
class Item
implements Comparable<Item> {
/**
* Constructs an item.
*
* @param aDescription
* the item's description
* @param aPartNumber
* the item's part number
*/
public Item(String aDescription,
int aPartNumber) {
description = aDescription;
partNumber = aPartNumber;
}

/**
* Gets the description of this item.
*
* @return the description
*/
public String getDescription() {
return
description;
}

public String toString() {
return
"[descripion=" +
description + ", partNumber=" +
partNumber + "]";
}

public
boolean
equals(Object otherObject) {
if (this == otherObject)
return
true
;
if (otherObject ==
null)
return
false
;
if (getClass() != otherObject.getClass())
return
false
;
Item other = (Item) otherObject;
return
description.equals(other.description) &&
partNumber == other.partNumber;
}

public
int
hashCode() {
return 13 *
description.hashCode() + 17 *
partNumber;
}

public
int
compareTo(Item other) {
return
partNumber - other.partNumber;
}

private String
description;
private
int
partNumber;
}
API:java.lang.Comparable<T> 1.2

int compareTo(T other):将这个对象(this)与另一个对象(other)进行比较,如果this位于other之前则返回负值;如果两个对象在排列中处于相同的位置则返回0;如果this位于other之后则返回正值。

API:java.lang.Comparator<T> 1.2

int compare(T a, T b):将两个对象进行比较,如果a位于b之前则返回负值;如果两个对象在排列中处于相同的位置则返回0;如果a位于b之后则返回正值。

API:java.util.SortedSet<E> 1.2

Comparator<? super E> comparator():返回用于对元素进行排序的比较器。如果元素用Comparable接口的compareTo方法进行比较则返回null。

E first()

E last():返回有序集中的最小元素或最大元素。

API:java.util.NavigableSet<E> 6

E higher(E value)

E lower(E value):返回大于value的最小元素或小于value的最大元素,如果没有这样的元素则返回null。

E celling(E value)

E floor(E value):返回大于等于value的最小元素或小于等于value的最大元素,如果没有这样的元素则返回null。

E pollFirst()

E pollLast():刪除并返回这个集中的最大元素或最小元素,这个集为空时返回null。

Iterator<E> descendingIterator():返回一个按照递减顺序遍历集中元素的迭代器。

API:java.util.TreeSet<E> 1.2

TreeSet():构造一个用于排列Comparable对象的树集。

TreeSet(Comparator<? super E> ():构造一个树集,并使用指定的比较器对其中的元素进行排序。

TreeSet(SortedSet<? extends E> elements):构造一个树集,将有序集中的所有元素添加到这个树集中,并使用与给定集相同的元素比较器。

13.2.6
队列与双端队列

前面已经讨论过,队列可以让人们有效地在尾部添加一个元素,在头部删除一个元素。有两个端头的队列,即双端队列,可以让人们有效地在头部和尾部同时添加或删除元素。不支持在队列中间添加元素。在Java SE 6中引入了Deque接口,并由ArrayDeque和LinkedList类实现。这两个类都提供了双端队列,而且在必要时可以增加队列的长度。在第14章将会看到有限队列和有限双端队列。

API:java.util.Queue<e> 5.0

boolean add(E element)

boolean offer(E element):如果队列没有满,将给定的元素添加到这个双端队列的尾部并返回true。如果队列满了,第一个方法将抛出一个IllegalStateException,而第二个方法返回false。

E remove()

E poll():假如队列不空,删除并返回这个队列头部的元素。如果队列是空的,第一个方法抛岀NoSuchElementException,而第二个方法返回null。

E element()

E peek():如果队列不空。返回这个队列头部的元素,但不删除。如果队列空,第一个方法将抛出一个NoSuchElementException,而第二个方法返回null。

API:java.util.Deque<E> 6

void addFirst(E element)

void addLast(E element)

boolean offerFirst(E element)

boolean offerLast(E element):将给定的对象添加到双端队列的头部或尾部。如果队列满了,前面两个方法将拋出一个IllegalStateException,而后面两个方法返回false。

E removeFirst()

E removeLast()

E pollFirst()

E pollLast():假如队列不空,删除并返回这个队列头部的元素。如果队列是空的,前两个方法抛岀NoSuchElementException,而后两个方法返回null。

E getFirst()

E getLast()

E peekFirst()

E peekLast():如果队列不空。返回这个队列头部的元素,但不删除。如果队列空,前两个方法将抛出一个NoSuchElementException,而后两个方法返回null。

API:java.util.ArrayDeque<E> 6

ArrayDeque()

ArrayDeque(int initialCapacity):用初始容量16或给定的初始容量构造一个无限双端队列。

13.2.7
优先级队列

优先级队列(priority queue)中的元素可以按照任意的顺序插入,却总是按照排序的顺序进行检索。也就是说,无论何时调用remove方法,总会获得当前优先级队列中最小的元素。然而,优先级队列并没有对所有的元素进行排序。如果用迭代的方式处理这些元素,并不需要对它们进行棑序。优先级队列使用了一个优雅且高效的数据结构,称为堆(heap)。堆是一个可以自我调整的二叉树,对树执行添加和删除操作,可以让最小的元素移动到根,而不必花费时间对元素进行排序。

与TreeSet一样,一个优先级队列既可以保存实现了Comparable接口的类对象,也可以保存在构造器中提供比较器的对象。

使用优先级队列的典型示例是任务调度。每一个任务有一个优先级,任务以随机顺序添加到队列中。每当启动一个新的任务时,都将优先级最高的任务从队列中删除(由于习惯上将1设为“最高”优先级,所以会将最小的元素删除)。

例13-4显示了一个正在运行的优先级队列,与TreeSet中的迭代不同,这里的迭代并不是按照元素的排列顺序访问的。而删除却总是刪掉剩余元素中优先级数最小的那个元素。

例13-4 PriorityQueueTest.java
import java.util.*;

/**
* This program demonstrates the use of a priority queue.
*
* @version 1.00 2004-08-03
* @author Cay Horstmann
*/
public
class
PriorityQueueTest {
public
static void
main(String[] args) {
PriorityQueue<GregorianCalendar> pq = new PriorityQueue<GregorianCalendar>();
pq.add(new GregorianCalendar(1906, Calendar.DECEMBER, 9));
// G. Hopper
pq.add(new GregorianCalendar(1815, Calendar.DECEMBER, 10));
// A. Lovelace
pq.add(new GregorianCalendar(1903, Calendar.DECEMBER, 3));
// J. von Neumann
pq.add(new GregorianCalendar(1910, Calendar.JUNE, 22));
// K. Zuse

System.out.println("Iterating over elements...");
for (GregorianCalendar date : pq)
System.out.println(date.get(Calendar.YEAR));
System.out.println("Removing elements...");
while (!pq.isEmpty())
System.out.println(pq.remove().get(Calendar.YEAR));
}
}
API:java.util.PriorityQueue 5.0

PriorityQueue()

PriorityQueue(int initialCapacity):构造一个用于存放Comparable对象的优先级队列。

PriorityQueue(int initialCapacity, Comparator<? super E> c):构造一个优先级队列,并用指定的比较器对元素进行排序。

13.2.8
映射表

集是一个集合,它可以快速地査找现有的元素。但是,要査看一个元素,需要有要査找元素的精确副本。这不是一种非常通用的査找方式。通常,我们知道某些键的信息并想要査找与之对应的元素。映射表(map)数据结构就是为此设计的。映射表用来存放键/值对。如果提供了键,就能够査找到值。例如,有一张关于员工信息的记录表,键为员工ID,值为Employee对象。

Java类库为映射表提供了两个通用的实现:HashMap和TreeMap。这两个类都实现了Map接口。

散列映射表对键进行散列,树映射表用键的整体顺序对元素进行排序,并将其组织成搜索树。散列或比较函数只能作用于键。与键关联的值不能进行散列或比较。

应该选择散列映射表还是树映射表呢?与集一样,散列稍微快一些,如果不需要按昭排列顺序访问键,就最好选择散列。

下列代码将为存储的员工信息建立一个散列映射表:

Map<String, Employee> staff = new HashMap<String, Employee>();
staff.put("144-25-5464",
new Employee("Amy Lee"));
staff.put("567-24-2546",
new Employee("Harry Hacker"));
staff.put("157-62-7935",
new Employee("Gary Cooper"));
staff.put("456-62-5527",
new Employee("Francesca Cruz"));
每当往映射表中添加对象时,必须同时提供一个键。在这里,键是一个字符串,对应的值是Employee对象。

要想检索一个对象,必须提供(因而,必须记住)一个键。

String s = "144-25-5464";
staff.get(s);

如果在Java中没有与给定键对应的信息,get将返回null。

键必须是唯一的。不能对同一个键存放两个值。如果对同一个键两次调用put方法,第二个值就会取代第一个值。实际上,put将返回用这个键参数存储的上一个值。

remove方法用于从映射表中删除给定键对应的元素。size方法用于返回映射表中的元素数。

集合框架并没有将映射表本身视为一个集合(其他的数据结构框架则将映射表视为对(pair)的集合,或者视为用键作为索引的值的集合。然而,可以获得映射表的视图,这是一组实现了Collection接口对象,或者它的子接口的视图。

有3个视图,它们分别是:键集、值集合(不是集)和键/值对集。键与键/值对形成了一个集,这是因为在映射表中一个键只能有一个副本。下列方法将返回这3个视图(条目集的元素是静态内部类Map.Entry的对象)。

Set<K> keySet()

Collection<K> values()

Set<Nap.Entry<K, V>> entrySet()

注意,keySet既不是HashSet,也不是TreeSet,而是实现了Set接口的某个其他类的对象。Set接口扩展了Collection接口,因此,可以与使用任何集合一样使用keySet。

例如,可以枚举映射表中的所有键:

Set<String> keys = map.keySet();
for (String key : keys)
doSomethingWith(key);

提示:如果想要同时查看键与值,就可以通过枚举各个条目(entries)查看,以避免对值进行查找,可以使用下面这段代码框架:

for (Map.Entry<String, Employee> entry : staff.entrySet()) {
String key = entry.getKey();
Employee value = entry.getValue();
doSomethingWith(key, value);
}

如果调用迭代器的remove方法,实际上就从映射表中删除了键以及对应的值。但是,不能将元素添加到键集的视图中。如果只添加键而不添加值是毫无意义的。如果试图调用add方法,将会抛出一个UnsupportedOperationException异常,条目集视图也有同样的限制,不过,从概念上讲,添加新的键/值对是有意义的。

例13-5显示了映射表的操作过程。首先将键/值对添加到映射表中。然后,从映射表中删除一个键,同时与之对应的值也被删除掉了,接下来,修改与某一个键对应的值,并调用get方法査看这个值。最后,对条目集进行迭代。

例13-5 MapTest.java
import java.util.*;

/**
* This program demonstrates the use of a map with key type String and value
* type Employee.
*
* @version 1.10 2004-08-02
* @author Cay Horstmann
*/
public
class
MapTest {
public
static void
main(String[] args) {
Map<String, Employee> staff = new HashMap<String, Employee>();
staff.put("144-25-5464",
new Employee("Amy Lee"));
staff.put("567-24-2546",
new Employee("Harry Hacker"));
staff.put("157-62-7935",
new Employee("Gary Cooper"));
staff.put("456-62-5527",
new Employee("Francesca Cruz"));

// print all entries

System.out.println(staff);

// remove an entry

staff.remove("567-24-2546");

// replace an entry

staff.put("456-62-5527",
new Employee("Francesca Miller"));

// look up a value

System.out.println(staff.get("157-62-7935"));

// iterate through all entries

for (Map.Entry<String, Employee> entry : staff.entrySet()) {
String key = entry.getKey();
Employee value = entry.getValue();
System.out.println("key=" + key +
", value=" + value);
}
}
}

/**
* A minimalist employee class for testing purposes.
*/
class Employee {
/**
* Constructs an employee with $0 salary.
*
* @param n
* the employee name
*/
public Employee(String n) {
name = n;
salary = 0;
}

public String toString() {
return
"[name=" +
name + ", salary=" +
salary + "]";
}

private String
name;
private
double
salary;
}
API:java.util.Map<K, V> 1.2

V get(K key):获取与键对应的值;返回与键对应的对象,如果在映射表中没有这个对象则返回null。键可以为null。

V put(K key, V value):将键与对应的值关系插入到映射表中。如果这个键已经存在,新的对象将取代与这个键对应的旧对象。这个方法将返回键对应的旧值。如果这个键以前没有出现过则返回null。键可以为null,但值不能为null。

void putAll(Map<? extends K, ? extends V> entries):将给定映射表中的所有条目添加到这个映射表中。

boolean containsKey(Object Key):如果在映射表中已经有这个键,返回true。

boolean containsValue(0bject value):如果映射表中已经有这个值,返回true。

Set<Map.Entry<K, V>> entrySet():返回Map.Entry对象的集视图,即映射表中的键/值对,可以从这个集中删除元素,同时也从映射表中删除了它们。但是,不能添加任何元素。

Set<K> keySet():返回映射表中所有键的集视图。可以从这个集中删除元素,同时也从映射表中刪除了它们。但是,不能添加任何元素。

Collection<V> values():返回映射表中所有值的集合视图。可以从这个集中删除元素,同时也从映射表中删除了它们。但是,不能添加任何元素。

API:java.util.Map.Entry<K, V> 1.2

K getKey()

V getValue():返回这个条目的键或值。

V setValue(V newValue):设置在映射表中与新值对应的值,并返回旧值。

API:java.util.HashMap<K, V> 1.2

HashMap()

HashMap(int initialCapacity)

HashMap(int initialCapacity, float loadFactor):用给定的容量和装填因子构造一个空散列映射表(装填因子是一个0.0-1.0之间的数值。这个数值决定散列表填充的百分比。一旦到了这个比例,就要将其再散列到更大的表中)。默认的装填因子是0.75。

API:java.util.TreeMap<K,V> 1.2

TreeMap(Comparator<? super K> c):构造一个树映射表,并使用一个指定的比较器对键进行排序。

TreeMap(Map<? extends K, ? extends V> entries):构造一个树映射表,并将某个映射表中的所有条目添加到树映射表中。

TreeMap(SortedMap<? extends K, ? extends V> entries):构造一个树映射表,将某个有序映射表中的所有条目添加到树映射表中,并使用与给定的有序映射表相同的比较器。

API:java.util.SortedMap<K, V> 1.2

Comparator<? super K> comparator():返回对键进行排序的比较器。如果键是用Comparable接口的compareTo方法进行比较的,返回null。

K firstKey()

K lastKey():返回映射表中最小元素和最大元素。

13.2.9
专用集与映射表类

在集合类库中,有几个专用的映射表类,本节对它们做一下简要地介绍。

1. 弱散列映射表
设计WeakHashMap类是为了解决一个有趣的问题。如果有一个值,对应的键已经不再使用了,将会出现什么情况呢?假定对某个键的最后一次引用已经消亡,不再有任何途径引用这个值的对象了。但是,由于在程序中的任何部分没有再出现这个键,所以,这个键/值对无法从映射表中删除。为什么垃圾回收器不能够删除它呢?难道删除无用的对象不是垃圾回收器的工作吗?

遗憾的是,事情没有这样简单。垃圾回收器跟踪活动的对象,只要映射表对象是活动的,其中的所有桶也是活动的,它们不能被回收。因此,需要由程序负责从长期存活的映射表中刪除那些无用的值,或者使用WeakHashMap完成这件事情。当对键的唯一引用来自散列表条目时,这一数据结构将与垃圾回收器协同工作一起删除键/值对。

下面是这种机制的内部运行情况。WeakHashMap使用弱引用保存键。WeakReference对象将引用保存到另外一个对象中,在这里,就是散列表键,对于这种类型的对象,垃圾回收器用一种特有的方式进行处理。通常,如果垃圾回收器发现某个特定的对象已经没有他人引用,就将其回收。然而,如果某个对象只能由WeakReference引用,垃圾回收器仍然回收它,但要将引用这个对象的弱引用放入队列中。WeakHashMap将周期性地检査队列,以便找出新添加的弱引用。一个弱引用进入队列意味着这个键不再被他人使用,并且已经被收集起来。于是,WeakHashMap将刪除对应的条目。

2. 链接散列集和链接映射表
Java SE 1.4增加了两个类:LinkedHashSet和LinkedHashMap,用来记住插入元素项的顺序。这样就可以避免在散列表中的项从表面上看是随机排列的,当条目插入到表中时,就会并入到双向链表中(见图13-9)。

例如,在例13-5中包含下列映射表插入的处理:

Map staff = new LinkedHashMap();
staff.put("144-25-5464",
new Employee("Amy Lee"));
staff.put("567-24-2546",
new Employee("Harry Hacker"));
staff.put("157-62-7935",
new Employee("Gary Cooper"));
staff.put("456-62-5527",
new
Employee("Francesca Cruz"));

然后,staff.keySet.iterator()以下面次序枚举键:

144-25-5464

567-24-2546

157-62-7935

456-62-5527

并且staff.values().iterator()以下列顺序枚举这些值:

Amy Lee

Harry Hacker

Gary Cooper

Francesca Cruz

链接散列映射表将用访问顺序,而不是插入顺序,对映射表条目进行迭代。每次调用get或put,受到影响的条目将从当前的位置删除,并放到条目链表的尾部(只有条目在链表中的位置会受影响,而散列表中的桶不会受影响。一个条目总位于与键散列码对应的桶中)。要想构造一个这样的散列映射表,请调用

LinkedHashffap<K, V>(initialCapacity, loadFactor, true)

访问顺序对于实现高速缓存的“最近最少使用”原则十分重要,例如,可能希望将访问频率高的元素放在内存中,而访问频率低的元素则从数据库中读取。当在表中找不到元素项且表又已经满时,可以将迭代器加入到表中,并将枚举的前几个元素删除掉。这些是近期最少使用的几个元素。

甚至可以让这一过程自动化。即构造一个LinkedHashMap的子类,然后覆盖下面这个方法:

protected
boolean
removeEldestEntry(Map.Entry<K, V> eldest)

每当方法返回true时,就添加一个新条目,从而导致删除eldest条目。例如,下面的高速缓存可以存放100个元素:

Map<K, V> cache = new LinkedHashMap<K, v>(128, 0.75F,
true) {
protected
boolean
removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > 100;
}
};

另外,还可以对eldest条目进行评估,以此决定是否应该将它删除。例如,可以检査与这个条目一起存在的时间戳。

3. 枚举集与枚举映射表
EnumSet是一个枚举类型元素集的高效实现。由于枚举类型只有有限个实例,所以EnumSet内部用位序列实现。如果对应的值在集中,则相应的位被置为1。

EnumSet类没有公共的构造器。可以使用静态工厂方法构造这个集:

enum Weekday {
MONDAY, TUESDAY,
WEDNESDAY, THURSDAV,
FRIDAY, SATURDAY,
SUNDAY };

EnumSet<Weekday> always = EnumSet.allOf(Weekday.class);
EnumSet<Weekday> never = EnumSet.noneOf(Weekday.class);
EnumSet<Weekday> workday = EnumSet.range(Weekday.MONDAY, Weekday.FRIDAY);
EnumSet<Weekday> mwf = EnumSet.of(Weekday.MONDAY, Weekday.WEDNESDAY, Weekday.FRIDAY);
可以使用Set接口的常用方法来修改EnumSet。

EnumMap是一个键类型为枚举类型的映射表。它可以直接且高效地用一个值数组实现。在使用时,需要在构造器中指定键类型:

EnumMap<Weekday, Employee> personInCharge = new EnumMap<Weekday, Employee>(Weekday.class);
注释:在EnumSet的API文挡中,将会看到E extends Enum<E>这样奇怪的类型参数。简单地说,它的意思是“E是一个枚举类型”。所有的枚举类型都扩展于泛型Enum类。例如,Weekday扩展于Enum<Weekday>。

4. 标识散列映射表
Java SE 1.4还为另外一个特殊目的增加了另一个类IdentityHashMap。在这个类中,键的散列值不是用hashCode函数计算的,而是用System.identityHashCode方法计算的。这是Object.hashCode方法根据对象的内存地址来计算散列码时所使用的方式。而且,在对两个对象进行比较时,IdentityHashMap类使用==而不使用equals。

也就是说,不同的键对象,即使内容相同,也被视为是不同的对象。在实现对象遍历算法(如对象序列化)时,这个类非常有用,可以用来跟踪每个对象的遍历状况。

API:java.util.WeakHashMap<K, V> 1.2

WeakHashMap()

WeakHashMap(int initialCapacity)

WeakHashMap(int initialCapacity, float loadFactor):用给定的容量和填充因子构造一个空的散列映射表。

API:java.util.LinkedHashSet<E> 1.4

LinkedHashSet()

LinkedHashSet(int initialCapacity)

LinkedHashSet(int initialCapacity, float loadFactor):用给定的容量和填充因子构造一个空链接散列集。

API:java.util.LinkedHashMap<K, V> 1.4

LinkedHashMap()

LinkedHashMap(int initialCapacity)

LinkedHashNap(int initialCapacity, float loadFactor)

LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder):用给定的容量、填充因子和顺序构造一个空的链接散列映射表。

protected boolean removeEldestEntry(Map.Entry<K, V> eldest):如果想删除eldest元素,并同时返回true,就应该覆盖这个方法。eldest参数是预期要删除的条目。这个方法将在条目添加到映射表中之后调用,其默认的实现将返回false。即在默认情况下,旧元素没有被删除。然而,可以重新定义这个方法,以便有选择地返回true。例如,如果最旧的条目符合一个条件,或者映射表超过了一定大小,则返回true。

API:java.util.EnumSet<E extends Enum<E>> 5.0

static <E extends Enum<E>> EnumSet<E> allOf(Class<E> enumType):返回一个包含给定枚举类型的所有值的集。

static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> enumType):返回一个空集,并有足够的空间保存给定的枚举类型所有的值。

static <E extends Enum<E>> EnumSet<E> range(E from, E to):返回一个包含from-to之间的所有值(包括两个边界元素)的集。

static <E extends Enum<E>> EnumSet<E> of(E value)

static <E extends Enum<E>> EnumSet<E> of(E value, E... values):返回包括给定值的集。

API:java.util.EnumMap<K extends Enum<K>, V> 5.0

EnumMap(Class<K> keyType):构造一个键为给定类型的空映射集。

API:java.util.IdentityHashMap<K, V> 1.4

IdentityHashMap()

IdentityHashMap(int expectedMaxSize):构造一个空的标识散列映射集,其容量是大于1.5* expectedMaxSize的2的最小次幕(expectedMaxSize的默认值是21)。

API:java.lang.System 1.0

static int identityHashCode(Object obj) 1.1:返回Object.hashCode计算出来的相同散列码(根据对象的内存地址产生),即使obj所属的类已经重新定义了hashCode方法也是如此。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: