除了Map结尾的类之外,其他都实现了Collection接口,而以Map结尾的类实现了Map接口。
链表
在Java程序设计语言中,所有链表实际上都是双向链表的(double linked)——即每个节点还存放着指向前去节点的引用。
从链表中间删除一个元素是一个很轻松的操作, 即需要更新被删除元素附近的链接。
在链表中添加或删除元素时,绕来绕去的指针可能已经给人们留下了极坏的印象。如果真是如此的话,就会为Java集合类库提供一个LinkedList而感到拍手称快。
在下面的代码示例中,先添加3个元素,然后再将第2个元素删除;
List staff = new LinkedList<>();// LinkedList implements List
staff.add("Amy");
staff.add("Bob");
staff.add("Carl");
Interator iter = staff.iterator();
String first = iter.next();//visit first element
String second = iter.next();//visit second element
iter.remove();//remove last visited element
链表是一个有序集合(ordered collection),每个对象的位置十分重要。LinkedList.add方法将对象添加到链表的尾部。由于迭代器时描述集合中位置的,所以这种依赖于位置的add方法将由迭代器负责。只有对自然有序的集合使用迭代器添加元素才有实际意义。Add方法在迭代器之前添加一个新对象。
当用一个刚刚由Iterator方法返回,并且指向链表表头的迭代器调用add操作时,新添加的元素将变成列表的新表头。当迭代器越过链表的最后一个元素时(即hasNext返回false),添加的元素将变成列表的新表尾。如果链表由n个元素,由n+1个位置可以添加新元素。这些位置与迭代器的n+1个可能的位置相对应。例如,如果链表包含3个元素,A、B、C,就有四个位置(标有|)可以插入新元素:
|ABC
A|BC
AB|C
ABC|
注释:在用“光标”类比时要格外小心。remove操作与BACKSPACE键的工作方式不太一样。在调用next之后,remove方法确实与BACKSPACE键一样删除了迭代器左侧的元素。但是如果调用previous就会将右侧的元素删除掉,并且不能连续调用两次remove。
add方法只依赖于迭代器的位置,而remove方法依赖于迭代器的状态。
例如,一个迭代器指向另一个迭代器刚刚删除的元素前面,现在这个迭代器就是无效的,并且不应该在使用。链表迭代器的设计使它能够检测到这种修改。如果迭代器发现它的集合被另一个迭代器修改了,或是被该集合自身的方法修改了,就会抛出一个ConcurrentModificationException异常。
为了避免发生并发修改的异常,请遵循下述简单规则:可以根据需要给容器附加许多的迭代器,但是这些迭代器只能读取列表。另外,再单独附加一个既能读又能写的迭代器。
有一个简单的方法可以检测到并发修改的问题。集合可以跟踪改写操作(诸如添加或删除元素)的次数。每个迭代器都维护一个独立的计数值。在每个迭代器方法的开始处检查自己改写操作的计数值是否与集合的改写操作计数值一致。如果不一致,抛出一个ConcurrentModificationException异常。
注释:对于并发修改列表的检测有一个奇怪的例外。链表只负责跟踪对列表的结构性修改,例如,添加元素、删除元素。set方法不被视为结构性修改。可以将多个迭代器附加给一个链表,所有的迭代器都调用set方法对现有节点的内容进行修改。
使用链表的唯一理由是尽可能地减少在列表中间插入或删除元素所付出的代价。如果列表只有少数几个元素,就完全可以使用ArrayList。
我们建议避免使用以整数索引表示链表中位置的所有方法。如果需要对集合进行随机访问,就使用数组或ArrayLister,而不要使用链表。
数组列表
散列集
有一种众所周知的数据结构,可以快速地查找所需要的对象,这就是散列表(hash table)。散列表为每个对象计算一个整数,称为散列码(hash code)。散列码是由对象地实例域产生的一个整数。更准确地说,具有不同数据域的对象将产生不同的散列码。
在Java中,散列表用链表数组实现。每个列表被称为桶(bucket)。
要想查找表中对象的位置,就要先计算它的散列码,然后与桶的总数取余,所得到的结果就是保存这个元素的桶的索引。当然,有时候会遇到桶被占满的情况,这也是不可避免地。这种现象被称为散列冲突(hash collision)。这时,需要用新对象与桶中的所有对象进行比较,查看这个对象是否已经存在。如果散列码是合理且随机分布的,桶的数目也足够大,需要比较的次数就会很少。
注释:在Java SE 8中,桶满时会从链表变为平衡二叉树。如果选择的散列函数不当,会产生很多冲突,或者如果有恶意代码视图在散列表中填充多个有相同散列码的值,这样就能提高性能。
如果想更多地控制散列表地运行性能,就要指定一个初始的桶数。桶数是指用于收集具有相同散列值的桶的数目。如果要插入到散列表中的元素太多,就会增加冲突的可能性,降低运行性能。入宫大致知道最终会有多少个元素要插入到散列表中,就可以设置桶数。通常,将桶数设置为预计元素个数的75%~150%。有些研究人员认为:尽管还没有确凿的证据,但最好将桶数设置为一个素数,以防键的集聚。
如果散列表太满,就需要再散列(rehashed)。如果对散列表再散列,就需要创建一个桶数更多的表,并将所有元素插入到这个新表中,然后丢弃原来的表。装填因子(load factor)决定何时对散列表进行再散列。
Java集合类库提供了一个HashSet类,它实现了基于散列表的集。可以用add方法添加元素。contains方法已经被重新定义,用来快速地查看是否某个元素已经出现在集中。散列表迭代器将依次访问所有的桶。由于散列将元素分散在表的各个位置上,所以访问他们的顺序几乎是随机的。
树集
TreeSet类与散列集十分类似,不过,它比散列集有所改进。树集是一个有序集合(sorted collection)。可以以任意顺序将元素插入到集合中。在对集合进行遍历时,每个值将自动地按照排序后的顺序呈现。
树地排序必须是全序。也就是说,任意两个元素必须是可比的,并且只有在两个元素相等时结果才为0。
队列与双端队列
队列可以让人们有效地在尾部添加一个元素,在头部删除一个元素。有两个端头的队列,即双端队列,可以让人们有效地在头部和尾部同时添加或删除元素。不支持在队列中间添加元素。在Java SE 6中引入了Deque接口,并由ArrayDeque和LinkedList类实现。这两个类都提供了双端队列,而且在必要时可以增加队列的长度。
优先级队列
优先级队列(priority queue)中的元素可以按照任意的顺序插入,却总是按照排序的顺序进行检索。也就是说,无论何时调用remove方法,总会获得当前优先级队列中最小的元素。优先级队列使用了一个优雅且高效的数据结构,称为堆(heap)。堆是一个可以自我调整的二叉树,对树执行添加(add)和删除(remove)操作,可以让最小的元素移动到根,而不必花费时间对元素进行排序。
使用优先级队列的典型示例是任务调度。每个任务有一个优先级,任务以随机顺序添加到队列中。每当启动一个新的任务时,都将优先级最高的任务从队列中删除(由于习惯上将1设为“最高”优先级,所以会将最小的元素删除)。