第9章 集合
9.1 Java 集合框架
Java 最初版本数据结构
- Java 最初版本只为最常用的数据结构提供了很少的一组类:Vector、 Stack、HashtableBitSet 与 Enumeration 接口。
9.1.1 将集合的接口与实现分离
队列 (queue)
// a simplified form of the interface in the standard library
public interface Queue<E> {
void add(E element) ;
E remove();
int size();
}
//这个接口并没有说明队列是如何实现的。队列通常有两种实现方式:一种是使用循环数组;另一种是使用链表。
- 队列接口指出可以在队列的尾部添加元素,在队列的头部删除元素,并且可以査找队列中元素的个数。
- 当需要收集对象,并按照“ 先进先出” 的规则检索对象时就应该使用队列。
队列的使用
- 如果需要一个循环数组队列,就可以使用 ArrayDeque 类。如果需要一个链表队列, 就直接使用 LinkedList类,这个类实现了 Queue 接口。
- 当在程序中使用队列时,一旦构建了集合就不需要知道究竟使用了哪种实现。因此,只有在构建集合对象时,使用具体的类才有意义。
9.1.2 Collection 接口
Collection 接口
public interface Collection<b
{
//add方法用于向集合中添加元素。如果添加元素确实改变了集合就返回 true, 如果集合没有发生变化就返回 false。
boolean add(E element);
//iterator方法用于返回一个实现了 Iterator 接口的对象。可以使用这个迭代器对象依次访问集合中的元素。
Iterator<E> iterator();
}
- 在 Java 类库中,集合类的基本接口是 Collection 接口。
- 这个接口有两个基本方法:add和iterator。
9.1.3 迭代器
Iterator 接口
public interface Iterator<E>
{
E next();
boolean hasNext();
void remove();
default void forEachRemaining(Consumer<? super E> action);
}
next方法
- 通过反复调用 next 方法,可以逐个访问集合中的每个元素。
- 但是,如果到达了集合的末尾,next 方法将抛出一个 NoSuchElementException。因此,需要在调用 next 之前调用 hasNext方法。
- 如果想要査看集合中的所有元素,就请求一个迭代器,并在 hasNext 返回 true 时反复地调用 next 方法。
Collection<String> c = . . .;
Iterator<String> iter = c.iterator();
while (iter.hasNext()) {
String element = iter.next();
do something with element
}
关于for each循环
for (String element : c) {
do something with element
}
- 编译器简单地将“ foreach” 循环翻译为带有迭代器的循环。
- ”for each”循环可以与任何实现了 Iterable 接口的对象一起工作。
Iterable 接口
public interface Iterable<E> {
Iterator<E> iterator();
}
- Collection 接口扩展了 Iterable 接口。因此,对于标准类库中的任何集合都可以使用“ foreach” 循环。
- 在 Java SE 8中,可以调用 forEachRemaining 方法并提供一 lambda
表达式(它会处理一个元素)。
iterator.forEachRemaining(element -> do something with element);
元素访问顺序
- 元素被访问的顺序取决于集合类型。
- 虽然可以确定在迭代过程中能够遍历到集合中的所有元素,但却无法预知元素被访问的次序。
如果对 ArrayList 进行迭代,迭代器将从索引 0开始,每迭代一次,索引值加工。
如果访问 HashSet 中的元素,每个元素将会按照某种随机的次序出现。
Iterator 接口和Enumeration 接口
- Iterator 接口的 next 和 hasNext 方法与 Enumeration 接口的nextElement 和 hasMoreElements 方法的作用一样。
- Java 集合类库的设计者可以选择使用Enumeration 接口。但是,他们不喜欢这个接口累赘的方法名,于是引入了具有较短方法名的新接口。
Java迭代器位置
- 查找一个元素的唯一方法是调用 next,而在执行查找操作的同时,迭代器的位置随之向前移动。
- 应该将 Java 迭代器认为是位于两个元素之间。当调用 next 时,迭代器就越过下一个元素,并返回刚刚越过的那个元素的引用。
remove 方法
Iterator<String> it = c.iterator();
it.next(); // skip over the first element
it.remove(); // now remove it
- Iterator 接口的 remove 方法将会删除上次调用 next 方法时返回的元素。
- 更重要的是,对 next 方法和 remove 方法的调用具有互相依赖性。如果调用 remove 之前没有调用next 将是不合法的。 ( IllegalStateException 异常)
it.remove();
it.remove();// Error!
//
it.remove() ;
it.next();
it.remove(); // OK
9.1.4 泛型实用方法
Collection实用方法
int size()
boolean isEmpty()
boolean contains (Object obj)
boolean containsAll (Col1ection<?> c)
boolean equals (Object other)
boolean addAll (Collection<? extends E> from)
boolean remove(Object obj)
boolean removeAll (Col1ection<?> c)
void clear ()
boolean retainAll (Col1ection<?> c)
Object口 toArray();
<T> T[] toArray(T[] arrayToFill )
- Collection 接口声明了很多有用的方法,所有的实现类都必须提供这些方法
- 为了能够让实现者更容易地实现这个接口,Java 类库提供了一个类 AbstractCollection,它只将 size 和 iterator 方法抽象化了,提供了其他例行方法。
在Java8的使用
- 如果这些方法是 Collection 接口的默认方法会更好。但实际上并不是这样。
- 有一个很有用的默认方法,可以用于删除满足某个条件的元素。
default boolean removelf (Predicate<? super E> filter)
9.1.5 集合框架中的接口
Java 集合框架
- 集合有两个基本接口:Collection 和 Map。
List集合
- List 是一个有序集合(ordered collection)。元素会增加到容器中的特定位置。
- 以采用两种方式访问元素:使用迭代器访问,或者使用一个整数索引来访问。
随机访问
//List 接口定义了多个用于随机访问的方法:
void add(int index, E element)
void remove(int index)
E get(int index)
E set(int index, E element)
- 使用一个整数索引来访问的方法称为随机访问 (random access)。因为这样可以按任意顺序访问元素。
- 使用迭代器访问时,必须顺序地访问元素。
Java SE 1.4 引入了一个标记接口 RandomAccess。这个接口不包含任何方法, 不过可以用它来测试一个特定的集合是否支持高效的随机访问:if (c instanceof RandomAccess)。
Listlterator 接口
- Listlterator 接口是 Iterator 的一个子接口。它定义了一个方法用于在迭代器位置前面增加一个元素:
void add(E element)
Set 接口
- Set 接口等同于 Collection 接口,不过其方法的行为有更严谨的定义。集(set) 的 add方法不允许增加重复的元素。
- SortedSet 和 SortedMap 接口会提供用于排序的比较器对象。
- Java SE 6 引人了接口 NavigableSet 和 NavigableMap, 其中包含一些用于搜索和遍历有序集和映射的方法。
Set接口的使用
- 要适当地定义集的 equals 方法:只要两个集包含同样的元素就认为是相等的,而不要求这些元素有同样的顺序。
- hashCode 方法的定义要保证包含相同元素的两个集会得到相同的散列码。
为什么要单独定义Set
- 既然方法签名是一样的,为什么还要建立一个单独的接口呢?
- 从概念上讲,并不是所有集合都是集。建立一个 Set 接口可以让程序员编写只接受集的方法。
9.2 具体的集合
Java 库中的具体集合
集合框架中的类
9.2.1 链 表
数组和数组列表的缺陷
- 从数组的中间位置删除一个元素要付出很大的代价,其原因是数组中处于被删除元素之后的所有元素都要向数组的前端移动。 在数组中间的位置上插入一个元素也是如此。
链表
- 数组在连续的存储位置上存放对象引用, 但链表却将每个对象存放在独立的结点中。每个结点还存放着序列中下一个结点的引用。
- 在 Java 程序设计语言中,所有链表实际上都是双向链接的(doubly linked)—即每个结点还存放着指向前驱结点的引用。
链表的add方法
- LinkedList.add 方法将对象添加到链表的尾部。但是,常常需要将元素添加到链表的中间。由于迭代器是描述集合中位置的,所以这种依赖于位置的 add 方法将由迭代器负责。
- 只有对自然有序的集合使用迭代器添加元素才有实际意义。例如, 下一节将要讨论的集 (set ) 类型,其中的元素完全无序。 因此,在 Iterator 接口中就没有add 方法。相反地,集合类库提供了子接口 Listlterator, 其中包含 add 方法:void add(E element);
Listlterator接口
- 与 Collection.add 不同,这个方法不返回 boolean 类型的值,它假定添加操作总会改变链表。
- Listlterator 接口有两个方法, 可以用来反向遍历链表。
E previous()
boolean hasPrevious()
- LinkedList 类的 listlterator 方法返回一个实现了 Listlterator 接口的迭代器对象。
ListIterator<String> iter = staff.listlterator();//LinkedList 的方法
//ListIterator 类是其一个内部类,获得外部数据并操作
//lterator 方法是从Iteratable 继承来的
- 如果多次调用 add 方法,将按照提供的次序把元素添加到链表中。
- Add 方法在迭代器位置之前添加一个新对象。
当用一个刚刚由 Iterator 方法返回,并且指向链表表头的迭代器调用 add 操作时,新添加的元素将变成列表的新表头。
当迭代器越过链表的最后一个元素时(即 hasNext 返回 false),添加的元素将变成列表的新表尾。
- add 方法只依赖于迭代器的位置,而 remove 方法依赖于迭代器的状态。
remove 操作与 BACKSPACE 键的工作方式不太一样。在调用 next 之后,remove 方法确实与 BACKSPACE 键一样删除了迭代器左侧的元素。但是, 如果调用 previous 就会将右侧的元素删除掉, 并且不能连续调用两次remove()。
- set 方法用一个新元素取代调用 next 或 previous 方法返回的上一个元素。
迭代器冲突
- 如果在某个迭代器修改集合时,另一个迭代器对其进行遍历,一定会出现混乱的状况。
- 链表迭代器的设计使它能够检测到这种修改。如果迭代器发现它的集合被另一个迭代器修改了,或是被该集合自身的方法修改了,就会抛出一个ConcurrentModificationException 异常。
解决的简单规则
- 可以根据需要给容器附加许多的迭代器,但是这些迭代器只能读取列表。另外,再单独附加一个既能读又能写的迭代器。
检测到并发修改的问题
- 集合可以跟踪改写操作 (诸如添加或删除元素)的次数。每个迭代器都维护一个独立的计数值。
- 在每个迭代器方法的开始处检查自己改写操作的计数值是否与集合的改写操作计数值一致。如果不一致,抛出一个 ConcurrentModificationException 异常。
- 链表只负责跟踪对列表的结构性修改。
例如,添加元素、删除元素。set 方法不被视为结构性修改。可以将多个迭代器附加给一个链表,所有的迭代器都调用 set 方法对现有结点的内容进行修改。
链表争议的方法
- 链表不支持快速地随机访问。
如果要查看链表中第 n 个元素,就必须从头开始,越过个元素。没有捷径可走。
- 在程序需要采用整数索引访问元素时,程序员通常不选用链表。
链表get方法
- LinkedList 类还是提供了一个用来访问某个特定元素的 get 方法:
LinkedList<String> list = ...;
String obj = list.get(n);
- 当然,这个方法的效率并不太高。
//绝对不应该使用这种让人误解的随机访问方法来遍历链表。下面这段代码的效率极低:
for (int i = 0; i < list.sizeO;i++)
do something with list.get(i);
//每次査找一个元素都要从列表的头部重新开始搜索。LinkedList 对象根本不做任何缓存
位置信息的操作。
获得当前索引
- 列表迭代器接口还有一个方法,可以告之当前位置的索引。
- 由于Java 迭代器指向两个元素之间的位置, 所以可以同时产生两个索引。(nextlndex和previouslndex)。
- 如果有一个整数索引n,list.listlterator(n) 将返回一个迭代器, 这个迭代器指向索引为 n 的元素前面的位置。
也就是说,调用 next 与调用 Hst.get(n) 会产生同一个元素,只是获得这个迭代器的效率比较低。
为什么要优先使用链表呢?
- 使用链表的唯一理由是尽可能地减少在列表中间插人或删除元素所付出的代价。
- 如果列表只有少数几个元素,就完全可以使用 ArrayList。
9.2.2 数组列表
两种访问元素的协议
- 一种是用迭代器,另一种是用 get 和 set 方法随机地访问每个元素。
- 后者不适用于链表,但对数组却很有用。
ArrayList 特性
- ArrayList 封装了一个动态再分配的对象数组。
**ArrayList 取代 Vector **
- 原因很简单:Vector 类的所有方法都是同步的。
- 而 ArrayList 方法不是同步的,因此,建议在不需要同步时使用 ArrayList, 而不要使用 Vector。
9.2.3 散列集
使用目的
- List 中查看不知位置的元素时,要遍历查找,元素多时效率低。
- 如果不在意元素的顺序,可以按照有利于其操作目的的原则组织数据。
散列表
- 散列表 ( hash table ) 是一种可以快速地査找所需要的对象的数据结构。
- 散列表为每个对象计算一个整数,称为散列码(hashcode)。散列码是由对象的实例域产生的一个整数。
最重要的问题
- 最重要的问题是散列码要能够快速地计算出来,并且这个计算只与要散列的对象状态有关,与散列表中的其他对象无关。
Java中计算方法
- 在 Java 中, 散列表用链表数组实现。每个列表被称为桶 (bucket)。
- 要想査找表中对象的位置,就要先计算它的散列码,然后与桶的总
数取余,所得到的结果就是保存这个元素的桶的索引。
例如,如果某个对象的散列码为 76268, 并且有 128 个桶,对象应该保存在第 108 号桶中(76268除以 128余 108 )。 或许会很幸运,在这个桶中没有其他元素,此时将元素直接插人到桶中就可以了。
散列冲突
- 有时候会遇到桶被占满的情况,这也是不可避免的。这种现象被称为散列冲突(hashcollision) 。
- 这时,需要用新对象与桶中的所有对象进行比较,査看这个对象是否已经存在。如果散列码是合理且随机分布的,桶的数目也足够大,需要比较的次数就会很少。
在 JavaSE 8 中,桶满时会从链表变为平衡二叉树。
初始的桶数
- 如果想更多地控制散列表的运行性能,就要指定一个初始的桶数。
- 桶数是指用于收集具有相同散列值的桶的数目。
桶数设置
- 有些研究人员认为:尽管还没有确凿的证据,但最好将桶数设置为一个素数,以防键的集聚。
- 标准类库使用的桶数是 2 的幂,默认值为 16 (为表大小提供的任何值都将被自动地转换为 2 的下一个幂)。
再散列
- 并不是总能够知道需要存储多少个元素的,也有可能最初的估计过低。
- 如果散列表太满,就需要再散列 (rehashed)。
装填因子
- 装填因子 (load factor) 决定何时对散列表进行再散列。
例如,如果装填因子为 0.75 (默认值,) 而表中超过 75%的位置已经填人元素,这个表就会用双倍的桶数自动地进行再散列。
- 对于大多数应用程序来说,装填因子为0.75 是比较合理的。
HashSet 类
- Java 集合类库提供了一个 HashSet 类,它实现了基于散列表的集。
- contains方法已经被重新定义,用来快速地查看是否某个元素已经出现在集中。它只在某个桶中査找元素,而不必查看集合中的所有元素。
- 散列集迭代器将依次访问所有的桶。由于散列将元素分散在表的各个位置上,所以访问它们的顺序几乎是随机的。只有不关心集合中元素的顺序时才应该使用 HashSet。
更改集中的元素
- 在更改集中的元素时要格外小心。如果元素的散列码发生了改变,元素在数据结构中的位置也会发生变化。
9.2.4 树集
树集
- 树集是一个有序集合 (sorted collection) 。
- 可以以任意顺序将元素插入到集合中。在对集合进行遍历时,每个值将自动地按照排序后的顺序呈现。
树结构
- 正如 TreeSet 类名所示,排序是用树结构完成的 (当前实现使用的是红黑树 (red-black tree) )。
- 每次将一个元素添加到树中时,都被放置在正确的排序位置上。因此,迭代器总是以排好序的顺序访问每个元素。
使用注意点
- 要使用树集,必须能够比较元素。
- 这些元素必须实现 Comparable 接口,或者构造集时必须提供一个 Comparator。
树集和散列集
- 如果不需要对数据进行排序,就没有必要付出排序的开销。
- 更重要的是,对于某些数据来说,对其排序要比散列函数更加困难。
- 散列函数只是将对象适当地打乱存放,而比较却要精确地判别每个对象。
NavigableSet 接口
- 从 JavaSE 6 起,TreeSet 类实现了 NavigableSet 接口。这个接口增加了几个便于定位元素以及反向遍历的方法。
9.2.5 队列与双端队列
双端队列
- 队列可以让人们有效地在尾部添加一个元素,在头部删除一个元素。
- 有两个端头的队列,即双端队列,可以让人们有效地在头部和尾部同时添加或删除元素。不支持在队列中间添加元素。
Deque 接口
- 在 Java SE 6 中引人了 Deque 接口,并由 ArrayDeque 和 LinkedList 类实现。
- 这两个类都提供了双端队列,而且在必要时可以增加队列的长度。
9.2.6 优先级队列
优先级队列
- 优先级队列 (priority queue) 中的元素可以按照任意的顺序插人,却总是按照排序的顺序进行检索。
也就是说,无论何时调用 remove 方法,总会获得当前优先级队列中最小的元素。
- 优先级队列并没有对所有的元素进行排序。
- 优先级队列使用了一个优雅且高效的数据结构,称为堆 (heap)。
- 一个优先级队列既可以保存实现了 Comparable 接口的类对象,也可以保存在构造器中提供的 Comparator 对象。
堆
- 堆是一个可以自我调整的二叉树,对树执行添加 (add) 和删除 (remore) 操作,可以让最小的元素移动到根,而不必花费时间对元素进行排序。
9.3 映 射
映射目的
- 通常,我们知道某些键的信息,并想要查找与之对应的元素。
9.3.1 基本映射操作
两个通用的实现
- Java 类库为映射提供了两个通用的实现:HashMap 和 TreeMap 。
散列映射
- 散列映射对键进行散列,树映射用键的整体顺序对元素进行排序,并将其组织成搜索树。
- 散列或比较函数只能作用于键。与键关联的值不能进行散列或比较。
选择散列映射还是树映射
- 与集一样,散列稍微快一些, 如果不需要按照排列顺序访问键,就最好选择散列。
检索对象
//要想检索一个对象,必须使用 (因而,必须记住)一个键。
String id = "987-98-9996";
e = staff.get(id);// gets harry
Map<String, Integer> scores = . .
int score = scores.get(id, 0); // Gets 0 if the id is not present
- 如果在映射中没有与给定键对应的信息,get 将返回 null。
- null 返回值可能并不方便。有时可以有一个好的默认值,用作为映射中不存在的键。然后使用 getOrDefault 方法。
键的使用
- 键必须是唯一的。不能对同一个键存放两个值。
- 如果对同一个键两次调用 put 方法, 第二个值就会取代第一个值。实际上,put 将返回用这个键参数存储的上一个值。
其他方法
- remove 方法用于从映射中删除给定键对应的元素。
- size 方法用于返回映射中的元素数。
- 要迭代处理映射的键和值,最容易的方法是使用 forEach 方法。可以提供一个接收键和值的 lambda 表达式。映射中的每一项会依序调用这个表达式。
scores.forEach((k, v) ->
System.out.println("key=" + k + ", valu e:" + v)) ;
9.3.2 更新映射项
更新映射项
//正常情况下,可以得到与一个键关联的原值,完成更新,再放回更新后的值。
counts.put (word, counts.get(word)+ 1);
//如果键第一次出现,可以使用 getOrDefault 方法
counts.putlfAbsent (word, 0);
//另一种方法是首先调用 putlfAbsent 方法。只有当键原先存在时才会放入一个值。
counts,put(word, counts.getOrDefault(word, 0)+ 1);
counts.put (word, counts.get(word)+ 1); // Now we know that get will succeed
//如果键原先不存在,下面的调用将把 word 与 1 关联,否则使用Integer::sum 函数组合原值和 1
counts.merge(word, 1, Integer::sum);
9.3.3 映射视图
映射和集合
- 集合框架不认为映射本身是一个集合。
其他数据结构框架认为映射是一个键 / 值对集合,或者是由键索引的值集合。
- 可以得到映射的视图 (View) —这是实现了Collection 接口或某个子接口的对象。
映射的视图
Set<K> keySet()
Collection<V> values0
Set<Map.Entry<K, V» entrySetO
- 有 3 种视图:键集、值集合 (不是一个集) 以及键 / 值对集。
- 条目集的元素是实现 Map.Entry 接口的类的对象。
keySet
- 需要说明的是,keySet 不是 HashSet 或 TreeSet,而是实现了 Set 接口的另外某个类的对象。Set 接口扩展了 Collection 接口。
- 因此,可以像使用集合一样使用 keySet。
同时查看键和值
for (Map.Entry<String, Employee> entry : staff.entrySet()) {
String k = entry.getKey();
Employee v = entry.getValue();
do something with k, v
}
- 如果想同时查看键和值,可以通过枚举条目来避免查找值。
- 原先这是访问所有映射条目的最高效的方法。如今,只需要使用 forEach 方法:
counts.forEach((k, v) -> {
dosomethingwith k, v
});
注意删除功能
- 如果在键集视图上调用迭代器的 remove 方法,实际上会从映射中删除这个键和与它关联的值。
- 不过,不能向键集视图增加元素。另外, 如果增加一个键而没有同时增加值也是没有意义的。
- 条目集视图有同样的限制,尽管理论上增加一个新的键 / 值对好像是有意义的。(实际视图是一个封装类,里面数据标记为final)
如果试图调用 add 方法, 它会抛出一个 UnsupportedOperationException。
9.3.4 弱散列映射
WeakHashMap 类
- 解决某个键的最后一次引用已经消亡,但是映射对象是活动的,其中的所有桶也是活动的,它们不能被回收的问题。
- 需要由程序负责从长期存活的映射表中删除那些无用的值。 或者使用 WeakHashMap 完成这件事情。
- 当对键的唯一引用来自散列条目时, 这一数据结构将与垃圾回收器协同工作一起删除键 / 值对。
弱引用
- WeakHashMap 使用弱引用 (weak references) 保存键。
- WeakReference 对象将引用保存到另外一个对象中,对于这种类型的对象,垃圾回收器用一种特有的方式进行处理。
- 如果某个对象只能由 WeakReference 引用,垃圾回收器仍然回收它,但要将引用这个对象的弱引用放人队列中。
WeakHashMap 将周期性地检查队列, 以便找出新添加的弱引用。一个弱引用进人队列意味着这个键不再被他人使用,并且已经被收集起来。于是,WeakHashMap 将删除对应的条目。
9.3.5 链接散列集与映射
记住插人元素项的顺序
- LinkedHashSet 和 LinkedHashMap类用来记住插人元素项的顺序。
- 储存散列元素之间有链表连接。
访问顺序
//构造散列映射表
LinkedHashMap<K, V>(initialCapacity, loadFactor, true)
- 链接散列映射将用访问顺序,而不是插入顺序,对映射条目进行迭代。
- 每次调用 get 或put, 受到影响的条目将从当前的位置删除,并放到条目链表的尾部(只有条目在链表中的位置会受影响,而散列表中的桶不会受影响。一个条目总位于与键散列码对应的桶中)。
应用
- 访问顺序对于实现高速缓存的“ 最近最少使用” 原则十分重要。
例如,可能希望将访问频率高的元素放在内存中, 而访问频率低的元素则从数据库中读取。当在表中找不到元素项且表又已经满时,可以将迭代器加入到表中,并将枚举的前几个元素删除掉。这些是近期最少使用的几个元素。
自动化移除老元素
- 构造一 LinkedHashMap 的子类,然后覆盖下面这个方法。每当方法返回 true 时,就添加一个新条目,从而导致删除 eldest 条目。
protected boolean removeEldestEntry(Map.Entry<K, V> eldest)
//举例,应该在表已经满了加载新对象时触发
Map<K, V> cache = new LinkedHashMapo(128, 0.75F, true) {
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > 100;
}
}();
- 另外,还可以对 eldest 条目进行评估,以此决定是否应该将它删除。例如,可以检査与这个条目一起存在的时间戳。
9.3.6 枚举集与映射
EnumSet
- EmimSet 是一个枚举类型元素集的高效实现。
- 由于枚举类型只有有限个实例,所以EnumSet 内部用位序列实现。如果对应的值在集中,则相应的位被置为 1。
构造
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);
- EnumSet 类没有公共的构造器。
- 可以使用静态工厂方法构造这个集。
EnumMap
- EnumMap 是一个键类型为枚举类型的映射。它可以直接且高效地用一个值数组实现。在使用时, 需要在构造器中指定键类型:
EnumMap<Weekday, Employee> personlnCharge = new EnumMap<>(Weekday.class)
E extends Enum<E>
- 在 EnumSet 的 API 文档中,将会看到 E extends Enum 这样奇怪的类型参数。
- 简单地说,它的意思是 “ E 是一个枚举类型。” 所有的枚举类型都扩展于泛型 Enum 类。
9.3.7 标识散列映射
类 IdentityHashMap
- 在这个类中,键的散列值不是用 hashCode 函数计算的,而是用 System.identityHashCode 方法计算的。(根据对象的内存地址来计算散列码)
- 而且,在对两个对象进行比较时,IdentityHashMap 类使用 ==, 而不使用 equals。
- 也就是说,不同的键对象,即使 (键存的) 内容相同,也被视为是不同的对象。
9.4 视图与包装器
视图
- 通过使用视图 ( views) 可以获得其他的实现了 Collection 接口和 Map 接口的对象。
- 初看起来,好像这个方法创建了一个新集, 并将映射中的所有键都填进去,然后返回这个集。
- 取而代之的是:keySet 方法返回一个实现 Set 接口的类对象,这个 类的方法对原映射进行操作。这种集合称为视图。
9.4.1 轻量级集合包装器
asList方法
Card[] cardOeck = new Card[52];
List<Card> cardList = Arrays.asList(cardDeck);
//asList 方法可以接收可变数目的参数
List<String> names = Arrays.asList("A«iy", "Bob", "Carl");
//这个方法调用
Col1ections.nCopies(n, anObject)
//将返回一个实现了 List 接口的不可修改的对象,并给人一种包含》个元素, 每个元素都像是一个 anObject 的错觉。
- Arrays 类的静态方法 asList 将返回一个包装了普通 Java 数组的 List 包装器。
- 返回的对象不是 ArrayList。它是一个视图对象,带有访问底层数组的 get 和 set 方法。
- 改变数组大小的所有方法(例如,与迭代器相关的 add 和 remove 方法)都会抛出一个 Unsupported OperationException 异常。
Collections 类
- Collections 类包含很多实用方法,这些方法的参数和返回值都是集合。不要将它与 Collection 接口混淆起来。
//用n个元素填充,实际就一个,储存代价小。
Collections.nCopies(n, anObject)
//返回的对象实现了一个不可修改的单元素集(**Set**),而不需要付出建立数据结构的开销。
Collections.singleton(anObject);
//singletonList方法与 singletonMap 方法类似。
//对于集合框架中的每一个接口,还有一些方法可以生成空集、列表、映射,等等。
Set<String> deepThoughts = Col1ecti ons.emptySet() ;//集的类型可以推导得出
9.4.2 子范围
子范围视图
- 可以为很多集合建立子范围(subrange) 视图。
subList 方法
List group2 = staff.subList(10, 20) ;
- 可以使用 subList 方法来获得一个列表的子范围视图。
- 第一个索引包含在内,第二个索引则不包含在内。
- 可以将任何操作应用于子范围,并且能够自动地反映整个列表的情况。
SortedSet / SortedMap子范围
//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)
- 对于有序集和映射,可以使用排序顺序而不是元素位置建立子范围。
- 有序映射也有类似的方法。
NavigableSet 接口
- Java SE 6 引人的 NavigableSet 接口赋予子范围操作更多的控制能力。
- 可以指定是否包括边界:
NavigableSet<E> subSet ( E from, boolean fromlnclusive, E to, boolean tolnclusive)
NavigableSet<E> headSet(E to, boolean tolnclusive)
Navigab1eSet<E> tailSet(E from, boolean fromlnclusive)
9.4.3 不可修改的视图
不可修改视图
//可以使用下面 8 种方法获得不可修改视图:
Coll ecti ons. unmodi fi abl eColl ection
Collections.unmodifiableList
Collections.unmodifiableSet
Collections.unmodifiableSortedSet
Collections.unmodifiableNavigableSet
Collections.unmodifiableMap
Collections.unmodifiableSortedMap
Collections.unmodifiableNavigableMap
- Collections 还有几个方法,用于产生集合的不可修改视图 (unmodifiable views)。
- 这些视图对现有集合增加了一个运行时的检查。如果发现试图对集合进行修改, 就抛出一个异常,同时这个集合将保持未修改的状态。
- 不可修改视图并不是集合本身不可修改。仍然可以通过集合的原始引用(在这里是 staff)对集合进行修改。并且仍然可以让集合的元素调用更改器方法。
unmodifiableCollection 方法
- unmodifiableCollection 方法 ( synchronizedCollection 和 checkedCollection 方法一样 )将返回一个集合,它的 equals 方法不调用底层集合的 equals 方法。
- 相反,它继承了 Object 类的 equals 方法,这个方法只是检测两个对象是否是同一个对象。(并未覆写equals方法)
- 然而,unmodifiableSet 类和 unmodifiableList 类却使用底层集合的 equals 方法 和 hashCode 方法。(equals内调用该类equals方法)
9.4.4 同步视图
如果由多个线程访问集合,就必须确保集不会被意外地破坏。
synchronizedMap方法
- 类库的设计者使用视图机制来确保常规集合的线程安全,而不是实现线程安全的集合类。
- Collections 类的静态 synchronizedMap方法可以将任何一个映射表转换成具有同步访问方法的 Map:
Map<String, Employee〉map = Collections.synchronizedMap(new HashMap<String, Employee>());
- 实际将各个方法使用 synchronized 关键字加锁了,但是注意并未对iterator()方法同步
9.4.5 受查视图
“ 受査” 视图用来对泛型类型发生问题时提供调试支持。
解决混入泛型问题
ArrayList<String> strings = new ArrayListoO;
ArrayList rawList = strings; // warning only, not an error, for compatibility with legacy code
rawList.add(new DateO); // now strings contains a Date object!
//这个错误的 add 命令在运行时检测不到。相反, 只有在稍后的另一部分代码中调用 get 方法, 并将结果转化为 String 时,这个类才会抛出异常。
- 受査视图可以探测到这类问题。下面定义了一个安全列表。
- 视图的 add 方法(以及addAll方法)将检测插人的对象是否属于给定的类。 如果不属于给定的类,就立即抛出一个 ClassCastException。
List<String> safestrings = Collections.checkedList(strings, String,class);
//使用
ArrayList rawList = safestrings;
rawList.add(new DateO); // checked list throws a ClassCastException
9.4.6 关于可选操作的说明
视图局限性
- 视图有一些局限性,即可能只可以读、无法改变大小、只支持删除而不支持插人,这些与映射的键视图情况相同。
可选操作
- 在集合和迭代器接口的 API 文档中,许多方法描述为“ 可选操作”。这看起来与接口的概念有所抵触。
- 一个更好的解决方案是为每个只读视图和不能改变集合大小的视图建立各自独立的两个接口。
- 要同时达到所有目标的要求,或者尽量兼顾所有目标完全是不可能的。但是,在自己的编程问题中,很少遇到这样极端的局限性。应该能够找到一种不必依靠极端衡量“ 可选的” 接口操作来解决这类问题的方案。
9.5 算 法
链表
- 对于链表来说,无法实施高效的随机访问,但却可以使用迭代器。
Java类库算法
- Java 类库中的算法没有如此丰富, 但是,也包含了基本的
排序、二分查找等实用算法。
9.5.1 排序与混排
** sort 方法**
List<String> staff = new LinkedListo();
fill collection
Collections.sort(staff);
//使用比较器
staff.sort(Comparator.comparingDouble(Employee::getSalary));
//逆序
staff.sort(Comparator.reverseOrder())
- Collections 类中的 sort 方法可以对实现了 List 接口的集合进行排序。
- 这个方法假定列表元素实现了 Comparable 接口。如果想采用其他方式对列表进行排序,可以使用 List 接口的 sort方法并传入一个 Comparator 对象。
- 如果想按照降序对列表进行排序,可以使用一种非常方便的静态方法 Collections.reverseOrder()。
Java排序算法
- 实际上,可以使用归并排序对列表进行高效的排序。
- 然而,Java 程序设计语言直接将所有元素转人一个数组,对数组进行排序,然后,再将排序后的序列复制回列表。
- 集合类库中使用的排序算法比快速排序要慢一些,快速排序是通用排序算法的传统选择。但是,归并排序有一个主要的优点:稳定,即不需要交换相同的元素。
如果两个雇员的工资相等发生什么情况呢? 如果采用稳定的排
序算法,将会保留按名字排列的顺序。
排序要求
- 因为集合不需要实现所有的“ 可选” 方法,因此,所有接受集合参数的方法必须描述什么时候可以安全地将集合传递给算法。
例如,显然不能将 unmodifiableList 列表传递给排序算法。
- 根据文档说明,列表必须是可修改的,但不必是可以改变大小的。
- 如果列表支持 set 方法,则是可修改的。
- 如果列表支持 add 和 remove 方法,则是可改变大小的。
混排算法 shuffle
ArrayList<Card> cards = . . .;
Collections.shuffle(cards);
- Collections 类有一个算法 shuffle,其功能与排序刚好相反,即随机地混排列表中元素的顺序。
- -如果提供的列表没有实现 RandomAccess 接口,shuffle 方法将元素复制到数组中,然后打乱数组元素的顺序,最后再将打乱顺序后的元素复制回列表。
9.5.2 二分查找
二分查找
i = Collections.binarySearch(c, element);
i = Collections.binarySearch(c, element, comparator);
- Collections 类的 binarySearch方法实现了这个算法。
- 注意,集合必须是排好序的,否则算法将返回错误的答案。
- 要想查找某个元素,必须提供实现 List 接口的集合。
- 如果集合没有采用 Comparable 接口的 compareTo 方法进行排序,就还要提供一个比较器对象。
使用
- 如果 binarySearch 方法返回的数值大于等于 0, 则表示匹配对象的索引。
- 如果返回负值,则表示没有匹配的元素。可以利用返回值计算应该将 element 插人到集合的哪个位置,以保持集合的有序性。
insertionPoint = -i - 1;
//插入位置
if (i < 0)
c.add(-i - 1, element) ;
说明
- 只有采用随机访问,二分査找才有意义。
- 如果必须利用迭代方式一次次地遍历链表的一半元素来找到中间位置的元素,二分査找就完全失去了优势。
- 因此,如果为 binarySearch 算法提供一个链表, 它将自动地变为线性查找。
9.5.3 简单算法
为什么提供简单算法
- 大多数程序员肯定可以很容易地采用循环实现这些算法。
- 它们可以让程序员阅读算法变成一件轻松的事情。
举例
words.removelff(w -> w.length() <= 3) ;
words.repl aceAll(String: :toLowerCase) ;
- Java SE 8 增加了默认方法 Collection.removelf 和 List.replaceAll, 这两个方法稍有些复杂。要提供一个 lambda 表达式来测试或转换元素。
9.5.4 批操作
批量操作
//批量删除
colll.removeAll(coll2);
//会从 coll1 中删除所有未在 co112 中出现的元素。
colli.retainAll(coll2);
求交集
//利用构造器包含初始值
Set<String> result = new HashSeto(a);
//和 b 求交集得到结果
result.retainAll(b);
利用视图批操作
- 可以把这个思路更进一步,对视图应用一个批操作。
Map<String, Employee> staffMap = . . .;
Set<String> terainatedIDs = . . .;
//直接建立一个键集,并删除终止聘用关系的所有员工的 ID。
staffMap.keySet().removeAll (terminatedIDs);
9.5.5 集合与数组的转换
数组转换
//数组转换为集合
String□ values = . .
HashSet<String> staff = new HashSet<>(Arrays.asList(values));
//从集合得到数组
Object[] values = staff.toArray();
//不能使用强制类型转换:
String[] values = (String[]) staff.toArray();// Error!
//使用toArray 方法的一个变体形式,提供一个所需类型而且长度为 0 的数组。
String[] values = staff.toArray(new Stringt[0]);
//可以构造一个指定大小的数组
staff.toArray(new String[staff.size()]);
- 如果需要把一个数组转换为集合,Arrays.asList 包装器可以达到这个目的。
- 从集合得到数组会更困难一些。可以使用 toArray 方法(两种形式)。
为什么不直接传类型到数组
- 原因是这个方法有“ 双重职责”,不仅要填充一个已有的数组(如果它足够长) ,还要创建一个新数组。
9.5.6 编写自己的算法
编写自己算法
- 如果编写自己的算法(实际上,是以集合作为参数的任何方法) ,应该尽可能地使用接口,而不要使用具体的实现。
void fillMenu(JMenu menu, ArrayList<JMenuItem> items) {
for (JMenuItem item : items)
menu.add(item);
}
//在这里, 只需要访问所有的元素,这是Collection 接口的基本功能。
void fillMenu(JMenu menu, Collection<JMenuItem> items) {
for (jMenuItem item : items)
menu.add(item);
}
- 如果编写了一个返回集合的方法,可能还想要一个返回接口,而不是返回类的方法, 因为这样做可以在日后改变想法,并用另一个集合重新实现这个方法。
List<3MenuIteni> getAll Items0Menu menu) {
List<JMenuItem> items = new ArrayList<>();
for (int i = 0; i < menu.getltemCount(); i ++)
iterns.add(menu.getItem(i));
return items;
}
//如果不复制所有的菜单项,而仅仅提供这些菜单项的视图。
//要做到这一点,只需要返回 AbstractList 的匿名子类。
List<]MenuItem> getAllItems(final JHenu menu) {
return new AbstractList<>() {
public JMenuItem get(int i ) {
return menu.getltem(i);
}
public int size() {
return menu.getltemCount();
}
};
}
9.6 遗留的集合
集合框架中的遗留类
9.6.1 Hashtable 类
- Hashtable 类与 HashMap 类的作用一样,实际上,它们拥有相同的接口。
- Hashtable 的方法是同步的。
- 如果对同步性或与遗留代码的兼容性没有任何要求,就应该使用 HashMap。
- 如果需要并发访问, 则要使用 ConcurrentHashMap
9.6.2 枚举
- 遗留集合使用 Enumeration 接口对元素序列进行遍历。
- Enumeration 接口有两个方法,即 hasMoreElements 和 nextElement。这两个方法与 Iterator 接口的 hasNext 方法和 next 方法十分类似。
Enuineration<Einployee> e = staff.elements();
whi1e (e.hasMoreElements()) {
Employee e = e.nextElement();
...
}
- 静态方法 Collections.enumeration将产生一个枚举对象,枚举集合中的元素。
List<InputStream> streams = . . .;
SequenceInputStream in = new SequencelnputStream(Collections.enumeration(streams));
// the SequencelnputStream constructor expects an enumeration
在 C++ 中, 用迭代器作为参数十分普遍。幸好,在 Java 的编程平台中,只有极少的程序员沿用这种习惯。传递集合要比传递迭代器更为明智。 集合对象的用途更大。
9.6.3 属性映射
- 实现属性映射的 Java 平台类称为 Properties。
- 属性映射通常用于程序的特殊配置选项。
3 个特性
- 键与值都是字符串。
- 表可以保存到一个文件中, 也可以从文件中加载。
- 使用一个默认的辅助表。
9.6.4 栈
- 标准类库中就包含了 Stack 类,其中有大家熟悉的 push 方法和 pop方法。
- 但是,Stack 类扩展为 Vector 类,从理论角度看,Vector 类并不太令人满意,它可以让栈使用不属于栈操作的 insert 和 remove 方法,即可以在任何地方进行插入或删除操作,而不仅仅是在栈顶。
9.6.5 位集
//例如,对于一个名为 bucketOfBits 的 BitSet,
bucketOfBits.get(i)
//如果第 i 位处于“ 开” 状态,就返回 true; 否则返回 false。同样地,
bucketOfBits.set(i)
//将第 i 位置为“ 开” 状态。最后,
bucketOfBits.clear(i)
//将第 i 位置为“ 关” 状态。
- Java 平台的 BitSet 类用于存放一个位序列(它不是数学上的集,称为位向量或位数组更为合适)。
- 如果需要高效地存储位序列(例如,标志)就可以使用位集。
- 由于位集将位包装在字节里,所以,使用位集要比使用 Boolean 对象的 ArrayList 更加高效。
Eratosthenes 筛子 算法
- Eratosthenes 筛子” 算法是最早发现的用来枚举这些基本数字(素数)的方法之一。
- 这并不是一种查找素数的最好方法,但是由于某种原因,它已经成为测试编译程序性能的一种流行的基准。