JAVA学习笔记——集合

Java 集合框架

接口、实现分离

Java 集合框架与现代的数据结构类库的常见情况一样,类库也将接口 (interface) 与实现 (implementation) 分离。每一个实现都可以通过一个实现了该接口的类表示。在程序运行过程中,我们并不知道如何实现接口才能使程序达到最高的效率,因此将接口与实现分离,能够更有效地编写程序。

另外,利用这种方式,可以轻松地使用另外一种不同的实现。例如,CircularArrayQueueLinkedListQueue 都实现了 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

向前移动迭代器

图1 向前移动迭代器

更重要的是,对 next 方法和 remove 方法的调用具有互相依赖性。如果调用 remove 之前没有调用 next 将是不合法的。如果这样做,将会抛出一个 IllegalStateException 异常。

集合框架中的接口

Java 集合框架为不同类型的集合定义了大量接口, 如图 2 所示:

集合框架的接口

图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 接口。

表1 Java 库中的具体集合
集合类型描述
ArrayList一种可以动态增长和缩减的索引序列
LinkedList一种可以在任何位置进行高效地插人和删除操作的有序序列
ArrayDeque一种用循环数组实现的双端队列
HashSet一种没有重复元素的无序集合
TreeSet—种有序集
EnumSet一种包含枚举类型值的集
LinkedHashSet一种可以记住元素插入次序的集
PriorityQueue一种允许高效删除最小元素的集合
HashMap一种存储键 / 值关联的数据结构
TreeMap—种键值有序排列的映射表
EnumMap一种键值属于枚举类型的映射表
LinkedHashMap一种可以记住键 / 值项添加次序的映射表
WeakHashMap一种其值无用武之地后可以被垃圾回收器回收的映射表
IdentityHashMap一种用 == 而不是用 equals 比较键值的映射表

集合框架中的类

图3 集合框架中的类

链表

动态的 ArrayList 类和数组列表都有一个重大的缺陷,是从数组的中间位置删除一个元素要付出很大的代价,其原因是数组中处于被删除元素之后的所有元素都要向数组的前端移动,在数组中间的位置上插入一个元素也是如此。

而链表则很好地解决了这个问题,在 Java 程序设计语言中,所有链表实际上都是双向链接的 (doubly linked) ——即每个结点还存放着指向前驱结点的引用,如下图所示:

链表

图4 双向链表

与普通的迭代器相同,当迭代器调用 next 方法后,再调用 remove 方法将删除 next 方法返回的对象。由于 LinkedList 类是双向链表,迭代器 ListIterator 也同样拓展了 previous 方法,并且可以反向访问(调用 previous 方法后再调用 remove 方法,也会删除 previous 方法返回的对象,即迭代器跨越的对象)。

set 方法用一个新元素取代调用 nextprevious 方法返回的上一个元素(规则类似 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):用新元素取代 nextprevious 上次访问的元素。如果在 nextprevious 上次调用之后列表结构被修改了,将拋出一个 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 所示。要想査找表中对象的位置,就要先计算它的散列码,然后与桶的总数取余,所得到的结果就是保存这个元素的桶的索引。

在这里插入图片描述

图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():返回这个对象的散列码。散列码可以是任何整数,包括正数或负数。equalshashCode 的定义必须兼容,即如果 x.equals(y)truex.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,但是与检查数组或链表中的重复元素相比还是快很多。

表2 将元素添加到散列集和树集
文档单词总数不同的单词个数HashSetTreeSet
Alice in Wonderland2819559095 秒7 秒
The Count of Monte Cristo4663003754575 秒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 接口,并由 ArrayDequeLinkedList 类实现。这两个类都提供了双端队列,而且在必要时可以增加队列的长度。

优先级队列 (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 类库为映射提供了两个通用的实现:HashMapTreeMap。这两个类都实现了 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 将删除对应的条目。

链接散列集与映射

LinkedHashSetLinkedHashMap 类用来记住插人元素项的顺序,这样就可以避免在散列表中的项从表面上看是随机排列的。当条目插入到表中时,就会并入到双向链表中,图 6所示。

链接散列表

图6 链接散列表

链接散列集用插入顺序对集合进行迭代;链接散列映射将用访问顺序,对映射条目进行迭代。每次调用 getput,受到影响的条目将从当前的位置删除,并放到条目链表的尾部(只有条目在链表中的位置会受影响,而散列表中的桶不会受影响。一个条目总位于与键散列码对应的桶中)。访问顺序对于实现高速缓存的 “最近最少使用” 原则十分重要。

枚举集与映射

EnumSet 是一个枚举类型元素集的高效实现。由于枚举类型只有有限个实例,所以 EnumSet 内部用位序列实现。如果对应的值在集中,则相应的位被置为 1。可以使用 Set 接口的常用方法来修改 EnumSetEnumSet 类没有公共的构造器,可以使用静态工厂方法构造这个集:

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 方法也是如此。

参考资料

  1. 《Java核心技术 卷1 基础知识》
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值