20220424 Java核心技术 卷1 基础知识 9
集 合
Java 集合框架
Java 最初版本只为最常用的数据结构提供了很少的一组类:Vector
、 Stack
、 Hashtable
、BitSet
与 Enumeration
接口, 其中的 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 -> {
//
});
元素被访问的顺序取决于集合类型。 对 ArrayList
和 HashSet
的迭代顺序不同。
Iterator
接口的 next
和 hasNext
方法与 Enumeration
接口的 nextElement
和 hasMoreElements
方法的作用一样。
Java 集合类库中的迭代器查找操作与位置变更是紧密相连的。查找一个元素的唯一方法是调用 next
, 而在执行查找操作的同时, 迭代器的位置随之向前移动
因此,应该将 Java 迭代器认为是位于两个元素之间。 当调用 next
时,迭代器就越过下一个元素,并返回刚刚越过的那个元素的引用
这里还有一个有用的推论。 可以将 Iterator.next
与 InputStream.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 集合框架为不同类型的集合定义了大量接口
集合有两个基本接口:Collection
和 Map
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
接口可以让程序员编写只接受集的方法。
SortedSet
和 SortedMap
接口会提供用于排序的比较器对象,这两个接口定义了可以得到集合子集视图的方法。
最后,Java SE 6 引人了接口 NavigableSet
和 NavigableMap
, 其中包含一些用于搜索和遍历有序集和映射的方法。(理想情况下,这些方法本应当直接包含在 SortedSet
和 SortedMap
接口中)。TreeSet
和 TreeMap
类实现了这些接口
具体的集合类
Java 库中的具体集合
集合类型 | 描 述 |
---|---|
ArrayList |
一种可以动态增长和缩减的索引序列 |
LinkedList |
一种可以在任何位置进行高效地插入和删除操作的有序序列 |
ArrayDeque |
一种用循环数组实现的双端队列 |
HashSet |
一种没有重复元素的无序集合 |
TreeSet |
—种有序集 |
EnumSet |
一种包含枚举类型值的集 |
LinkedHashSet |
一种可以记住元素插入次序的集 |
PriorityQueue |
一种允许高效删除最小元素的集合 |
HashMap |
一种存储键 / 值关联的数据结构 |
TreeMap |
—种键值有序排列的映射表 |
EnumMap |
一种键值属于枚举类型的映射表 |
LinkedHashMap |
一种可以记住键 / 值项添加次序的映射表 |
WeakHashMap |
一种其值无用武之地后可以被垃圾回收器回收的映射表 |
IdentityHashMap |
一种用 == 而不是用 equals 比较键值的映射表 |
Collection
接口实现类:
Map
接口实现类:
链表( 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
就会将右侧的元素删除掉, 并且不能连续调用两次 remove
。add
方法只依赖于迭代器的位置, 而 remove
方法依赖于迭代器的状态。
set
方法用一个新元素取代调用 next
或 previous
方法返回的上一个元素。
如果迭代器发现它的集合被另一个迭代器修改了, 或是被该集合自身的方法修改了, 就会抛出一个 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); |
用新元素取代 next 或 previous 上次访问的元素。 如果在 next 或 previous 上次调用之后列表结构被修改了, 将拋出一个 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
接口用于描述一个有序集合,并且集合中每个元素的位置十分重要。 有两种访问元素的协议:一种是用迭代器, 另一种是用 get
和 set
方法随机地访问每个元素。后者不适用于链表, 但对数组却很有用。 集合类库提供了一种大家熟悉的 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(); |
返回这个对象的散列码。散列码可以是任何整数, 包括正数或负数。equals 和 hashCode 的定义必须兼容,即如果 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("===========================");
队列与双端队列( Queue
、Deque
)
队列可以让人们有效地在尾部添加一个元素, 在头部删除一个元素。有两个端头的队列, 即双端队列, 可以让人们有效地在头部和尾部同时添加或删除元素。不支持在队列中间添加元素。在 Java SE 6 中引人了 Deque
接口, 并由 ArrayDeque
和 LinkedList
类实现。这两个类都提供了双端队列,而且在必要时可以增加队列的长度。
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 类库为映射提供了两个通用的实现:HashMap
和 TreeMap
。这两个类都实现了 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
不是 HashSet
或 TreeSet
, 而是实现了 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 的原因可能与字符串常量池有关。
链接散列集与映射 ( LinkedHashSet
、LinkedHashMap
)
LinkedHashSet
和 LinkedHashMap
类用来记住插入元素项的顺序。这样就可以避免在散列表中的项从表面上看是随机排列的。当条目插入到表中时,就会并入到双向链表中
LinkedHashMap
遍历时,默认使用的是 插入顺序 ,通过配置 accessOrder
可以修改为使用 访问顺序 ,最近访问的会被放在最后遍历。
accessOrder
参数为 true
时表示访问顺序, 为 false
时表示插入顺序。
Map<String, String> linkedHashMap = new LinkedHashMap<>(16, 0.75f, true);
每次调用 get
或 put
, 受到影响的条目将从当前的位置删除, 并放到条目链表的尾部(只有条目在链表中的位置会受影响, 而散列表中的桶不会受影响。一个条目总位于与键散列码对应的桶中)
访问顺序对于实现高速缓存的 “ 最近最少使用 ” 原则十分重要。例如, 可能希望将访问频率高的元素放在内存中, 而访问频率低的元素则从数据库中读取。当在表中找不到元素项且表又已经满时, 可以将迭代器加入到表中, 并将枚举的前几个元素删除掉。这些是近期最
少使用的几个元素。
甚至可以让这一过程自动化。即构造一个 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}
枚举集与映射( EmimSet
、EnumMap
)
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
接口的类对象, 这个类的方法对原映射进行操作。这种集合称为 视图 。
视图技术在集框架中有许多非常有用的应用。
轻量级集合包装器( Arrays
、Collections
)
Arrays 类的静态方法 asList
将返回一个包装了普通 Java 数组的 List
包装器。这个方法可以将数组传递给一个期望得到列表或集合参数的方法。 返回的对象不是 ArrayList
,而是静态内部类 java.util.Arrays.ArrayList
。它是一个视图对象, 带有访问底层数组的 get
和 set
方法。改变数组大小的所有方法(例如,与迭代器相关的 add
和 remove
方法)都会抛出一个 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
子范围( sub
、head
、tail
)
可以为很多集合建立 子范围(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.unmodifiableList
与 ArrayList
、 LinkedList
或者任何实现了 List
接口的其他类一起协同工作。
例如, 假设想要查看某部分代码, 但又不触及某个集合的内容, 就可以进行下列操作:
List<String> staff = new LinkedList<>();
// ...
lookAt(Collections.unmodifiableList(staff));
Collections.unmodifiableList
方法将返回一个实现 List
接口的类对象。 其访问器方法将从 staff 集合中获取值。当然,lookAt 方法可以调用 List
接口中的所有方法, 而不只是访问器。但是所有的更改器方法(例如, add) 已经被重新定义为抛出一个 UnsupportedOperationException
异常,而不是将调用传递给底层集合。
不可修改视图并不是集合本身不可修改。仍然可以通过集合的原始引用(在这里是 staff ) 对集合进行修改。并且仍然可以让集合的元素调用更改器方法。
由于视图只是包装了接口而不是实际的集合对象, 所以只能访问接口中定义的方法。例如, LinkedList
类有一些非常方便的方法,addFirst
和 addLast
,它们都不是 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
方法,则是 可修改 的 - 如果列表支持
add
和remove
方法, 则是 可改变大小 的
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 版问世以来, 在集合框架出现之前已经存在大量 “遗留的” 容器类。这些类已经集成到集合框架中
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
类并不太令人满意,它可以让栈使用不属于栈操作的 insert
和 remove
方法, 即可以在任何地方进行插入或删除操作,而不仅仅是在栈顶。
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 类提供了一个便于读取、设置或清除各个位的接口。
例如,对于一个名为 bucketOfBits
的 BitSet
,
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}