20220424 Java核心技术 卷1 基础知识 9

集 合

Java 集合框架

Java 最初版本只为最常用的数据结构提供了很少的一组类:VectorStackHashtableBitSetEnumeration 接口, 其中的 Enumeration 接口提供了一种用于访问任意容器中各个元素的抽象机制。

随着 Java SE 1.2 的问世,设计人员感到是推出一组功能完善的数据结构的时机了。

将集合的接口与实现分离

Java 集合类库将接口 ( interface ) 与 实现( implementation ) 分离

以 队列 (queue ) 为例

队列接口指出可以在队列的尾部添加元素, 在队列的头部删除元素,并且可以査找队列中元素的个数。当需要收集对象, 并按照 “先进先出” 的规则检索对象时就应该使用队列

public interface Queue<E> {
    void add(E element);

    E remove();

    int size();
}

队列通常有两种实现方式: 一种是使用循环数组; 另一种是使用链表,每一个实现都可以通过一个实现了 Queue 接口的类表示。

当在程序中使用队列时,一旦构建了集合就不需要知道究竟使用了哪种实现。因此, 只有在构建集合对象时,使用具体的类才有意义。可以使用接口类型存放集合的引用。

利用这种方式,一旦改变了想法, 可以轻松地使用另外一种不同的实现。只需要对程序的一个地方做出修改, 即调用构造器的地方。

Queue<Customer> expresslane = new CircularArrayQueue<>(100);
expresslane.add(new Customer("Harry"));

expressLane = new LinkedListQueue();
expressLane.add(new Customer("Harry"));

为什么选择这种实现, 而不选择那种实现呢? 接口本身并不能说明哪种实现的效率究竟如何。

在研究 API 文档时, 会发现另外一组名字以 Abstract 开头的类, 例如, AbstractQueue 。这些类是为类库实现者而设计的。 如果想要实现自己的队列类,会发现扩展 AbstractQueue 类要比实现 Queue 接口中的所有方法轻松得多。

Collection 接口

在 Java 类库中,集合类的基本接口是 Collection 接口。

add 方法用于向集合中添加元素。如果添加元素确实改变了集合就返回 true , 如果集合没有发生变化就返回 false

iterator 方法用于返回一个实现了 Iterator 接口的对象。可以使用这个迭代器对象依次访问集合中的元素

迭代器

Iterator 接口包含 4 个方法:

public interface Iterator<E> {
    E next();
    boolean hasNext();
    default void remove();
    default void forEachRemaining(Consumer<? super E> action);
}

通过反复调用 next 方法,可以逐个访问集合中的每个元素。但是,如果到达了集合的末尾,next 方法将抛出一个 NoSuchElementException 。 因此,需要在调用 next 之前调用 hasNext 方法。如果迭代器对象还有多个供访问的元素, 这个方法就返回 true 。如果想要査看集合中的所有元素,就请求一个迭代器,并在 hasNext 返回 true 时反复地调用 next 方法。

Iterator<String> iterator = collection.iterator();
System.out.println(iterator.getClass());    // class java.util.ArrayList$Itr

while (iterator.hasNext()) {
    String next = iterator.next();
    // 
}

用 foreach 循环可以更加简练地表示同样的循环操作:

for (String element : collection) {
    // 
}

foreach 循环可以与任何实现了 Iterable 接口的对象一起工作, 这个接口只包含一个抽象方法:

public interface Iterable<T> {
    Iterator<T> iterator();
    default void forEach(Consumer<? super T> action);
    default Spliterator<T> spliterator();
}

Collection 接口扩展了 Iterable 接口。因此, 对于标准类库中的任何集合都可以使用 foreach 循环。

在 Java SE 8 中, 甚至不用写循环。可以调用 forEachRemaining 方法并提供一个 lambda 表达式(它会处理一个元素)。 将对迭代器的每一个元素调用这个 lambda 表达式,直到再没有元素为止。

iterator.forEachRemaining(element -> {
    // 
});

元素被访问的顺序取决于集合类型。 对 ArrayListHashSet 的迭代顺序不同。

Iterator 接口的 nexthasNext 方法与 Enumeration 接口的 nextElementhasMoreElements 方法的作用一样。

Java 集合类库中的迭代器查找操作与位置变更是紧密相连的。查找一个元素的唯一方法是调用 next , 而在执行查找操作的同时, 迭代器的位置随之向前移动

因此,应该将 Java 迭代器认为是位于两个元素之间。 当调用 next 时,迭代器就越过下一个元素,并返回刚刚越过的那个元素的引用

img

这里还有一个有用的推论。 可以将 Iterator.nextInputStream.read 看作为等效的。从数据流中读取一个字节,就会自动地“消耗掉” 这个字节。 下一次调用 read 将会消耗并返回输入的下一个字节。 用同样的方式, 反复地调用 next 就可以读取集合中所有元素

Iterator 接口的 remove 方法将会删除上次调用 next 方法时返回的元素。 在大多数情况下,在决定删除某个元素之前应该先看一下这个元素是很具有实际意义的。然而, 如果想要删除指定位置上的元素, 仍然需要越过这个元素。

例如, 下面是如何删除字符串集合中第一个元素的方法:

Iterator<String> iterator = collection.iterator();
iterator.next();
iterator.remove();

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

如果想删除两个相邻的元素:

Iterator<String> iterator = collection.iterator();
iterator.next();
iterator.remove();
iterator.next();
iterator.remove();

泛型实用方法

Collection 接口声明了很多实用的方法,所有的实现类都必须提供这些方法,为了能够让实现者更容易地实现这些接口, Java 类库提供了一个类 AbstractCollection

对于 Java SE 8 ,这种方法有些过时了。 如果这些方法是 Collection 接口的默认方法会更好。

java.util.Collection<E> 方法名称 方法声明 描述
iterator Iterator<E> iterator(); 返回一个用于访问集合中每个元素的迭代器
clear void clear(); 从这个集合中删除所有的元素
containsAll boolean containsAll(Collection<?> other); 如果这个集合包含 other 集合中的所有元素, 返回 true
addAll boolean addAll(Collection<? extends E> other); other 集合中的所有元素添加到这个集合。如果由于这个调用改变了集合, 返回 true
removeAll boolean removeAll(Collection<?> other); 从这个集合中删除 other 集合中存在的所有元素。如果由于这个调用改变了集合,返回 true
retainAll boolean retainAll(Collection<?> other); 从这个集合中删除所有与 other 集合中的元素不同的元素。 如果由于这个调用改变了集合, 返回 true
removeIf default boolean removeIf(Predicate<? super E> filter) 从这个集合删除 filter 返回 true 的所有元素。如果由于这个调用改变了集合, 则返回 true
toArray Object[] toArray(); 返回这个集合的对象数组
toArray <T> T[] toArray(T[] arrayToFi11); 返回这个集合的对象数组。 如果 arrayToFill 足够大, 就将集合中的元素填入这个数组中。剩余空间填补 null ; 否则, 分配一个新数组, 其成员类型与 arrayToFill 的成员类型相同, 其长度等于集合的大小, 并填充集合元素

集合框架中的接口

Java 集合框架为不同类型的集合定义了大量接口

img

集合有两个基本接口:CollectionMap

List 是一个有序集合( ordered collection )。元素会增加到容器中的特定位置。 可以采用两种方式访问元素:使用迭代器访问, 或者使用一个整数索引来访问。后一种方法称为 随机访问( random access ),因为这样可以按任意顺序访问元素。与之不同, 使用迭代器访问时,必须顺序地访问元素。

void add(int index, E element)
void remove(int index)
E get(int index)
E set(int index, E element)

Listlterator 接口是 Iterator 的一个子接口。它定义了一个方法用于在迭代器位置前面增加一个元素:

void add(E element)

坦率地讲,集合框架的这个方面设计得很不好。实际中有两种有序集合,其性能开销有很大差异。由数组支持的有序集合可以快速地随机访问,因此适合使用 List 方法并提供一个整数索引来访问。与之不同, 链表尽管也是有序的, 但是随机访问很慢,所以最好使用迭代器来遍历。 如果原先提供两个接口就会容易一些了。

为了避免对链表完成随机访问操作,Java SE 1.4 引入了一个标记接口 RandomAccess 。 这个接口不包含任何方法, 不过可以用它来测试一个特定的集合是否支持高效的随机访问:

if (c instanceof RandomAccess) {

} else {
    
}

Set 接口等同于 Collection 接口,不过其方法的行为有更严谨的定义。集(set )add 方法不允许增加重复的元素。要适当地定义集的 equals 方法:只要两个集包含同样的元素就认为是相等的,而不要求这些元素有同样的顺序。hashCode 方法的定义要保证包含相同元素的两个集会得到相同的散列码。

既然方法签名是一样的, 为什么还要建立一个单独的接口呢? 从概念上讲, 并不是所有集合都是集。建立一个 Set 接口可以让程序员编写只接受集的方法。

SortedSetSortedMap 接口会提供用于排序的比较器对象,这两个接口定义了可以得到集合子集视图的方法。

最后,Java SE 6 引人了接口 NavigableSetNavigableMap, 其中包含一些用于搜索和遍历有序集和映射的方法。(理想情况下,这些方法本应当直接包含在 SortedSetSortedMap 接口中)。TreeSetTreeMap 类实现了这些接口

具体的集合类

Java 库中的具体集合

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

Collection 接口实现类:

img

Map 接口实现类:

img

链表( LinkedList

数组和数组列表都有一个重大的缺陷。这就是从数组的中间位置删除一个元素要付出很大的代价,其原因是数组中处于被删除元素之后的所有元素都要向数组的前端移动。在数组中间的位置上插入一个元素也是如此。 链表( linked list ) 解决了这个问题

在 Java 程序设计语言中, 所有链表实际上都是双向链接的 (doubly linked ) —— 即每个结点还存放着指向前驱结点的引用

链表与泛型集合之间有一个重要的区别。链表是一个有序集合(ordered collection ) ,每个对象的位置十分重要。

LinkedList.add 方法将对象添加到链表的尾部。但是,常常需要将元素添加到链表的中间。由于迭代器是描述集合中位置的, 所以这种依赖于位置的 add 方法将由迭代器负责。

只有对自然有序的集合使用迭代器添加元素才有实际意义。例如, 下一节将要讨论的集 (set ) 类型,其中的元素完全无序。 因此, 在 Iterator 接口中就没有 add 方法。相反地,集合类库提供了子接口 Listlterator , 其 中 包 含 add 方 法

当用一个刚刚由 Iterator 方法返回, 并且指向链表表头的迭代器调用 add 操作时, 新添加的元素将变成列表的新表头。当迭代器越过链表的最后一个元素时(即 hasNext 返回 false ) ,添加的元素将变成列表的新表尾。 如果链表有 n 个元素,有 n+1 个位置可以添加新元素。这些位置与迭代器的 n+1 个可能的位置相对应。

在调用 next 之后,remove 方法确实与 BACKSPACE 键一样删除了迭代器左侧的元素。但是, 如果调用 previous 就会将右侧的元素删除掉, 并且不能连续调用两次 removeadd 方法只依赖于迭代器的位置, 而 remove 方法依赖于迭代器的状态。

set 方法用一个新元素取代调用 nextprevious 方法返回的上一个元素。

如果迭代器发现它的集合被另一个迭代器修改了, 或是被该集合自身的方法修改了, 就会抛出一个 ConcurrentModificationException 异常

List<String> list = new LinkedList<>();
list.add("Amy");
list.add("Bob");
list.add("Carl");

ListIterator<String> iter1 = list.listIterator();
ListIterator<String> iter2 = list.listIterator();

iter1.next();
iter1.remove();
iter2.next(); // 抛出异常 ConcurrentModificationException

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

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

对于并发修改列表的检测肴一个奇怪的例外。链表只负责跟踪对列表的结构性修改, 例如,添加元素、 删除元素。set 方法不被视为结构性修改。 可以将多个迭代器附加给一个链表, 所有的迭代器都调用 set 方法对现有结点的内容进行修改。

可以使用 Listlterator 类从前后两个方向遍历链表中的元素, 并可以添加、 删除元素。

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

尽管如此, LinkedList 类还是提供了一个用来访问某个特定元素的 get 方法,这个方法的效率并不太高。 如果发现自己正在使用这个方法,说明有可能对于所要解决的问题使用了错误的数据结构。

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

for (int i = 0; i < list.sizeO;i++)
do something with list.get(i);

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

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

如果链表中只有很少几个元素, 就完全没有必要为 get 方法和 set 方法的开销而烦恼。

使用链表的唯一理由是尽可能地减少在列表中间插人或删除元素所付出的代价。 如果列表只有少数几个元素, 就完全可以使用 ArrayList

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

java.util.List<E> 方法名称 方法声明 描述
listIterator ListIterator<E> listIterator(); 返回一个列表迭代器, 以便用来访问列表中的元素
listIterator ListIterator<E> listIterator(int index); 返回一个列表迭代器, 以便用来访问列表中的元素, 这个元素是第一次调用 next 返回的给定索引的元素
add void add(int index, E element); 在给定位置添加一个元素
addAll boolean addAll(int index, Collection<? extends E> c); 将某个集合中的所有元素添加到给定位置
remove E remove(int index); 删除给定位置的元素并返回这个元素
get E get(int index); 获取给定位置的元素
set E set(int index, E element); 用新元素取代给定位置的元素, 并返回原来那个元素
indexOf int indexOf(Object o); 返回与指定元素相等的元素在列表中第一次出现的位置, 如果没有这样的元素将返回 -1
lastIndexOf int lastIndexOf(Object o); 返回与指定元素相等的元素在列表中最后一次出现的位置, 如果没有这样的元素将返回 -1
java.util.ListIterator<E> 方法名称 方法声明 描述
add void add(E e); 在当前位置前添加一个元素
set void set(E e); 用新元素取代 nextprevious 上次访问的元素。 如果在 nextprevious 上次调用之后列表结构被修改了, 将拋出一个 IllegalStateException 异常
hasPrevious boolean hasPrevious(); 当反向迭代列表时, 还有可供访问的元素, 返回 true
previous E previous(); 返回前对象。如果已经到达了列表的头部, 抛出一个 NoSuchElementException 异常
nextIndex int nextIndex(); 返回下一次调用 next 方法时将返回的元素索引
previousIndex int previousIndex(); 返回下一次调用 previous 方法时将返回的元素索引
java.util.LinkedList<E> 方法名称 方法声明 描述
构造器 LinkedList() 构造一个空链表
构造器 LinkedList(Collection<? extends E> c) 构造一个链表, 并将集合中所有的元素添加到这个链表中
addFirst void addFirst(E e) 将某个元素添加到列表的头部
addLast void addLast(E e) 将某个元素添加到列表的尾部
getFirst E getFirst() 返回列表头部的元素
getLast E getLast() 返回列表尾部的元素
removeFirst E removeFirst() 删除并返回列表头部的元素
removeLast E removeLast() 删除并返回列表尾部的元素

数组列表( ArrayList

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

为什么要用 ArrayList 取代 Vector 呢? 原因很简单:Vector 类的所有方法都是同步的。 而 ArrayList 方法不是同步的,因此
,建议在不需要同步时使用 ArrayList , 而不要使用 Vector

散列集( HashSet

链表和数组可以按照人们的意愿排列元素的次序。但是, 如果想要査看某个指定的元素, 却又忘记了它的位置, 就需要访问所有元素, 直到找到为止。如果集合中包含的元素很多, 将会消耗很多时间。

如果不在意元素的顺序, 可以有几种能够快速査找元素的数据结构。其缺点是无法控制元素出现的次序。 它们将按照有利于其操作目的的原则组织数据。

有一种众所周知的数据结构, 可以快速地査找所需要的对象, 这就是 散列表( hash table ) 。散列表为每个对象计算一个整数, 称为 散列码(hash code ) 。散列码是由对象的实例域产生的一个整数。更准确地说, 具有不同数据域的对象将产生不同的散列码。

如果自定义类,就要负责实现这个类的 hashCode 方法。 注意,自己实现的 hashCode 方法应该与 equals 方法兼容,即如果 a.equals(b)true , a 与 b 必须具有相同的散列码。

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

在 Java 中, 散列表用链表数组实现。每个列表被称为桶 (bucket ) 。要想査找表中对象的位置, 就要先计算它的散列码, 然后与桶的总数取余, 所得到的结果就是保存这个元素的桶的索引。

或许会很幸运, 在这个桶中没有其他元素, 此时将元素直接插人到桶中就可以了。有时,也会遇到桶中有其他元素的情况, 这种现象被称为 散列冲突( hash collision ) 。这时, 需要用新对象与桶中的所有对象进行比较,査看这个对象是否已经存在。如果散列码是合理且随机分布的, 桶的数目也足够大, 需要比较的次数就会很少。

在 JavaSE 8 中, 桶满时会从链表变为平衡二叉树。如果选择的散列函数不当, 会产生很多冲突。

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

如果大致知道最终会有多少个元素要插人到散列表中, 就可以设置桶数。通常, 将桶数设置为预计元素个数的 75% ~ 150% 。 标准类库使用的桶数是 2 的幂, 默认值为 16 (为表大小提供的任何值都将被自动地转换为 2 的下一个幂)

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

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

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

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

java.util.HashSet<E> 方法名称 方法声明 描述
构造器 HashSet() 构造一个空散列表
构造器 HashSet(Collection<? extends E> c) 构造一个散列集, 并将集合中的所有元素添加到这个散列集中
构造器 HashSet(int initialCapacity) 构造一个空的具有指定容量(桶数)的散列集
构造器 HashSet(int initialCapacity, float loadFactor) 构造一个具有指定容量和装填因子(一个 0.0 ~ 1.0 之间的数值, 确定散列表填充的百分比, 当大于这个百分比时, 散列表进行再散列)的空散列集
java.lang.Object 方法名称 方法声明 描述
hashCode int hashCode(); 返回这个对象的散列码。散列码可以是任何整数, 包括正数或负数。equalshashCode 的定义必须兼容,即如果 x.equals(y)true , x.hashCode() 必须等于 y.hashCode()

树集 ( TreeSet

树集是一个有序集合 ( sorted collection ) 。 可以以任意顺序将元素插入到集合中。在对集合进行遍历时, 每个值将自动地按照排序后的顺序呈现。

TreeSet 类排序是用树结构完成的,当前实现使用的是红黑树( red-black tree )。 每次将一个元素添加到树中时,都被放置在正确的排序位置上。因此,迭代器总是以排好序的顺序访问每个元素。

要使用树集, 必须能够比较元素。这些元素必须实现 Comparable 接口,或者构造集时必须提供一个 Comparator

将一个元素添加到树中要比添加到散列表中慢,但是,与检查数组或链表中的重复元素相比还是快很多。

树的排序必须是 全序 。 也就是说, 任意两个元素必须是可比的, 并且只有在两个元素相等时结果才为 0。

从 JavaSE 6 起, TreeSet 类实现了 NavigableSet 接口。 这个接口增加了几个便于定位元素以及反向遍历的方法。

java.util.TreeSet<E> 方法名称 方法声明 描述
构造器 TreeSet(Comparator<? super E> comparator) 构造一个空树集,指定比较器
构造器 TreeSet(SortedSet<E> s) 构造一个树集, 并增加一个集合或有序集中的所有元素(对于后一种情况, 要使用同样的顺序)
java.util.SortedSet<E> 方法名称 方法声明 描述
构造器 TreeSet(Comparator<? super E> comparator) 构造一个空树集,指定比较器
first
last
E first();
E last();
返回有序集中的最小元素或最大元素
java.util.NavigableSet<E> 方法名称 方法名称 描述
higher
lower
E higher(E value)
E lower(E value)
返回大于 value 的最小元素或小于 value 的最大元素,如果没有这样的元素则返回 null
ceiling
floor
E ceiling(E value)
E floor(E value)
返回大于等于 value 的最小元素或小于等于 value 的最大元素, 如果没有这样的元素则返回 null
pollFirst
pollLast
E pollFirst()
E pollLast()
删除并返回这个集中的最大元素或最小元素, 这个集为空时返回 null
descendingIterator Iterator<E> descendingIterator() 返回一个按照递减顺序遍历集中元素的迭代器
代码示例
TreeSet<Integer> treeSet = new TreeSet<>();
for (int i = 0; i < 10; i++) {
    treeSet.add(i);
}

System.out.println(treeSet);    // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

System.out.println(treeSet.first());    // 0
System.out.println(treeSet.last());     // 9

System.out.println(treeSet.higher(3));      // 4
System.out.println(treeSet.lower(3));      // 2

System.out.println(treeSet.ceiling(3));     // 3
System.out.println(treeSet.floor(3));     // 3

System.out.println(treeSet.pollFirst());    // 0
System.out.println(treeSet.pollLast());     // 9
System.out.println(treeSet);    // [1, 2, 3, 4, 5, 6, 7, 8]

System.out.println("===========================");
Iterator<Integer> iterator = treeSet.descendingIterator();
while (iterator.hasNext()) {
    Integer next = iterator.next();
    System.out.println(next);       // 87654321
}
System.out.println("===========================");

队列与双端队列( QueueDeque

队列可以让人们有效地在尾部添加一个元素, 在头部删除一个元素。有两个端头的队列, 即双端队列, 可以让人们有效地在头部和尾部同时添加或删除元素。不支持在队列中间添加元素。在 Java SE 6 中引人了 Deque 接口, 并由 ArrayDequeLinkedList 类实现。这两个类都提供了双端队列,而且在必要时可以增加队列的长度。

java.util.Queue<E> 方法名称 方法声明 描述
add
offer
boolean add(E element)
boolean offer(E element)
如果队列没有满,将给定的元素添加到这个双端队列的尾部并返回 true 。如果队列满了,第一个方法将拋出一个 IllegalStateException , 而第二个方法返回 false
remove
poll
E remove()
E poll()
假如队列不空,删除并返回这个队列头部的元素。如果队列是空的,第一个方法抛出NoSuchElementException , 而第二个方法返回 null
element
peek
E element()
E peek()
如果队列不空,返回这个队列头部的元素, 但不删除。如果队列空,第一个方法将拋出一个 NoSuchElementException , 而第二个方法返回 null
java.util.Deque<E> 方法名称 方法声明 描述
addFirst
addLast
offerFirst
offerLast
void addFirst(E element)
void addLast(E element)
boolean offerFirst(E element)
boolean offerLast( E element)
将给定的对象添加到双端队列的头部或尾部。如果队列满了,前面两个方法将拋出一个 IllegalStateException , 而后面两个方法返回 false
removeFirst
removeLast
pollFirst
pollLast
E removeFirst()
E removeLast()
E pollFirst()
E pollLast()
如果队列不空,删除并返回队列头部的元素。 如果队列为空,前面两个方法将拋出一个 NoSuchElementException , 而后面两个方法返回 null
getFirst
getLast
peekFirst
peekLast
E getFirst()
E getLast()
E peekFirst()
E peekLast()
如果队列非空,返回队列头部的元素, 但不删除。 如果队列空,前面两个方法将拋出一个 NoSuchElementException , 而后面两个方法返回 null
java.util.ArrayDeque<E> 方法名称 方法声明 描述
构造器 ArrayDeque()
ArrayDeque(int initialCapacity)
用初始容量 16 或给定的初始容量构造一个无限双端队列

优先级队列( PriorityQueue

优先级队列(priority queue) 中的元素可以按照任意的顺序插人,却总是按照排序的顺序进行检索。也就是说,无论何时调用 remove 方法,总会获得当前优先级队列中最小的元素。

然而,优先级队列并没有对所有的元素进行排序。如果用迭代的方式处理这些元素,并不需要对它们进行排序。优先级队列使用了一个优雅且高效的数据结构,称为 堆(heap ) 。堆是一个可以自我调整的二叉树,对树执行添加 (add) 和删除(remore) 操作, 可以让最小的元素移动到根,而不必花费时间对元素进行排序。

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

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

TreeSet 中的迭代不同,这里的迭代并不是按照元素的排列顺序访问的(按照堆的存放顺序)。而删除却总是删掉剩余元素中优先级数最小的那个元素。

java.util.PriorityQueue<E> 方法名称 方法声明 描述
构造器 PriorityQueue()
PriorityQueue(int initialCapacity)
构造一个用于存放 Comparable 对象的优先级队列
构造器 PriorityQueue(int initialCapacity, Comparator<? super E> comparator) 构造一个优先级队列, 并用指定的比较器对元素进行排序

映 射

我们知道某些键的信息,并想要查找与之对应的元素。 映射( map ) 数据结构就是为此设计的。映射用来存放键 / 值对。

基本映射操作

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

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

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

要迭代处理映射的键和值, 最容易的方法是使用 forEach 方法。可以提供一个接收键和值的 lambda 表达式:

staff.forEach((k, v) -> System.out.println("key=" + k + ", value=" + v));
java.util.Map<K,V> 方法名称 方法声明 描述
getOrDefault default V getOrDefault(Object key, V defaultValue) 获得与键关联的值;返回与键关联的对象, 或者如果未在映射中找到这个键, 则返回 defaultValue
putAll void putAll(Map<? extends K, ? extends V> m); 将给定映射中的所有条目添加到这个映射中
containsKey boolean containsKey(Object key); 如果在映射中已经有这个键, 返回 true
containsValue boolean containsValue(Object value); 如果映射中已经有这个值, 返回 true
forEach default void forEach(BiConsumer<? super K, ? super V> action) 对这个映射中的所有键 / 值对应用这个动作
java.util.SortedMap<K,V> 方法名称 方法声明 描述
comparator Comparator<? super K> comparator(); 返回对键进行排序的比较器。 如果键是用 Comparable 接口的 compareTo 方法进行比较的,返回 null
firstKey
lastKey
K firstKey();
K lastKey();
返回映射中最小元素和最大元素

更新映射项

处理映射时的一个难点就是更新映射项。正常情况下, 可以得到与一个键关联的原值,完成更新, 再放回更新后的值。不过,必须考虑一个特殊情况, 即键第一次出现。下面来看一个例子,使用一个映射统计一个单词在文件中出现的频度。看到一个单词(word) 时, 我
们将计数器增 1, 如下所示:

Map<String, Integer> counts = new HashMap<>();
counts.put(word, counts.get(word) + 1);

这是可以的, 不过有一种情况除外:就是第一次看到 word 时。在这种情况下,get 会返回 null ,因此会出现一个 NullPointerException 异常。

作为一个简单的补救, 可以使用 getOrDefault 方法:

counts.put(word, counts.getOrDefault(word, 0) + 1);

另一种方法是首先调用 putIfAbsent 方法。只有当键原先存在时才会放入一个值:

counts.putIfAbsent(word, 0);
counts.put(word, counts.get(word) + 1);

不过还可以做得更好。merge 方法可以简化这个常见的操作。如果键原先不存在,下面的调用将把 word 与 1 关联,否则使用 Integer::sum 函数组合原值和 1 (也就是将原值与 1 求和) 。

counts.merge(word, 1, Integer::sum);

另外一些更新映射项的方法:

java.util.Map<K,V> 方法名称 方法声明 描述
merge default V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) 如果 key 与一个非 null 值 v 关联, 将函数应用到 v 和 value , 将 key 与结果关联, 或者如果结果为 null , 则删除这个键。否则, 将 key 与 value 关联, 返回 get(key)
compute default V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) 将函数应用到 key 和 get(key) 。将 key 与结果关联, 或者如果结果为 null , 则删除这个键。返回 get(key)
computeIfPresent default V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) 如果 key 与一个非 null 值 v 关联, 将函数应用到 key 和 v, 将 key 与结果关联, 或者如果结果为 null , 则删除这个键。返回 get(key)
computeIfAbsent default V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) 将函数应用到 key , 除非 key 与一个非 null 值关联。将 key 与结果关联, 或者如果结果为 null , 则删除这个键。返回 get(key)
replaceAll default void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) 在所有映射项上应用函数。将键与非 null 结果关联, 对于 null 结果, 则将相应的键删除
Map<String, Integer> counts = new HashMap<>();
counts.put("k1", 1);
counts.put("k2", 2);
counts.put("k3", 3);

counts.compute("k1", (k, v) -> v + 10);
// counts.compute("k4", (k, v) -> v + 10); // NullPointerException
counts.computeIfAbsent("k2", k -> 10);
counts.computeIfPresent("k3", (k, v) -> v + 10);

System.out.println(counts);     // {k1=11, k2=2, k3=13}


counts.replaceAll((k, v) -> v * 2);
System.out.println(counts);     // {k1=22, k2=4, k3=26}

映射视图

可以得到映射的视图(View ) ——这是实现了 Collection 接口或某个子接口的对象。

有 3 种视图: 键集、 值集合(不是一个集) 以及键 / 值对集。键和键 / 值对可以构成一个集, 因为映射中一个键只能有一个副本。 下面的方法:

  • Set<K> keySet()
  • Collection<V> values()
  • Set<Map.Entry<K, V>> entrySet();

会分别返回这 3 个视图(条目集的元素是实现 Map.Entry 接口的类的对象)

3 个视图都是可以删除,不可以增加元素

需要说明的是, keySet 不是 HashSetTreeSet , 而是实现了 Set 接口的另外某个类的对象。

如果在键集视图上调用迭代器的 remove 方法, 实际上会从映射中删除这个键和与它关联的值。不过,不能向键集视图增加元素。另外, 如果增加一个键而没有同时增加值也是没有意义的。 如果试图调用 add 方法, 它会抛出一个 UnsupportedOperationException 。 条目集视图有同样的限制。

Map<String, Integer> counts = new HashMap<>();
counts.put("k1", 1);
counts.put("k2", 2);
counts.put("k3", 3);

Set<String> keySet = counts.keySet();
keySet.remove("k1");

System.out.println(counts);     // {k2=2, k3=3}

// keySet.add("k1");   // 抛出异常:UnsupportedOperationException
java.util.Map<K,V> 方法名称 方法声明 描述
entrySet Set<Map.Entry<K, V>> entrySet(); 返回 Map.Entry 对象(映射中的键 / 值对)的一个集视图。可以从这个集中删除元素,它们将从映射中删除,但是不能增加任何元素
keySet Set<K> keySet(); 返回映射中所有键的一个集视图。可以从这个集中删除元素,键和相关联的值将从映射中删除, 但是不能增加任何元素
values Collection<V> values(); 返回映射中所有值的一个集合视图。可以从这个集合中删除元素, 所删除的值及相应的键将从映射中删除, 不过不能增加任何元素
java.util.Map.Entry<K,V> 方法名称 方法声明 描述
getKey
getValue
K getKey();
V getValue();
返回这一条目的键或值
setValue V setValue(V value); 将相关映射中的值改为新值, 并返回原来的值

弱散列映射( WeakHashMap

设计 WeakHashMap 类是为了解决一个有趣的问题。 如果有一个值,对应的键已经不再使用了, 将会出现什么情况呢? 假定对某个键的最后一次引用已经消亡,不再有任何途径引用这个值的对象了。但是,由于在程序中的任何部分没有再出现这个键, 所以, 这个键 / 值
对无法从映射中删除。

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

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

@Data
@AllArgsConstructor
class Key {
    private Integer i;
}

Key a = new Key(1);
Key b = new Key(2);

Map<Key, String> weakHashMap = new WeakHashMap<>();
weakHashMap.put(a, "a1");
weakHashMap.put(b, "b1");

a = null;
b = null;

System.out.println("GC 前:" + weakHashMap);
System.gc();
System.out.println("GC 后:" + weakHashMap);

这里碰到一个问题:

// String a = new String("1");  // 会被GC
// String b = new String("2");

String a = "1";     // 不会被GC
String b = "2";

Map<String, String> weakHashMap = new WeakHashMap<>();
weakHashMap.put(a, "a1");
weakHashMap.put(b, "b1");

a = null;
b = null;

System.out.println("GC 前:" + weakHashMap);
System.gc();
System.out.println("GC 后:" + weakHashMap);

推测如果使用直接字符串赋值,不被 GC 的原因可能与字符串常量池有关。

链接散列集与映射 ( LinkedHashSetLinkedHashMap

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

LinkedHashMap 遍历时,默认使用的是 插入顺序 ,通过配置 accessOrder 可以修改为使用 访问顺序 ,最近访问的会被放在最后遍历。

accessOrder 参数为 true 时表示访问顺序, 为 false 时表示插入顺序。

Map<String, String> linkedHashMap = new LinkedHashMap<>(16, 0.75f, true);

每次调用 getput , 受到影响的条目将从当前的位置删除, 并放到条目链表的尾部(只有条目在链表中的位置会受影响, 而散列表中的桶不会受影响。一个条目总位于与键散列码对应的桶中)

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

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

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

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

Map<String, String> linkedHashMap = new LinkedHashMap<String, String>(128, 0.75f, true) {
    @Override
    protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
        return size() > 3;
    }
};

for (int i = 0; i < 10; i++) {
    linkedHashMap.put("k" + i, "v" + i);
    System.out.println("i==" + i + " :: " + linkedHashMap);
}

linkedHashMap.get("k7");
System.out.println(linkedHashMap);  // {k8=v8, k9=v9, k7=v7}
linkedHashMap.put("k10", "v10");
System.out.println(linkedHashMap);  // {k9=v9, k7=v7, k10=v10}

输出:

i==0 :: {k0=v0}
i==1 :: {k0=v0, k1=v1}
i==2 :: {k0=v0, k1=v1, k2=v2}
i==3 :: {k1=v1, k2=v2, k3=v3}
i==4 :: {k2=v2, k3=v3, k4=v4}
i==5 :: {k3=v3, k4=v4, k5=v5}
i==6 :: {k4=v4, k5=v5, k6=v6}
i==7 :: {k5=v5, k6=v6, k7=v7}
i==8 :: {k6=v6, k7=v7, k8=v8}
i==9 :: {k7=v7, k8=v8, k9=v9}
{k8=v8, k9=v9, k7=v7}
{k9=v9, k7=v7, k10=v10}

枚举集与映射( EmimSetEnumMap

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

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

EnumSet<TimeUnit> allOf = EnumSet.allOf(TimeUnit.class);
EnumSet<TimeUnit> noneOf = EnumSet.noneOf(TimeUnit.class);
EnumSet<TimeUnit> range = EnumSet.range(TimeUnit.MILLISECONDS, TimeUnit.MINUTES);
EnumSet<TimeUnit> of = EnumSet.of(TimeUnit.SECONDS, TimeUnit.MILLISECONDS, TimeUnit.DAYS, TimeUnit.NANOSECONDS);
EnumSet<TimeUnit> copyOf1 = EnumSet.copyOf(of);
EnumSet<TimeUnit> copyOf2 = EnumSet.copyOf(Arrays.asList(TimeUnit.SECONDS, TimeUnit.MILLISECONDS, TimeUnit.DAYS, TimeUnit.NANOSECONDS));

可以使用 Set 接口的常用方法来修改 EnumSet

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

EnumMap<TimeUnit, String> enumMap = new EnumMap<>(TimeUnit.class);
enumMap.put(TimeUnit.SECONDS, "s");
enumMap.put(TimeUnit.DAYS, "d");
System.out.println(enumMap);    // {SECONDS=s, DAYS=d}

可以使用 Map 接口的常用方法来修改 EnumMap

EnumSet 的 API 文档中, 将会看到 E extends Enum<E> 这样奇怪的类型参数。简单地说, 它的意思是 “ E 是一个枚举类型 ”。 所有的枚举类型都扩展于泛型 Enum 类。例如, TimeUnit 扩展 Enum<TimeUnit>

标识散列映射 ( IdentityHashMap

IdentityHashMap 有特殊的作用。在这个类中, 键的散列值不是用 hashCode 函数计算的, 而是用 System.identityHashCode 方法计算的。 这是 Object.hashCode 方法根据对象的内存地址来计算散列码时所使用的方式,即使类中已经重新定义了 hashCode 方法也是如此 。 而且, 在对两个对象进行比较时, IdentityHashMap 类使用 == , 而不使用 equals

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

IdentityHashMap<String, String> identityHashMap = new IdentityHashMap<>();
identityHashMap.put("a", "v1");
identityHashMap.put("a", "v2");
identityHashMap.put(new String("a"), "v3");
System.out.println(identityHashMap);    // {a=v2, a=v3}

视图与包装器

通过使用 视图 ( views ) 可以获得其他的实现了 Collection 接口和 Map 接口的对象。映射类的 keySet 方法就是一个这样的示例。初看起来, 好像这个方法创建了一个新集, 并将映射中的所有键都填进去,然后返回这个集。但是, 情况并非如此。取而代之的是:keySet 方法返回一个实现 Set 接口的类对象, 这个类的方法对原映射进行操作。这种集合称为 视图

视图技术在集框架中有许多非常有用的应用。

轻量级集合包装器( ArraysCollections

Arrays 类的静态方法 asList 将返回一个包装了普通 Java 数组的 List 包装器。这个方法可以将数组传递给一个期望得到列表或集合参数的方法。 返回的对象不是 ArrayList ,而是静态内部类 java.util.Arrays.ArrayList它是一个视图对象, 带有访问底层数组的 getset 方法。改变数组大小的所有方法(例如,与迭代器相关的 addremove 方法)都会抛出一个 UnsupportedOperationException 异常。

String[] strArr = {"a", "b", "c"};
List<String> list1 = Arrays.asList(strArr);

Collections.nCopies(n, anObject) 将返回一个实现了 List 接口的不可修改的对象, 并给人一种包含 n 个元素, 每个元素都像是一个 anObject 的错觉

Collections.singleton(anObject) 将返回一个视图对象。这个对象实现了 Set 接口。返回的对象实现了一个不可修改的单元素集, 而不需要付出建立数据结构的开销。 singletonList 方法与 singletonMap 方法类似。

类似地,对于集合框架中的每一个接口,还有一些方法可以生成空集、 列表、 映射, 等等。特别是, 集的类型可以推导得出:

Set<String> deepThoughts = Collections.emptySet();
Collections.emptyEnumeration
Collections.emptyIterator
Collections.emptyListIterator
Collections.emptyList
Collections.emptySet
Collections.emptySortedSet
Collections.emptyNavigableSet
Collections.emptyMap
Collections.emptySortedMap
Collections.emptyNavigableMap

子范围( subheadtail

可以为很多集合建立 子范围(subrange ) 视图

可以使用 subList 方法来获得一个列表的子范围视图。第一个索引包含在内, 第二个索引则不包含在内(包左不包右)。

List<String> list = IntStream.range(0, 10).boxed().map(String::valueOf).collect(Collectors.toList());
List<String> group2 = list.subList(3, 8);

System.out.println(list);       // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
System.out.println(group2);     // [3, 4, 5, 6, 7]

group2.clear();

System.out.println(list);   // [0, 1, 2, 8, 9]
System.out.println(group2);     // []

对于有序集和映射, 可以使用排序顺序而不是元素位置建立子范围。SortedSet 接口声明了 3 个方法:

SortedSet<E> subSet(E from, E to)
SortedSet<E> headSet(E to)
SortedSet<E> tailSet(E from)  

这些方法将返回大于等于 from 且小于 to 的所有元素子集。有序映射也有类似的方法:

SortedMap<K, V> subMap(K from, K to)
SortedMap<K, V> headMap(K to)
SortedMap<K, V> tailMap(K from)

返回映射视图, 该映射包含键落在指定范围内的所有元素。

Java SE 6 引入的 NavigableSet 接口赋予子范围操作更多的控制能力。可以指定是否包括边界:

NavigableSet<E> subSet(E from, boolean fromInclusive, E to, boolean toInclusive)
NavigableSet<E> headSet(E to, boolean toInclusive)
Navigab1eSet<E> tailSet(E from, boolean fromInclusive)  

NavigableMap 接口赋予也有类似方法:

NavigableMap<K,V> subMap(K from, boolean fromInclusive, K to, boolean toInclusive);
NavigableMap<K,V> headMap(K to, boolean inclusive);
NavigableMap<K,V> tailMap(K from, boolean inclusive);

不可修改的视图( unmodifiable

Collections 还有几个方法, 用于产生集合的 不可修改视图 ( unmodifiable views ) 。这些视图对现有集合增加了一个运行时的检查。如果发现试图对集合进行修改, 就抛出一个异常,同时这个集合将保持未修改的状态。

可以使用下面 8 种方法获得不可修改视图:

Collections.unmodifiableCollection
Collections.unmodifiableList
Collections.unmodifiableSet
Collections.unmodifiableSortedSet
Collections.unmodifiableNavigableSet
Collections.unmodifiableMap
Collections.unmodifiableSortedMap
Collections.unmodifiableNavigableMap  

每个方法都定义于一个接口。 例如, Collections.unmodifiableListArrayListLinkedList 或者任何实现了 List 接口的其他类一起协同工作。

例如, 假设想要查看某部分代码, 但又不触及某个集合的内容, 就可以进行下列操作:

List<String> staff = new LinkedList<>();
// ...
lookAt(Collections.unmodifiableList(staff));

Collections.unmodifiableList 方法将返回一个实现 List 接口的类对象。 其访问器方法将从 staff 集合中获取值。当然,lookAt 方法可以调用 List 接口中的所有方法, 而不只是访问器。但是所有的更改器方法(例如, add) 已经被重新定义为抛出一个 UnsupportedOperationException 异常,而不是将调用传递给底层集合。

不可修改视图并不是集合本身不可修改。仍然可以通过集合的原始引用(在这里是 staff ) 对集合进行修改。并且仍然可以让集合的元素调用更改器方法。

由于视图只是包装了接口而不是实际的集合对象, 所以只能访问接口中定义的方法。例如, LinkedList 类有一些非常方便的方法,addFirstaddLast ,它们都不是 List 接口的方法,不能通过不可修改视图进行访问。

警告unmodifiableCollection 方法将返回一个集合, 它的 equals 方法不调用底层集合的 equals 方法。相反, 它继承了 Object 类的 equals 方法, 这个方法只是检测两个对象是否是同一个对象。 如果将集或列表转换成集合, 就再也无法检测其内容是否相同了。视图就是以这种方式运行的, 因为内容是否相等的检测在分层结构的这一层上没有定义妥当。视图将以同样的方式处理 hashCode 方法。

然而,unmodifiableSet 类和 unmodifiableList 类 却 使 用 底 层 集 合 的 equals 方 法 和 hashCode 方 法。

同步视图( synchronized

类库的设计者使用视图机制来确保常规集合的线程安全, 而不是实现线程安全的集合类。

可以使用下面 8 种方法获得同步视图 :

Collections.synchronizedCollection
Collections.synchronizedList
Collections.synchronizedSet
Collections.synchronizedSortedSet
Collections.synchronizedNavigableSet
Collections.synchronizedMap
Collections.synchronizedSortedMap
Collections.synchronizedNavigableMap  

受查视图( checked

“受査” 视图用来对泛型类型发生问题时提供调试支持。

将错误类型的元素混入泛型集合中的问题极有可能发生。例如:

ArrayList<String> strings = new ArrayList<>();
ArrayList rawList = strings;
rawList.add(new Date()); // 不会报错

这个错误的 add 命令在运行时检测不到。相反, 只有在稍后的另一部分代码中调用 get 方法, 并将结果转化为 String 时,这个类才会抛出异常。

受査视图可以探测到这类问题。

视图的 add 方法将检测插入的对象是否属于给定的类。 如果不属于给定的类, 就立即抛出一个 ClassCastException 。这样做的好处是错误可以在正确的位置得以报告:

List<String> safestrings = Collections.checkedList(strings, String.class);
List rawList2 = safestrings;
rawList2.add(new Date());   // 抛出异常 ClassCastException

视图的 add 方法将检测插入的对象是否属于给定的类。 如果不属于给定的类, 就立即抛出一个 ClassCastException 。这样做的好处是错误可以在正确的位置得以报告

可以使用下面 8 种方法获得受査视图 :

Collections.checkedCollection
Collections.checkedList
Collections.checkedSet
Collections.checkedSortedSet
Collections.checkedNavigableSet
Collections.checkedMap
Collections.checkedSortedMap
Collections.checkedNavigableMap  

还对 Queue 提供了受查视图:

Collections.checkedQueue

警告: 受查视图受限于虚拟机可以运行的运行时检查(泛型类型擦除)。 例如, 对于 ArrayList<Pair<String>> , 由于虚拟机有一个单独的 “原始” Pair 类, 所以,无法阻止插入 Pair<Date>

关于可选操作的说明

通常, 视图有一些局限性, 即可能只可以读、 无法改变大小、只支持删除而不支持插入,这些与映射的键视图情况相同。如果试图进行不恰当的操作,受限制的视图就会抛出一个 UnsupportedOperationException

在集合和迭代器接口的 API 文档中, 许多方法描述为“可选操作”。这看起来与接口的概念有所抵触。 一个更好的解决方案是为每个只读视图和不能改变集合大小的视图建立各自独立的两个接口。不过, 这将会使接口的数量成倍增长,这让类库设计者无法接受。

算 法

泛型集合接口有一个很大的优点, 即算法只需要实现一次。

标准的 C++ 类库已经有几十种非常有用的算法, 每个算法都是在泛型集合上操作的。Java 类库中的算法没有如此丰富, 但是,也包含了基本的排序、二分查找等实用算法。

排序与混排

Collections 类中的 sort 方法可以对实现了 List 接口的集合进行排序。

这个方法假定列表元素实现了 Comparable 接口。如果想采用其他方式对列表进行排序,可以使用 List 接口的 sort 方法并传入一个 Comparator 对象。

如果想按照降序对列表进行排序, 可以使用一种非常方便的静态方法 Collections.reverseOrder()

Java 程序设计语言对 sort 方法所采用的排序手段:直接将所有元素传入一个数组, 对数组进行排序,然后,再将排序后的序列复制回列表。

集合类库中使用的排序算法比快速排序要慢一些, 快速排序是通用排序算法的传统选择。但是, 归并排序 有一个主要的优点:稳定, 即不需要交换相同的元素。

Arrays#sort(T[], java.util.Comparator<? super T>) 方法使用了3种排序算法:

方法 描述
java.util.Arrays#legacyMergeSort 归并排序,但可能会在新版本中废弃
java.util.ComparableTimSort#sort 不使用自定义比较器的 TimSort
java.util.TimSort#sort 使用自定义比较器的 TimSort

Timsort 是一种混合、稳定高效的排序算法,源自归并排序和插入排序,旨在很好地处理多种真实数据。

因为集合不需要实现所有的 “可选” 方法,因此, 所有接受集合参数的方法必须描述什么时候可以安全地将集合传递给算法。例如,显然不能将 unmodifiableList 列表传递给排序算法。可以传递什么类型的列表呢? 根据文档说明,列表必须是可修改的, 但不必是可以改变大小的。

下面是有关的术语定义:

  • 如果列表支持 set 方法,则是 可修改
  • 如果列表支持 addremove 方法, 则是 可改变大小

Collections 类有一个算法 shuffle , 其功能与排序刚好相反, 即随机地混排列表中元素的顺序。

如果提供的列表没有实现 RandomAccess 接口,shuffle 方法将元素复制到数组中,然后打乱数组元素的顺序,最后再将打乱顺序后的元素复制回列表。 如果实现了 RandomAccess 接口,就随机交换元素位置。

java.util.Collections 方法名称 方法声明 描述
sort static <T extends Comparable<? super T>> void sort(List<T> list) 使用稳定的排序算法, 对列表中的元素进行排序。这个算法的时间复杂度是 O(n logn) , 其中 n 为列表的长度
shuffle static void shuffle(List<?> list)
static void shuffle(List<?> list, Random rnd)
随机地打乱列表中的元素。这个算法的时间复杂度是 O(n a(n)) , n 是列表的长度,a (n) 是访问元素的平均时间
reverse static void reverse(List<?> list) 逆置列表中元素的顺序
reverseOrder static <T> Comparator<T> reverseOrder()
static <T> Comparator<T> reverseOrder(Comparator<T> cmp)
返回一个比较器,它在实现 Comparable 接口的对象集合上强加自然顺序的反向。 (自然排序是由对象自己的 compareTo 方法强加的排序)
java.util.List<E> 方法名称 方法声明 描述
sort default void sort(Comparator<? super E> c) 使用给定比较器对列表排序
java.util.Comparator<T> 方法名称 方法声明 描述
reversed default Comparator<T> reversed() 生成一个比较器, 将逆置这个比较器提供的顺序
reverseOrder static <T extends Comparable<? super T>> Comparator<T> reverseOrder() 生成一个比较器, 将逆置 Comparable 接口提供的顺序

二分查找

Collections 类的 binarySearch 方法实现了这个算法。 注意, 集合必须是排好序的, 否则算法将返回错误的答案。要想查找某个元素, 必须提供集合(这个集合要实现 List 接口)以及要查找的元素。 如果集合没有采用 Comparable 接口的 compareTo 方法进行排序, 就还要提供一个比较器对象。

如果 binarySearch 方法返回的数值大于等于 0, 则表示匹配对象的索引。也就是说,c.get(i) 等于在这个比较顺序下的 element 。 如果返回负值, 则表示没有匹配的兀素。但是,可以利用返回值计算应该将 element 插入到集合的哪个位置, 以保持集合的有序性。插入的位置是

if (i < 0) {
	c.add(-i - 1, element);
}

只有采用随机访问,二分査找才有意义。如果必须利用迭代方式一次次地遍历链表的一半元素来找到中间位置的元素,二分査找就完全失去了优势。因此, 如果为 binarySearch 算法提供一个链表, 它将自动地变为线性查找。

java.util.Collections 方法名称 方法声明 描述
binarySearch static <T> int binarySearch(List<? extends Comparable<? super T>> list, T key)
static <T> int binarySearch(List<? extends T> list, T key, Comparator<? super T> c)
从有序列表中搜索一个键, 如果元素扩展了 AbstractSequentialList 类, 则采用线性查找,否则将采用二分查找。
这个方法的时间复杂度为 O(a(n) logn) , n 是列表的长度,a(n) 是访问一个元素的平均时间。这个方法将返回这个键在列表中的索引, 如果在列表中不存在这个键将返回负值 i ,在这种情况下,应该将这个键插入到列表索引 -i-1 的位置上, 以保持列表的有序性
List<Integer> list = IntStream.range(1, 10).boxed().collect(Collectors.toList());
list.retainAll(Arrays.asList(1, 3, 4, 6, 8, 10, 15));
System.out.println(list);   // [1, 3, 4, 6, 8]

Collections.sort(list);

int key = 5;
int i = Collections.binarySearch(list, key);
System.out.println(i);      // -4

// 如果不存在,插入 key,并保证依然有序
if (i < 0) {
    list.add(-i - 1, key);
}

System.out.println(list);       // [1, 3, 4, 5, 6, 8]

简单算法

java.util.Collections 方法名称 方法声明 描述
min
max
static <T extends Object & Comparable<? super T>> T min(Collection<? extends T> coll)
static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll)
static <T> T min(Collection<? extends T> coll, Comparator<? super T> comp)
static <T> T max(Collection<? extends T> coll, Comparator<? super T> comp)
返回集合中最小的或最大的元素
copy static <T> void copy(List<? super T> dest, List<? extends T> src) 将原列表中的所有元素复制到目辱列表的相应位置上。目标列表的长度至少与原列表一样
fill static <T> void fill(List<? super T> list, T obj) 将列表中所有位置设置为相同的值
addAll static <T> boolean addAll(Collection<? super T> c, T... elements) 将所有的值添加到集合中。 如果集合改变了, 则返回 true
replaceAll static <T> boolean replaceAll(List<T> list, T oldVal, T newVal) newVal 取代所有值为 oldVal 的元素
indexOfSubList
lastIndexOfSubList
static int indexOfSubList(List<?> source, List<?> target)
static int lastIndexOfSubList(List<?> source, List<?> target)
返回 source 中第一个或最后一个等于 target 子列表的索引。如果 source 中不存在等于 target 的子列表, 则返回 -1 。例如, source[s, t, a, r] , target[t, a, r] , 两个方法都将返回索引 1
swap static void swap(List<?> list, int i, int j)
static void swap(Object[] arr, int i, int j)
交换给定偏移量的两个元素
rotate static void rotate(List<?> list, int distance) 旋转列表中的元素, 将索引 i 的条目移动到位置 (i + distance) % list.size() 例如, 将列表 [t, a, r] 旋转 2 个位置后得到 [a, r, t]
frequency static int frequency(Collection<?> c, Object o) 返回 c 中与对象 o 相同的元素个数
disjoint static boolean disjoint(Collection<?> c1, Collection<?> c2) 如果两个集合没有共同的元素, 则返回 true

批操作

coll1.removeAll(coll2);

将从 coll1 中删除 coll2 中出现的所有元素。与之相反,

coll1.retainAll(coll2);

会从 coll1 中删除所有未在 coll2 中出现的元素。

找出两个集的交集 ( intersection) ,也就是两个集中共有的元素

Set<String> result = new HashSet<>(a);
result.retainAll(b);

假设有一个映射, 将员工 ID 映射到员工对象, 而且建立了一个将不再聘用的所有员工的 ID。 直接建立一个键集,并删除终止聘用关系的所有员工的 ID。 由于键集是映射的一个视图,所以键和相关联的员工名会自动从映射中删除。 通过使用一个子范围视图,可以把批操作限制在子列表和子集上。

Map<String, Employee> staffMap = ...;
Set<String> terainatedlDs = ...;
staffMap.keySet().removeAll(terainatedlDs);

假设希望把一个列表的前 10 个元素增加到另一个容器,可以建立一个子列表选出前 10 个元素:

relocated.addAll(staff.subList(0, 10)):

这个子范围还可以完成更改操作:

staff.subList(0, 10).clear();

集合与数组的转换

由于 Java 平台 API 的大部分内容都是在集合框架创建之前设计的, 所以,有时候需要在传统的数组和比较现代的集合之间进行转换。

如果需要把一个数组转换为集合,Arrays.asList 包装器可以达到这个目的。

String[] values = ... ;
HashSet<String> staff = new HashSet<>(Arrays.asList(values));

从集合得到数组可以使用 toArray 方法:

Object[] values = staff.toArray();

toArray 方法返回的数组是一个 Object[] 数组, 不能改变它的类型。实际上, 必须使用 toArray 方法的一个变体形式,提供一个所需类型而且长度为 0 的数组。这样一来, 返回的数组就会创建为相同的数组类型:

String[] values = staff.toArray(new String[0]);

如果愿意,可以构造一个指定大小的数组:

staff.toArray(new String[staff.size()]);

在这种情况下,不会创建新数组。

注释: 你可能奇怪为什么不能直接将一个 Class 对象(如 String.class ) 传递到 toArray 方法。 原因是这个方法有 “双重职责”, 不仅要填充一个已有的数组(如果它足够长) ,还要创建一个新数组。

编写自己的算法

如果编写自己的算法(实际上,是以集合作为参数的任何方法) ,应该尽可能地使用接口,而不要使用具体的实现。 例如,使用 Collection 接口作为参数,而不是 ArrayList

Java 类库中仍然存在一些没有将集合接口作为参数的方法,这是因为 时间问题 ,有些代码是在集合类库之前创建的

遗留的集合

从 Java 第 1 版问世以来, 在集合框架出现之前已经存在大量 “遗留的” 容器类。这些类已经集成到集合框架中

img

Hashtable

Hashtable 类与 HashMap 类的作用一样,实际上,它们拥有相同的接口。Hashtable 的方法也是同步的。如果对同步性或与遗留代码的兼容性没有任何要求,就应该使用 HashMap 。如果需要并发访问, 则要使用 ConcurrentHashMap

Enumeration

遗留集合使用 Enumeration 接口对元素序列进行遍历。

java.util.Enumeration<E> 方法名称 方法声明 描述
hasMoreElements boolean hasMoreElements(); 如果还有更多的元素可以査看, 则返回 true
nextElement E nextElement(); 返回被检测的下一个元素。 如果 hasMoreElements() 返回 false , 则不要调用这个方法
java.util.Hashtable<K,V> 方法名称 方法声明 描述
keys synchronized Enumeration<K> keys() 返回一个遍历散列表中键的枚举对象
elements synchronized Enumeration<V> elements() 返回一个遍历散列表中元素的枚举对象
java.util.Vector<E> 方法名称 方法声明 描述
elements Enumeration<E> elements() 返回遍历向量中元素的枚举对象

属性映射( Properties

属性映射(property map) 是一个类型非常特殊的映射结构。它有下面 3 个特性:

  • 键与值都是字符串
  • 表可以保存到一个文件中, 也可以从文件中加载
  • 使用一个默认的辅助表

Properties 继承了 Hashtable<Object,Object>

java.util.Properties 方法名称 方法声明 描述
构造器 Properties() 创建一个空的属性映射
构造器 Properties(Properties defaults) 创建一个带有一组默认值的空的属性映射
getProperty String getProperty(String key) 获得属性的对应关系;返回与键对应的字符串。 如果在映射中不存在, 返回默认表中与这个键对应的字符串
getProperty String getProperty(String key, String defaultValue) 获得在键没有找到时具有的默认值属性;它将返回与键对应的字符串,如果在映射中不存在,就返回 defaultValue
load synchronized void load(InputStream inStream) throws IOException InputStream 加载属性映射
store void store(OutputStream out, String comments) throws IOException 把属性映射存储到 OutputStream

栈( Stack

从 1.0 版开始,标准类库中就包含了 Stack 类 ,其中有大家熟悉的 push 方法和 pop方法。

Stack 类继承自 Vector 类,, 从理论角度看, Vector 类并不太令人满意,它可以让栈使用不属于栈操作的 insertremove 方法, 即可以在任何地方进行插入或删除操作,而不仅仅是在栈顶。

java.util.Stack<E> 方法名称 方法声明 描述
push E push(E item) item 压人桟并返回 item
pop synchronized E pop() 弹出并返回栈顶的 item 。 如果栈为空,请不要调用这个方法
peek synchronized E peek() 返回栈顶元素,但不弹出。 如果栈为空, 请不要调用这个方法

位集( BitSet

BitSet 类用于存放一个位序列(它不是数学上的集,称为位向量或位数组更为合适)。如果需要高效地存储位序列(例如,标志)就可以使用位集。由于位集将位包装在字节里, 所以,使用位集要比使用 Boolean 对象的 ArrayList 更加高效。

BitSet 类提供了一个便于读取、设置或清除各个位的接口。

例如,对于一个名为 bucketOfBitsBitSet ,

bucketOfBits.get(i)

如果第 i 位处于 “开” 状态,就返回 true ; 否则返回 false 。同样地,

bucketOfBits.set(i)

将第 i 位置为 “开” 状态。最后,

bucketOfBits.clear(i)

将第 i 位置为 “关” 状态。

java.util.BitSet 方法名称 方法声明 描述
构造器 BitSet(int nbits) 创建一个位集
length int length() 返回位集的“ 逻辑长度”, 即 1 加上位集的最高设置位的索引
get boolean get(int bitIndex)
BitSet get(int fromIndex, int toIndex)
获得位
set void set(int bitIndex)
void set(int bitIndex, boolean value)
void set(int fromIndex, int toIndex)
void set(int fromIndex, int toIndex, boolean value)
设置位
clear void clear()
void clear(int bitIndex)
void clear(int fromIndex, int toIndex)
清除一个位
and void and(BitSet set) 这个位集与另一个位集进行逻辑 “AND”
or void or(BitSet set) 这个位集与另一个位集进行逻辑 “OR”
xor void xor(BitSet set) 异或,这个位集与另一个位集进行逻辑 “X0R”
andNot void andNot(BitSet set) 清除这个位集中对应另一个位集中设置的所有位
BitSet bitSet1 = new BitSet(10);
BitSet bitSet2 = new BitSet(10);
System.out.println(bitSet1);    // {}
for (int i = 0; i < 10; i++) {
    bitSet1.set(i);
    bitSet2.set(i);
}
System.out.println(bitSet1);    // {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

bitSet1.clear(1);
bitSet1.clear(3);
bitSet1.clear(7);

System.out.println(bitSet1);    // {0, 2, 4, 5, 6, 8, 9}

bitSet2.andNot(bitSet1);
System.out.println(bitSet2);    // {1, 3, 7}
posted @ 2022-04-24 21:16  流星<。)#)))≦  阅读(9)  评论(0编辑  收藏  举报