目录
Java 集合框架
接口、实现分离
Java 集合框架与现代的数据结构类库的常见情况一样,类库也将接口 (interface) 与实现 (implementation) 分离。每一个实现都可以通过一个实现了该接口的类表示。在程序运行过程中,我们并不知道如何实现接口才能使程序达到最高的效率,因此将接口与实现分离,能够更有效地编写程序。
另外,利用这种方式,可以轻松地使用另外一种不同的实现。例如,CircularArrayQueue
和 LinkedListQueue
都实现了 Queue<E>
接口,通过如下方式,我们可以很方便地修改接口的实现方法:
// CircularArrayQueue
Queue<Customer> expressLane = new CircularArrayQueue<>(100);
expressLane.add(new Customer("Harry"));
// LinkedListQueue
Queue<Customer> expressLane = new LinkedListQueue<>(100);
expressLane.add(new Customer("Harry"));
在研究 API 文档时,会发现另外一组名字以 Abstract
开头的类,例如,AbstractQueue
。这些类是为类库实现者而设计的。如果想要实现自己的队列类,会发现扩展 AbstractQueue
类要比实现 Queue
接口中的所有方法轻松得多。
Collection 接口
在 Java 类库中,集合类的基本接口是 Collection
接口。这个接口有两个基本方法:
public interface Collection<E>
{
boolean add(E element); // 添加一个元素
Iterator<E> iterator(); // 返回该集合的一个迭代器对象
...
}
迭代器
Iterator
接口如下:
public interface Iterator<E>
{
E next();
boolean hasNext();
void remove();
default void forEachRemaining(Consumer<? super E> action);
}
通过反复调用 next
方法,可以逐个访问集合中的每个元素。但是,如果到达了集合的末尾,next
方法将抛出一个 NoSuchElementException
。因此需要在调用 next
之前调用 hasNext
方法。如果迭代器对象还有多个供访问的元素,这个方法就返回 true
。
public interface Iterable<E>
{
Iterator<E> iterator();
}
Iterable
接口只包含一个抽象方法,for-each
循环可以与任何实现了 Iterable
接口的对象一起工作,而 Collection
接口扩展了 Iterable
接口。因此对于标准类库中的任何集合都可以使用 for-each
循环。这比通过 Iterator
对象不断调用 next
方法要便捷。
iterator.forEachRemaining(element -> {do something with element});
在 Java SE 8 中,甚至不用写循环。可以调用 forEachRemaining
方法并提供一个 lambda
表达式(它会处理一个元素)。将对迭代器的每一个元素调用这个 lambda
表达式,直到再没有元素为止。
元素被访问的顺序取决于集合类型。如果对 ArrayList
进行迭代,迭代器将从索引 0
开始,每迭代一次,索引值加 1。然而如果访问 HashSet
中的元素,每个元素将会按照某种随机的次序出现。虽然可以确定在迭代过程中能够遍历到集合中的所有元素,但却无法预知元素被访问的次序。
Iterator
接口的 remove
方法将会删除上次调用 next
方法时返回的元素。在大多数情况下,在决定删除某个元素之前应该先看一下这个元素是很具有实际意义的。然而如果想要删除指定位置上的元素,仍然需要越过这个元素。
Iterator<String> it = c.iterator();
it.next(); // skip over the first element
it.remove(); // now remove it
更重要的是,对 next
方法和 remove
方法的调用具有互相依赖性。如果调用 remove
之前没有调用 next
将是不合法的。如果这样做,将会抛出一个 IllegalStateException
异常。
集合框架中的接口
Java 集合框架为不同类型的集合定义了大量接口, 如图 2 所示:
泛型实用方法
java.util.Collection<E>
.iterator()
:返回一个用于访问集合中每个元素的迭代器。.size()
:返回当前存储在集合中的元素个数。.isEmpty()
:如果集合中没有元素,返回true
。.contains(Object obj)
:如果集合中包含了一个与 obj 相等的对象,返回true
。.containsAll(Collection<?> other)
:如果这个集合包含 other 集合中的所有元素,返回true
。.add(Object element)
:将一个元素添加到集合中。如果由于这个调用改变了集合,返回true
。.addAll(Collection<? extends E> other)
:将 other 集合中的所有元素添加到这个集合。如果由于这个调用改变了集合,返回true
。.remove(Object obj)
:从这个集合中删除等于 obj 的对象。如果有匹配的对象被删除,返回true
。.removeAll(Collection<?> other)
:从这个集合中删除 other 集合中存在的所有元素。如果由于这个调用改变了集合,返回true
。.removeIf(Predicate<? super E> filter)
:从这个集合删除 filter 返回true
的所有元素。如果由于这个调用改变了集合,则返回true
。.clear()
:从这个集合中删除所有的元素。.retainAll(Collection<?> other)
:从这个集合中删除所有与 other 集合中的元素不同的元素。如果由于这个调用改变了集合,返回true
。.toArray()
:返回这个集合的对象数组。.toArray(T[] arrayToFill)
:返回这个集合的对象数组。如果arrayToFill
足够大,就将集合中的元素填入这个数组中。剩余空间填补null
;否则分配一个新数组,其成员类型与arrayToFill
的成员类型相同,其长度等于集合的大小,并填充集合元素。
java.util.Iterator<E>
.hasNext()
:如果存在可访问的元素,返回true
。.next()
:返回将要访问的下一个对象。如果已经到达了集合的尾部,将拋出一个NoSuchElementException
。.remove()
:删除上次访问的对象。这个方法必须紧跟在访问一个元素之后执行。如果上次访问之后,集合已经发生了变化,这个方法将抛出一个IllegalStateException
。
具体的集合
表 1 列出了 Java 类库中的集合,并简要描述了每个集合类的用途。在表 1 中,除了以 “Map” 结尾的类之外,其他类都实现了 Collection
接口,而以 “Map” 结尾的类实现了 Map
接口。
集合类型 | 描述 |
---|---|
ArrayList | 一种可以动态增长和缩减的索引序列 |
LinkedList | 一种可以在任何位置进行高效地插人和删除操作的有序序列 |
ArrayDeque | 一种用循环数组实现的双端队列 |
HashSet | 一种没有重复元素的无序集合 |
TreeSet | —种有序集 |
EnumSet | 一种包含枚举类型值的集 |
LinkedHashSet | 一种可以记住元素插入次序的集 |
PriorityQueue | 一种允许高效删除最小元素的集合 |
HashMap | 一种存储键 / 值关联的数据结构 |
TreeMap | —种键值有序排列的映射表 |
EnumMap | 一种键值属于枚举类型的映射表 |
LinkedHashMap | 一种可以记住键 / 值项添加次序的映射表 |
WeakHashMap | 一种其值无用武之地后可以被垃圾回收器回收的映射表 |
IdentityHashMap | 一种用 == 而不是用 equals 比较键值的映射表 |
链表
动态的 ArrayList
类和数组列表都有一个重大的缺陷,是从数组的中间位置删除一个元素要付出很大的代价,其原因是数组中处于被删除元素之后的所有元素都要向数组的前端移动,在数组中间的位置上插入一个元素也是如此。
而链表则很好地解决了这个问题,在 Java 程序设计语言中,所有链表实际上都是双向链接的 (doubly linked) ——即每个结点还存放着指向前驱结点的引用,如下图所示:
与普通的迭代器相同,当迭代器调用 next
方法后,再调用 remove
方法将删除 next
方法返回的对象。由于 LinkedList
类是双向链表,迭代器 ListIterator
也同样拓展了 previous
方法,并且可以反向访问(调用 previous
方法后再调用 remove
方法,也会删除 previous
方法返回的对象,即迭代器跨越的对象)。
set
方法用一个新元素取代调用 next
或 previous
方法返回的上一个元素(规则类似 remove
方法)。
ListIterator<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> iter1 = list.listIterator();
ListIterator<String> iter2 = list.listIterator();
iter1.next();
iter1.remove();
iter2.next(); // throws ConcurrentModificationException
为了避免发生并发修改的异常,请遵循下述简单规则:可以根据需要给容器附加许多的迭代器,但是这些迭代器只能读取列表。另外,再单独附加一个既能读又能写的迭代器。
链表虽然在插入和删除操作过程中的效率很高,但在查找某一特定位置的数据时,效率远不如动态数组,这是由于它们的数据结构特性所决定的。因此如果需要频繁使用插入和删除操作时,可以考虑使用链表;如果需要频繁查找,则使用动态数组更好。
相关方法:
java.util.List<E>
.ListIterator()
:返回一个列表迭代器,以便用来访问列表中的元素。.ListIterator(int index)
:返回一个列表迭代器,以便用来访问列表中的元素,这个元素是第一次调用next
返回的给定索引的元素。.add(int i, E element)
:在给定位置添加一个元素。.addAll(int i, Collection<? extends E> elements)
:将某个集合中的所有元素添加到给定位置。.remove(int i)
:删除给定位置的元素并返回这个元素。.get(int i)
:获取给定位置的元素。.set(int i, E element)
:用新元素取代给定位置的元素,并返回原来那个元素。.indexOf(Object element)
:返回与指定元素相等的元素在列表中第一次出现的位置,如果没有这样的元素将返回 -1。.lastIndexOf(Object element)
:返回与指定元素相等的元素在列表中最后一次出现的位置,如果没有这样的元素将返回 -1。
java.util.ListIterator<E>
.add(E newElement)
:在当前位置前添加一个元素。.set(E newElement)
:用新元素取代next
或previous
上次访问的元素。如果在next
或previous
上次调用之后列表结构被修改了,将拋出一个IllegalStateException
异常。.hasPrevious()
:当反向迭代列表时,还有可供访问的元素,返回 true。.previous()
:返回前一个对象。如果已经到达了列表的头部,就抛出一个NoSuchElementException
异常。.nextlndex()
:返回下一次调用next
方法时将返回的元素索引。.previousIndex()
:返回下一次调用previous
方法时将返回的元素索引。
java.util.LinkedList<E>
LinkedList()
:构造一个空链表。LinkedList(Collection<? extends E> elements)
:构造一个链表,并将集合中所有的元素添加到这个链表中。.addFirst(E element)
/.addLast(E element)
:将某个元素添加到列表的头部或尾部。.getFirst()
/.getLast()
:返回列表头部或尾部的元素。.removeFirst()
/.removeLast()
:删除并返回列表头部或尾部的元素。
散列集
散列表可以快速地査找所需要的对象,它为每个对象计算一个整数,称为散列码 (hashcode)。散列码是由对象的实例域产生的一个整数,更准确地说, 具有不同数据域的对象将产生不同的散列码。如果自定义类,就要负责实现这个类的 hashCode
方法。
在 Java 中,散列表用链表数组实现,每个列表被称为桶 (bucket),如图 5 所示。要想査找表中对象的位置,就要先计算它的散列码,然后与桶的总数取余,所得到的结果就是保存这个元素的桶的索引。
例如,如果某个对象的散列码为 76268,并且有 128 个桶,对象应该保存在第 108 号桶中(76268 除以 128 余 108)。如果在这个桶中没有其他元素,此时将元素直接插人到桶中就可以了。有时候会遇到桶被占满的情况,这也是不可避免的。这种现象被称为散列冲突 (hash collision)。这时,需要用新对象与桶中的所有对象进行比较,査看这个对象是否已经存在。如果散列码是合理且随机分布的,桶的数目也足够大,需要比较的次数就会很少。
如果大致知道最终会有多少个元素要插人到散列表中,就可以设置桶数。通常,将桶数设置为预计元素个数的 75% ~ 150%。如果散列表太满,就需要再散列 (rehashed)。如果要对散列表再散列,就需要创建一个桶数更多的表,并将所有元素插入到这个新表中,然后丢弃原来的表。
Java 集合类库提供了一个 HashSet
类,它实现了基于散列表的集。可以用 add 方法添加元素。contains
方法已经被重新定义,用来快速地查看是否某个元素已经出现在集中。它只在某个桶中査找元素,而不必查看集合中的所有元素。
散列集迭代器将依次访问所有的桶。由于散列将元素分散在表的各个位置上,所以访问它们的顺序几乎是随机的。只有不关心集合中元素的顺序时才应该使用 HashSet
。
相关方法:
java.util.HashSet<E>
HashSet()
:构造一个空散列表。HashSet(Collection<? extends E> elements)
:构造一个散列集,并将集合中的所有元素添加到这个散列集中。HashSet(int initialCapacity)
:构造一个空的具有指定容量(桶数)的散列集。HashSet(int initialCapacity, float loadFactor)
:构造一个具有指定容量和装填因子(一个 0.0 ~ 1.0 之间的数值,确定散列表填充的百分比,当大于这个百分比时,散列表进行再散列)的空散列集。
java.lang.Object
.hashCode()
:返回这个对象的散列码。散列码可以是任何整数,包括正数或负数。equals
和hashCode
的定义必须兼容,即如果x.equals(y)
为true
,x.hashCode()
必须等于y.hashCode()
。
树集
树集是一个有序集合 (sorted collection),可以以任意顺序将元素插入到集合中,在对集合进行遍历时,每个值将自动地按照排序后的顺序呈现。排序是用树结构完成的,当前实现使用的是红黑树 (red-black tree)。
SortedSet<String> sorter = new TreeSet<>(); // TreeSet implements SortedSet
sorter.add("Bob");
sorter.add("Amy");
sorter.add("Carl");
for (String s : sorter) System.println(s);
// 依次输出 Amy Bob Carl
将一个元素添加到树中要比添加到散列表中慢,见表 2,但是与检查数组或链表中的重复元素相比还是快很多。
文档 | 单词总数 | 不同的单词个数 | HashSet | TreeSet |
---|---|---|---|---|
Alice in Wonderland | 28195 | 5909 | 5 秒 | 7 秒 |
The Count of Monte Cristo | 466300 | 37545 | 75 秒 | 98 秒 |
比较 “树集” 与 “散列集”:树集与散列集在时间的花费上基本相当,树集略慢,但能够将元素排序;如果对存入元素没有排序需求的话,可以使用散列集,因为没必要去额外花费时间进行排序;如果对排序有需求,那么就可以考虑使用树集。
要使用树集,必须能够比较元素。这些元素必须实现 Comparable
接口或者构造集时必须提供一个 Comparator
。 有些时候,实现两个对象的比较大小远比直接计算一个哈希值要复杂得多,散列函数只是将对象适当地打乱存放,因此在实际使用过程中,应兼顾使用需求和对象特点两个方面。
相关方法:
java.util.TreeSet<E>
TreeSet()
/TreeSet(Comparator<? super E> comparator)
:构造一个空树集。TreeSet(Collection<? extends E> elements)
/TreeSet(SortedSet<E> s)
:构造一个树集,并增加一个集合或有序集中的所有元素(对于后一种情况,要使用同样的顺序)。
java.util.SortedSet<E>
.comparator()
:返回用于对元素进行排序的比较器。如果元素用Comparable
接口的compareTo
方法进行比较则返回null
。.first()
/.last()
:返回有序集中的最小元素或最大元素。
java.util.NavigableSet<E>
.higher(E value)
/.lower(E value)
:返回大于 value 的最小元素或小于 value 的最大元素,如果没有这样的元素则返回null
。.ceiling(E value)
/.floor(E value)
:返回大于等于 vaiue 的最小元素或小于等于 value 的最大元素,如果没有这样的元素则返回null
。.pollFirst()
/.pollLast()
:删除并返回这个集中的最大元素或最小元素,这个集为空时返回null
。.descendingIterator
:返回一个按照递减顺序遍历集中元素的迭代器。
队列、双端队列与优先级队列
队列可以让人们有效地在尾部添加一个元素,在头部删除一个元素。
有两个端头的队列,即双端队列,可以让人们有效地在头部和尾部同时添加或删除元素。不支持在队列中间添加元素。在 Java SE 6 中引人了 Deque
接口,并由 ArrayDeque
和LinkedList
类实现。这两个类都提供了双端队列,而且在必要时可以增加队列的长度。
优先级队列 (priority queue) 中的元素可以按照任意的顺序插入,却总是按照排序的顺序进行检索。无论何时调用 remove
方法,总会获得当前优先级队列中最小的元素。。优先级队列使用了一个优雅且高效的数据结构,称为堆 (heap)。堆是一个可以自我调整的二叉树,对树执行添加 (add) 和删除 (remove) 操作,可以让最小的元素移动到根,而不必花费时间对元素进行排序。
与 TreeSet
—样,一个优先级队列既可以保存在实现了 Comparable
接口的类对象,也可以保存在构造器中提供的 Comparator
对象。
使用优先级队列的典型示例是任务调度。每一个任务有一个优先级,任务以随机顺序添加到队列中。每当启动一个新的任务时,都将优先级最高的任务从队列中删除(由于习惯上将 1 设为 “最高” 优先级,所以会将最小的元素删除)。
相关方法:
java.util.Queue<E>
.add(E element)
/.offer(E element)
:如果队列没有满,将给定的元素添加到这个双端队列的尾部并返回true
。如果队列满了,第一个方法将拋出一个IllegalStateException
,而第二个方法返回false
。.remove()
/.poll()
:假如队列不空,删除并返回这个队列头部的元素。如果队列是空的,第一个方法抛出NoSuchElementException
,而第二个方法返回null
。.element()
/.peek()
:如果队列不空,返回这个队列头部的元素,但不删除。如果队列空,第一个方法将拋出一个NoSuchElementException
,而第二个方法返回null
。
java.util.Deque<E>
.addFirst(E element)
/.addLast(E element)
/.offerFirst(E element)
/.offerLast(E element)
:将给定的对象添加到双端队列的头部或尾部。如果队列满了,前面两个方法将拋出一个IllegalStateException
,而后面两个方法返回false
。.removeFirst()
/.removeLast()
/.pollFirst()
/.pollLast()
:如果队列不空,删除并返回队列头部的元素。如果队列为空,前面两个方法将拋出一个NoSuchElementException
,而后面两个方法返回null
。.getFirst()
/.getLast()
/.peekFirst()
/.peekLast()
:如果队列非空,返回队列头部的元素,但不删除。如果队列空,前面两个方法将拋出一个NoSuchElementException
,而后面两个方法返回null
。
java.util.ArrayDeque<E>
ArrayDeque()
/ArrayDeque(int initialCapacity)
:用初始容量 16 或给定的初始容量构造一个无限双端队列。
java.util.PriorityQueue
PriorityQueue()
/PriorityQueue(int initialCapacity)
:构造一个用于存放Comparable
对象的优先级队列。PriorityQueue(int initialCapacity, Comparator <? super E> c)
:构造一个优先级队列,并用指定的比较器对元素进行排序。
映射
通常我们知道某些键的信息,并想要查找与之对应的元素。映射 (map) 数据结构就是为此设计的。映射用来存放 键-值 对,如果提供了键,就能够查找到值。
基本映射操作
Java 类库为映射提供了两个通用的实现:HashMap
和 TreeMap
。这两个类都实现了 Map
接口。散列映射对键进行散列,树映射用键的整体顺序对元素进行排序,并将其组织成搜索树。散列或比较函数只能作用于键,与键关联的值不能进行散列或比较。
每当往映射中添加对象时,必须同时提供一个键。键必须是唯一的,不能对同一个键存放两个值。
编写程序时,选择 “散列映射” 还是 “树映射”,可以参考 “散列集” 与 “树集” 的选择原则,二者的性能与集合类似。
相关方法:
java.util.Map<K, V>
.get(Object key)
:获取与键对应的值;返回与键对应的对象,如果在映射中没有这个对象则返回null
。键可以为null
。.getOrDefault(Object key, V defaultValue)
:获得与键关联的值;返回与键关联的对象,或者如果未在映射中找到这个键,则返回defaultValue
。.put(K key, V value)
:将键与对应的值关系插入到映射中。如果这个键已经存在,新的对象将取代与这个键对应的旧对象。这个方法将返回键对应的旧值,如果这个键以前没有出现过则返回null
。键可以为null
,但值不能为null
。.putAll(Map<? extends K, ? extends V> entries)
:将给定映射中的所有条目添加到这个映射中。.containsKey(Object key)
:如果在映射中已经有这个键,返回true
。.containsValue(Object value)
:如果映射中已经有这个值,返回true
。.forEach(BiConsumer<? super K, ? super V> action)
:对这个映射中的所有键 / 值对应用这个动作。
java.util.HashMap<K, V>
HashMap()
/HashMap(int initialCapacity)
/HashMap(int initialCapacity, float loadFactor)
:用给定的容量和装填因子构造一个空散列映射(装填因子是一个 0.0 〜 1.0 之间的数值。这个数值决定散列表填充的百分比。一旦到了这个比例,就要将其再散列到更大的表中)。默认的装填因子是 0.75。
java.util.TreeMap<K, V>
TreeMap()
:为实现Comparable
接口的键构造一个空的树映射。TreeMap(Comparator<? super K> c)
:构造一个树映射,并使用一个指定的比较器对键进行排序。TreeMap(Map<? extends K, ? extends V > entries)
:构造一个树映射,并将某个映射中的所有条目添加到树映射中。TreeMap(SortedMap<? extends K, ? extends V> entries)
:构造一个树映射,将某个有序映射中的所有条目添加到树映射中,并使用与给定的有序映射相同的比较器。
java.util.SortedMap<K, V>
.comparator()
:返回对键进行排序的比较器。如果键是用Comparable
接口的compareTo
方法进行比较的,返回null
。.firstKey()
/.lastKey()
:返回映射中最小元素和最大元素。
更新映射项
处理映射时的一个难点就是更新映射项。正常情况下,可以得到与一个键关联的原值,完成更新,再放回更新后的值。不过,必须考虑一个特殊情况,即键第一次出现。以统计一个单词出现的次数为例,给出以下三种方法:
// getOrDefault
counts.put(word, counts.getOrDefault(word, 0)+ 1);
// putIfAbsent
counts.putIfAbsent(word, 0);
counts.put(word, counts.get(word)+ 1); // Now we know that get will succeed
// merge
counts.merge(word, 1, Integer::sum);
相关方法:
java.util.Map<K, V>
.merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunctlon)
:如果 key 与一个非null
值 v 关联,将函数应用到 v 和 value,将 key 与结果关联;或者如果结果为null
,则删除这个键。否则,将 key 与 value 关联,返回get(key)
。.compute(K key, BiFunction<? super K, ? super V,? extends V> remappingFunction)
:将函数应用到 key 和 get(key),将 key 与结果关联;或者如果结果为null
,则删除这个键,返回get(key)
。.computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction)
:如果 key 与一个非null
值 v 关联,将函数应用到 key 和 v,将 key 与结果关联;或者如果结果为null
,则删除这个键,返回get(key)
。.computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)
:将函数应用到 key,除非 key 与一个非null
值关联,将 key 与结果关联;或者如果结果为null
,则删除这个键,返回get(key)
。.replaceAll(BiFunction<? super K, ? super V, ? extends V> function)
:在所有映射项上应用函数。将键与非null
结果关联,对于null
结果,则将相应的键删除。
映射视图
映射有 3 种视图:键集、值集合(不是一个集)以及键 / 值对集。键和键 / 值对可以构成一个集,因为映射中一个键只能有一个副本。下列方法分别返回三个视图:
// 获得三种视图
Set<K> keySet() // 键集
Collection<V> values() // 值集合
Set<Map.Entry<K, V>> entrySet() // 键 / 值对集
// 访问视图
Set<String> keys = map.keySet();
for (String key : keys)
{
// do something with key
}
for (Map.Entry<String, Employee〉entry : staff.entrySet())
{
String k = entry.getKey();
Employee v = entry.getValue();
// do something with k, v
}
counts.forEach((k,v) -> {
// do somethingwith k, v
});
如果在键集视图上调用迭代器的 remove
方法,实际上会从映射中删除这个键和与它关联的值。不过不能向键集视图增加元素。另外如果增加一个键而没有同时增加值也是没有意义的。如果试图调用 add
方法,它会抛出一个 UnsupportedOperationException
。条目集视图有同样的限制,尽管理论上增加一个新的键 / 值对好像是有意义的。
相关方法:
java.util.Map<K, V>
.entrySet()
:返回Map.Entry
对象(映射中的键 / 值对)的一个集视图。可以从这个集中删除元素,它们将从映射中删除,但是不能增加任何元素。.keySet()
:返回映射中所有键的一个集视图。可以从这个集中删除元素,键和相关联的值将从映射中删除,但是不能增加任何元素。.values()
:返回映射中所有值的一个集合视图。可以从这个集合中删除元素,所删除的值及相应的键将从映射中删除,不过不能增加任何元素。
java.util.Map.Entry<K, V>
.getKey()
/.getValue()
:返回这一条目的键或值。.setValue(V newValue)
:将相关映射中的值改为新值,并返回原来的值。
弱散列映射
垃圾回收器跟踪活动的对象。只要映射对象是活动的,其中的所有桶也是活动的,它们不能被回收。因此需要由程序负责从长期存活的映射表中删除那些无用的值,或者使用 WeakHashMap
完成这件事情。当对键的唯一引用来自散列条目时,这一数据结构将与垃圾回收器协同工作一起删除键 / 值对。
下面是这种机制的内部运行情况。WeakHashMap
使用弱引用 (weak references) 保存键。WeakReference
对象将引用保存到另外一个对象中,在这里,就是散列键。对于这种类型的对象,垃圾回收器用一种特有的方式进行处理。通常如果垃圾回收器发现某个特定的对象已经没有他人引用了,就将其回收。然而如果某个对象只能由 WeakReference
引用,垃圾回收器仍然回收它,但要将引用这个对象的弱引用放人队列中。WeakHashMap
将周期性地检查队列,以便找出新添加的弱引用。一个弱引用进入队列意味着这个键不再被他人使用,并且已经被收集起来。于是,WeakHashMap
将删除对应的条目。
链接散列集与映射
LinkedHashSet
和 LinkedHashMap
类用来记住插人元素项的顺序,这样就可以避免在散列表中的项从表面上看是随机排列的。当条目插入到表中时,就会并入到双向链表中,图 6所示。
链接散列集用插入顺序对集合进行迭代;链接散列映射将用访问顺序,对映射条目进行迭代。每次调用 get
或 put
,受到影响的条目将从当前的位置删除,并放到条目链表的尾部(只有条目在链表中的位置会受影响,而散列表中的桶不会受影响。一个条目总位于与键散列码对应的桶中)。访问顺序对于实现高速缓存的 “最近最少使用” 原则十分重要。
枚举集与映射
EnumSet
是一个枚举类型元素集的高效实现。由于枚举类型只有有限个实例,所以 EnumSet
内部用位序列实现。如果对应的值在集中,则相应的位被置为 1。可以使用 Set
接口的常用方法来修改 EnumSet
。EnumSet
类没有公共的构造器,可以使用静态工厂方法构造这个集:
enum Weekday { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, 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);
EnumMap
是一个键类型为枚举类型的映射。它可以直接且高效地用一个值数组实现。在使用时,需要在构造器中指定键类型:
EnumMap<Weekday, Employee> personInCharge = new EnumMap<>(Weekday.class);
标识散列映射
类 IdentityHashMap
有特殊的作用。在这个类中,键的散列值不是用 hashCode
函数计算的,而是用 System.identityHashCode
方法计算的。这是 Object.hashCode
方法根据对象的内存地址来计算散列码时所使用的方式。而且,在对两个对象进行比较时,IdentityHashMap
类使用 ==
,而不使用 equals
。也就是说,不同的键对象,即使内容相同,也被视为是不同的对象。在实现对象遍历算法(如对象串行化)时,这个类非常有用,可以用来跟踪每个对象的遍历状况。
上述特殊集合的相关方法:
java.util.WeakHashMap<K, V>
WeakHashMap()
/WeakHashMap(int initialCapacity)
/WeakHashMap(int initialCapacity, float loadFactor)
:用给定的容量和填充因子构造一个空的散列映射表。
java.util.LinkedHashSet<E>
LinkedHashSet()
/LinkedHashSet(int initialCapacity)
/LinkedHashSet(int initialCapacity, float loadFactor)
:用给定的容量和填充因子构造一个空链接散列集。
java.util.LinkedHashMap<K, V>
LinkedHashMap()
/LinkedHashMap()
/LinkedHashMap(int initialCapacity, float loadFactor)
/LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)
:用给定的容量、填充因子和顺序构造一个空的链接散列映射表。accessOrder 参数为true
时表示访问顺序,为 false 时表示插入顺序。
java.util.EnumSet<E extends Enum<E>>
均为静态方法.allOf(Class<E> enumType)
:返回一个包含给定枚举类型的所有值的集。.noneOf(Class<E> enumType)
:返回一个空集,并有足够的空间保存给定的枚举类型所有的值。.range(E from, E to)
:返回一个包含 from 〜 to 之间的所有值(包括两个边界元素)的集。.of(E value)
/.of(E value, E... values)
:返回包括给定值的集。
java.util.EnumMap<K extends Enum<K>, V>
EnumMap(Class<K> keyType)
:构造一个键为给定类型的空映射。
java.util.IdentityHashMap<K, V>
IdentityHashMap()
/IdentityHashMap(int expectedMaxSize)
:构造一个空的标识散列映射集,其容量是大于 1.5 *expectedMaxSize
的 2 的最小次幂 (expectedMaxSize
的默认值是 21)。
java.lang.System
static int identityHashCode(Object obj)
:返回ObjecUiashCode
计算出来的相同散列码(根据对象的内存地址产生),即使 obj 所属的类已经重新定义了hashCode
方法也是如此。
参考资料:
- 《Java核心技术 卷1 基础知识》