《Java核心卷 I》第10版阅读笔记(书第9章开始)

混江龙づ霸主 提交于 2020-10-01 08:07:48


day31

第八章 集合

8.1 Java 集合框架

8.1.1 将集合的接口与实现分离

● 队列 (queue) 是如何分离的?
队列的简化接口如下图
在这里插入图片描述队列通常有两种实现方式: 一种是使用循环数 组;另一种是使用链表。
利用这种方式,一旦改变了想法, 可以轻松地使用另外一种不同的实现。只需要对程序的一个地方做出修改, 即调用构造器的地方。
Queue< Customer> expresslane = new CircularArrayQueue<>(100):
Queue< Customer> expressLane = new LinkedListQueue<>();




8.1.2 Collection 接口

●在 Java 类库中,集合类的基本接口是 Collection 接口。这个接口有两个基本方法:
public interface Collection<b
{
boolean add(E element);
Iterator< E> iterator();

}
add方法用于向集合中添加元素。如果添加元素确实改变了集合就返回 true, 如果集合 没有发生变化就返回 false。
iterator方法用于返回一个实现了 Iterator 接口的对象。可以使用这个迭代器对象依次访 问集合中的元素。







8.1.3 迭代器

●Iterator 接口包含 4个方法:
public interface Iterator< E>
{
E next();
boolean hasNext();
void remove();
default void forEachRemaining(Consumer<? super E> action);
}
Collection< String> c = . . .;
Iterator< String> iter = c.iterator();
while (iter.hasNext())
{
String element = iter.next();
do something with element
}
用“ foreach” 循环可以更加简练地表示同样的循环操作:
for (String element : c)
{
do something with element
}
编译器简单地将“ foreach” 循环翻译为带有迭代器的循环。 “ for each” 循环可以与任何实现了 Iterable 接口的对象一起工作。Collection 接口扩展了 Iterable 接口。因此, 对于标准类库中的任何集合都可以使用“ for each” 循环。
●在 Java SE 8中,可以调用 forEachRemaining方法并提供一 lambda 表达式(它会处理一个元素) 。将对迭代器的每一个元素调用这个 lambda 表达式,直到再没有元素为止。 iterator.forEachRemaining(element -> do something with element); 元素被访问的顺序取决于集合类型
●应该将 Java迭代器认为是位于两个元素之间。当调用 next 时,迭代器就越过下 一个元素,并返回刚刚越过的那个元素的引用。
在这里插入图片描述●Iterator 接口的 remove 方法将会删除上次调用 next 方法时返回的元素。如果想要 删除指定位置上的元素, 需要先越过这个元素,例如:
Iterator< String> it = c.iterator()
it.next(); // skip over the first element
it.remove(); // now remove it
对 next 方法和 remove 方法的调用具有互相依赖性。如果调用 remove 之前 没有调用 next 将是不合法的,会抛出一个 IllegalStateException 异常


























8.1.4 泛型实用方法

●由于 Collection与 Iterator 都是泛型接口,可以编写操作任何集合类型的实用方法
-java.util.Collection< E> 1.2提供的方法
-Iterator< E> iterator() 返回一个用于访问集合中每个元素的迭代器
-int size() 返回当前存储在集合中的元素个数
-boolean isEmpty() 如果集合中没有元素, 返回 true
-boolean contains(Object obj) 如果集合中包含了一个与 obj 相等的对象, 返回 true
-boolean containsAll(Collection<?> other) 如果这个集合包含 other 集合中的所有元素, 返回 true
-boolean add(Object element) 将一个元素添加到集合中。如果由于这个调用改变了集合,返回 true
-boolean addAll(Collection<? extends E> other) 将 other 集合中的所有元素添加到这个集合。如果由于这个调用改变了集合,返回 true
-boolean remove(Object obj) 从这个集合中删除等于 obj 的对象。如果有匹配的对象被删除, 返回 true
-boolean removeAll(Collection<?> other) 从这个集合中删除other 集合中存在的所有元素。如果由于这个调用改变了集合,返回 true
-default boolean removelf(Predicate<? super E> filter) 8 从这个集合删除 filter返回 true 的所有元素。如果由于这个调用改变了集合,则返回 true
-void clear() 从这个集合中删除所有的元素
-boolean retainAll(Collection<?> other) 从这个集合中删除所有与 other 集合中的元素不同的元素。如果由于这个调用改变了 集合, 返回 true
-Object[] toArray() 返回这个集合的对象数组
-< T> T[] toArray(T[] arrayToFill) 返回这个集合的对象数组。如果 arrayToFill 足够大, 就将集合中的元素填入这个数组 中。剩余空间填补 null ; 否则, 分配一个新数组, 其成员类型与 arrayToFill 的成员类型相同,其长度等于集合的大小, 并填充集合元素














8.1.5 集合框架中的接口

●集合有两个基本接口:Collection 和 Map,如下图。
在这里插入图片描述List 是一个有序集合(ordered collection), 元 素 会 增 加 到 容 器 中 的 特 定 位 置。可 以 采 用 两种方式访问元素:使用迭代器访问, 或者使用一个整数索引来访问。后一种方法称为随机 访问(random access), 因为这样可以按任意顺序访问元素。与之不同, 使用迭代器访问时,必须顺序地访问元素。
●Listlterator 接口是 Iterator 的一个子接口。它定义了一个方法用于在迭代器位置前面增加 一个元素: void add(E element)
●集合框架的这个方面设计得很不好。实际中有两种有序集合,其性能开销有很大差异。由数组支持的有序集合可以快速地随机访问,因此适合使用 List 方法并提供一个整数索引来访问。与之不同, 链表尽管也是有序的, 但是随机访问很慢,所以最好使用迭代器来遍历。如果原先提供两个接口就会容易一些了。
为了避免对链表完成随机访问操作,Java SE 1.4 引入了一个标记接口 RandomAccess。 这个接口可以用来测试一个特定的集合是否支持高效的随机访问。
●Set 接口等同于 Collection 接口,不过其方法的行为有更严谨的定义。集(set) 的 add方 法不允许增加重复的元素。要适当地定义集的 equals方法:只要两个集包含同样的元素就认为是相等的,而不要求这些元素有同样的顺序。hashCode方法的定义要保证包含相同元素的 两个集会得到相同的散列码。
●SortedSet 和 SortedMap接口会提供用于排序的比较器对象,这两个接口定义了可以得到集合子集视图的方法。
●Java SE 6引人了接口 NavigableSet 和 NavigableMap, 其中包含一些用于搜索和遍历有序集和映射的方法,TreeSet 和 TreeMap 类实现了这些接口。






8.2 具体的集合

在下表中,除了以 Map结尾的类之外, 其他类都实现了 Collection 接口,而以 Map结尾的类实现了 Map接口。
在这里插入图片描述在这里插入图片描述### 8.2.1 链表
●在 Java 程序设计语言中,所有链表实际上都是双向链接的 .
●链表是一个有序集合,每个对象的位置十分重要。LinkedList.add 方法将对象添加到链表的尾部。但是, 常常需要将元素添加到链表的中间。由于迭代器是描述集合中位置的, 所以这种依赖于位置 的 add 方法将由迭代器负责。只有对自然有序的集合使用迭代器添加元素才有实际意义。例 如, 下一节将要讨论的集(set) 类型,其中的元素完全无序。因此, 在 Iterator 接口中就没有 add 方法,而是在集合类库中提供了子接口 Listlterator, 其中包含 add 方法,add 方法在迭代器位置之前添加一个新对象。


与 Collection.add 不同, Listlterator 的add方法不返回 boolean 类型的值, 它假定添加操作总会改变链表。 另外, Listlterator 接口有两个方法, 可以用来反向遍历链表。
E previous()
boolean hasPrevious()
与 next 方法一样, previous 方法返回越过的对象。
●在调用 next 之后,remove 方法确实与 BACKSPACE 键一样删除了迭代器左侧的元素。但是, 如果调用 previous 就会将右侧的元素删除掉, 并且不能连续调用两次 remove方法。 add 方法只依赖于迭代器的位置, 而 remove 方法依赖于迭代器的状态。
●set 方法用一个新元素取代调用 next 或 previous方法返回的上一个元素。
●可以想象, 如果在某个迭代器修改集合时, 另一个迭代器对其进行遍历,一定会出现混乱的状况。如果迭代 器发现它的集合被另一个迭代器修改了, 或是被该集合自身的方法修改了, 就会抛出一个 ConcurrentModificationException 异常。





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

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

链表只负责跟踪对列表的结构性修改,例如,添加元素、删除元素。set 方法不被视为结构性修改。可以将多个迭代器附加给一个链表,所有的迭代器都调用 set 方法对现有结点的内容进行修改
●链表不支持快速地随机访 问。如果要查看链表中第n个元素,就必须从头开始, 越过n-1个元素。没有捷径可走。因此若程序需要采用整数索引访问元素时,通常不选用链表。

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

绝对不应该使用这种让人误解的随机访问方法来遍历链表。下面这段代码的效率极低:
for (int i = 0; i < list.size();i++)
dosomethingwith list.get(i);
每次査找一个元素都要从列表的头部重新开始搜索。LinkedList对象根本不做任何缓存位置信息的操作。
●列表迭代器接口还有一个方法,可以告之当前位置的索引。
由于 Java 迭代器指向两个元素之间的位置, 所以可以同时产生两个索引:nextlndex方法返回下一 次调用 next方法时返回元素的整数索引;previouslndex方法返回下一次调用previous 方法时 返回元素的整数索引。previouslndex只比 nextlndex 返回的索引值小 1。这两个方法的效率非常高,因为迭代器保持着当前位置的计数值。




如果有一个整数索引 n,list.listlterator(n) 将返回一个迭代器, 这个迭代器指向索引为n 的元素前面的位置。即调用 next 与调用 list.get(n) 会产生同一个元素, 只是获得这个迭代器的效率比较低。

●建议避免使用以整数索引表示链表中位置的所有方法。如果需要对集合进行随机访 问, 就使用数组或 ArrayList, 而不要使用链表。
使用链表的唯一理由是尽可能地减少在列表中间插人或删除 元素所付出的代价。如果列表只有少数几个元素, 就完全可以使用 ArrayList

p 377代码实现

8.2.2 数组列表

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

Vector 类的所有方法都是同步的。 可 以由两个线程安全地访问一个 Vector 对象,建议在不需要同步时使用 ArrayList, 而不要使用 Vector。

8.2.3 散列集

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

●散列表(hash table)可以快速地査找所需要的对象,散列表为每个对象计算一个整数, 称为散列码(hashcode)。散列码是由对象的实 域产生的一个整数。 具有不同数据域的对象将产生不同的散列码。如果自定义类,就要负责实现这个类的 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 类型。set 是没有重复元素的元素集合。set 的 add方法首先在集中查找要添加的对象,如果不存在,就将这个对象添加进去。
Java 集合类库提供了一个 HashSet 类,它实现了基于散列表的集。可以用 add 方法添加 元素。contains方法已经被重新定义,用来快速地查看是否某个元素已经出现在集中。它只 在某个桶中査找元素,而不必查看集合中的所有元素。

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

8.2.4 树集

●TreeSet类与散列集十分类似, 不过, 它比散列集有所改进。树集是一个有序集合 ( sorted collection)。在对集合进行遍历时,每个值将自动地按照排序后的顺序呈现,例如,假设插入 3 个字符串,然后访问添加的所有元素。
SortedSet< String> sorter = new TreeSet<>(); // TreeSet implements SortedSet
sorter.add(“Bob”);
sorter.add(“Aniy”);
sorter.add(“Carl”);
for (String s : sorter)
System.println(s);
这时,每个值将按照顺序打印出来:Amy Bob Carl。正如 TreeSet 类名所示,排序是用树结构完成的(当前实现使用的是红黑树 red-black tree)。每次将一个元素添加到树中时,都被放置在正确的排序位置上。因此,迭代器总是以排好序的顺序访问每个元素.






●将一个元素添加到树中要比添加到散列表中慢,如果树中包含 n 个元素, 査找新元素的正确位置平 均需要 l0g2 n 次比较。但是,与检查数组或链表中的重复元素相比还是快很多。

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

●hashset VS treeset
考虑数据是否需要排序,如果不需要对数据进行排序, 就没有必要付出排序的开销。更重要的是,对于某些数据来说,对其排序要比散列函数更加困难。散列函数只是将对象适当地打乱存放, 而比 较却要精确地判别每个对象。 (两个矩形的比较问题,面积?行不通,可能会有两个不同的矩形, 它们的坐标不同, 但面积却相同。树的排序必须是全序。也 就是说, 任意两个元素必须是可比的, 并且只有在两个元素相等时结果才为 0)
p384 代码
●从 JavaSE 6 起, TreeSet 类实现了 NavigableSet 接口。这个接口增加了几个便于 定位元素以及反向遍历的方法


8.2.5 队列与双端队列

day33
●队列可以让人们有效地在尾部添加一个元素, 在头部删除一个元素。
双端队列,可以让人们有效地在头部和尾部同时添加或删除元 素。不支持在队列中间添加元素。
在 Java SE 6中引人了 Deque 接口,并由 ArrayDeque 和 LinkedList 类实现


●Queue
•boolean add(E element)
•boolean offer(E element)
如果队列没有满,将给定的元素添加到这个双端队列的尾部并返回 true。如果队列满 了,第一个方法将拋出一个 IllegalStateException, 而第二个方法返回 false。
•E remove( )
•E poll( )
假如队列不空,删除并返回这个队列头部的元素。如果队列是空的,第一个方法抛出 NoSuchElementException, 而第二个方法返回 null。
•E element( )
•E peek( )
如果队列不空,返回这个队列头部的元素, 但不删除。如果队列空,第一个方法将拋 出一个 NoSuchElementException, 而第二个方法返回 null。








●Deque
双端队列

8.2.6 优先级队列

优先级队列(priority queue) 中的元素可以按照任意的顺序插人,却总是按照排序的顺序进行检索。

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

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

使用优先级队列的典型示例是任务调度

8.3 映射

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

8.3.1 基本映射操作

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

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

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

如果在映射中没有与给定键对应的信息,get 将返回 null。 null 返回值可能并不方便。有时可以有一个好的默认值, 用作为映射中不存在的键。然后使用 getOrDefault方法。
Map<String,Integer> scores = …
int score = scores.getOrDefault(id, 0);// Gets 0 if the id is not present

要迭代处理映射的键和值, 最容易的方法是使用 forEach 方法。可以提供一个接收键和 值的 lambda 表达式。映射中的每一项会依序调用这个表达式。
scores.forEach((k, v) -> System.out.println(“key=” + k + ", value: " + v));

8.3.2 更新映射项

●正常情况下,可以得到与一个键关联的原值, 完成更新, 再放回更新后的值。不过,必须考虑一个特殊情况, 即键第一次出现。
下面来看 一个例子,使用一个映射统计一个单词在文件中出现的频度。看到一个单词(word) 时,我 们将计数器增 1,第一次看到 word时。在这种情况下,get 会返 回 null:
①可以使用 getOrDefault方法
counts.put(word, counts.getOrDefault(word, 0)+ 1) ;
②首先调用 putlfAbsent 方法。只有当键原先存在时才会放入一个值。
counts.putlfAbsent(word, 0) ;
counts.put(word, counts.get(word)+ 1) ;
③merge方法,如果键不存在,将把 word 与 1 关联,否则使用 Integer::sum 函数,将原值与 1 求和
counts.merge(word, 1, Integer::sum) ; (也就是)。







8.3.3 映射视图

●集合框架不认为映射本身是一个集合。(其他数据结构框架认为映射是一个键 / 值对 集合, 或者是由键索引的值集合。 )不过, 可以得到映射的视图(View)——这是实现了 Collection 接口或某个子接口的对象。

●有 3 种视图: 键集、 值集合(不是一个集)以及键 / 值对集。键和键 / 值对可以构成一个集, 因为映射中一个键只能有一个副本,下面的方法:
Set< K> keySet()
Collection< V> values()
Set<Map.Entry<K, V> entrySet()
会分别返回这 3 个视图。(条目集的元素是实现 Map.Entry 接口的类的对象。 ) 需要说明的是,keySet 不是 HashSet 或 TreeSet, 而是实现了 Set 接口的另外某个类的对 象。Set 接口扩展了 Collection接口。因此, 可以像使用集合一样使用keySet



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

8.3.4 弱散列映射

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

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

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

8.3.5 链接散列集与映射

LinkedHashSet 和 LinkedHashMap类用来记住插人元素项的顺序。这样就可以避免在散歹IJ表 中的项从表面上看是随机排列的。当条目插入到表中时,就会并人到双向链表中。
在这里插入图片描述●链接散列映射将用访问顺序, 而不是插入顺序, 对映射条目进行迭代。每次调用 get 或 put, 受到影响的条目将从当前的位置删除,并放到条目链表的尾部(只有条目在链表中的位置会受影响, 而散列表中的桶不会受影响。一个条目总位于与键散列码对应的桶中)。
访问顺序对于实现高速缓存的“ 最近最少使用” 原则十分重要

8.3.6 枚举集与映射

EmimSet 是一个枚举类型元素集的高效实现。由于枚举类型只有有限个实例, 所以 EnumSet 内部用位序列实现。如果对应的值在集中, 则相应的位被置为 1。
●EnumSet 类没有公共的构造器。可以使用静态工厂方法构造这个集:
在这里插入图片描述可以使用 Set 接口的常用方法来修改 EnumSet。
EnumMap是一个键类型为枚举类型的映射。它可以直接且高效地用一个值数组实现。 在使用时,需要在构造器中指定键类型:
EnumMap<Weekday, Employee〉personlnCharge = new EnumMapo(Weekday.class);



8.3.7 标识散列映射

类 IdentityHashMap 有特殊的作用。在这个类中, 键的散列值不是用 hashCode 函数计算 的, 而是用 System.identityHashCode 方法计算的。 这是 Object.hashCode 方法根据对象的内存地址来计算散列码时所使用的方式。而且, 在对两个对象进行比较时, IdentityHashMap 类 使用 ==, 而不使用 equals。 也就是说, 不同的键对象, 即使内容相同, 也被视为是不同的对象。 在实现对象遍历算 法(如对象串行化)时, 这个类非常有用, 可以用来跟踪每个对象的遍历状况。

8.4 视图与包装器

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

8.4.1 轻量级集合包装器

●Arrays 类的静态方法 asList 将返回一个包装了普通 Java 数组的 List 包装器
Card[] cardDeck= new Card[52];

List< Card> cardList = Arrays.asList(cardDeck):
返回的对象不是 ArrayList,而是一个视图对象, 带有访问底层数组的 get 和 set方 法。改变数组大小的所有方法(例如,与迭代器相关的 add 和 remove 方法)都会抛出一个 UnsupportedOperationException 异常。



●Collections.nCopies(n, anObject) 将返回一个实现了 List 接口的不可修改的对象, 并给人一种包含n个元素, 每个元素都像是 一个 anObject 的错觉。
视图的存储代价很小,这是视图技术的一种巧妙应用.

●Collections 类包含很多实用方法, 这些方法的参数和返回值都是集合。 不要将它 与 Collection 接口混淆起来.
如果调用下列方法 Collections.singleton(anObject) 则将返回一个视图对象。这个对象实现了 Set 接口(与产生 List 的 ncopies方法不同) 。返回 的对象实现了一个不可修改的单元素集, 而不需要付出建立数据结构的开销。singletonList 方法与 singletonMap方法类似。

8.4.2 子范围

●可以为很多集合建立子范围(subrange) 视图。例如, 假设有一个列表 staff, 想从中取出 第 10 个 ~ 第 19 个元素。可以使用 subList方法来获得一个列表的子范围视图。
List group2 = staff.subList(10, 20);//[10,20)

●可以将任何操作应用于子范围,并且能够自动地反映整个列表的情况。例如, 可以删除 整个子范围:
group2.clear(); // staff reduction
现在, 元素自动地从 staff 列表中清除了, 并且 group2 为空。

●对于有序集和映射, 可以使用排序顺序而不是元素位置建立子范围
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)
返回映射视图, 该映射包含键落在指定范围内的所有元素。






8.4.3 不可修改的视图

●Collections 还有几个方法, 用于产生集合的不可修改视图 (unmodifiable views)。这些视图对现有集合增加了一个运行时的检查。如果发现试图对集合进行修改, 就抛出一个异常, 同时这个集合将保持未修改的状态。
●不可修改视图并不是集合本身不可修改。仍然可以通过集合的原始引用(在这里是 staff) 对集合进行修改。并且仍然可以让集合的元素调用更改器方法。
●由于视图只是包装了接口而不是实际的集合对象, 所以只能访问接口中定义的方法。例 如, LinkedList 类有一些非常方便的方法,addFirst 和 addLast,它们都不是 List 接口的方法, 不能通过不可修改视图进行访问。
●unmodifiableCollection 方法(以及将要讨论的 synchronizedCollection 和 checked Collection 方法一样)将返回一个集合, 它的 equals 方法不调用底层集合的 equals 方法,而是继承了 Object 类的 equals 方法, 这个方法只是检测两个对象是否是同一个对 象。如果将集或列表转换成集合, 就再也无法检测其内容是否相同了。 •视图就是以这种 方式运行的, 因为内容是否相等的检测在分层结构的这一层上没有定义妥当。视图将以同样的方式处理 hashCode 方法
然而,unmodifiableSet 类和 unmodifiableList 类 却 使 用 底 层 集 合 的 equals 方 法 和 hashCode 方 法。



8.4.4 同步视图

如果由多个线程访问集合,就必须确保集不会被意外地破坏。
类库的设计者使用视图机制来确保常规集合的线程安全,而不是实现线程安全的集合 类。例如, Collections 类的静态 synchronizedMap方法可以将任何一个映射表转换成具有同步访问方法的 Map

8.4.5 受查视图

● "受査” 视图用来对泛型类型发生问题时提供调试支持。
实际上将错误类型的元素混人泛型集合中的问题极有可能发生。例如:
ArrayList< String> strings = new ArrayList<>();
ArrayList rawList = strings; // warning only, not an error, for compatibility with legacy code rawList.add(new Date()); // now strings contains a Date object!
这个错误的 add命令在运行时检测不到。相反,只有在稍后的另一部分代码中调用 get方法,并将结果转化为 String 时,这个类才会抛出异常。
受査视图可以探测到这类问题。下面定义了一个安全列表:
List< String> safestrings = Collections.checkedList(strings,String,class);
视图的 add 方法将检测插人的对象是否属于给定的类。如果不属于给定的类,就立即抛出一个 ClassCastException。
●受查视图受限于虚拟机可以运行的运行时检查。 例如, 对于 ArrayList <Pair <String», 由于虚拟机有一个单独的“ 原始” Pair 类, 所以,无法阻止插入 Pair< Date>。







8.4.6 关于可选操作的说明

通常,视图有一些局限性, 即可能只可以读、无法改变大小、只支持删除而不支持插 人,这些与映射的键视图情况相同。如果试图进行不恰当的操作,受限制的视图就会抛出一 个 UnsupportedOperationException。 在集合和迭代器接口的 API 文档中,许多方法描述为“ 可选操作”。

8.5 算法

泛型集合接口有一个很大的优点, 即算法只需要实现一次.
例如对数组,列表,链表查找最大值的算法,可以将 max 方法实现为能够接收任何实现了 Collection 接口的对象。

8.5.1 排序与混排

●Collections 类中的 sort方法可以对实现了 List 接口的集合进行排序。
Java 直接将所有元素转人一个数组,对数组进行排序, 然后,再将排序后的序列复制回列表。
集合类库中使用的排序算法比快速排序要慢一些,快速排序是通用排序算法的传统选 择。但是,归并排序有一个主要的优点:稳定, 即不需要交换相同的元素
●因为集合不需要实现所有的“ 可选” 方法,因此,所有接受集合参数的方法必须描述什么时 候可以安全地将集合传递给算法。例如,显然不能将 unmodifiableList 列表传递给排序算法。
下面是有关的术语定义:
•如果列表支持 set 方法,则是可修改的。
•如果列表支持 add 和 remove方法,则是可改变大小的。





8.5.2 二分查找

●应用于有序集合
要想查找某个元素,必须提供集合(这个集合要实现 List 接口)以及要查找的元素。如果集合没有采用 Comparable 接口的 compareTo方法进行排序, 就还要提供一个比较器对象。
i = Collections.binarySearch(c, element);
i = Collections.binarySearch(c, element, comparator);
如果 binarySearch方法返回的数值大于等于 0, 则表示匹配对象的索引;如果返回负值, 则表示没有匹配的兀素。但是, 可以利用返回值计算应该将 element 插人到集合的哪个位置(insertionPoint = -i -1 ), 以保持集合的有序性。



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

8.5.3 简单算法

在 Collections 类中包含了几个简单且很有用的算法

8.5.4 批操作

●很多操作会“ 成批” 复制或删除元素。以下调用 coll1.removeAll(coll2); 将从 colli 中删除 coll2中出现的所有元素。与之相反, coll1.retainAll(coll2); 会从 coll1 中删除所有未在 coll2中出现的元素,可以用于求交集。
●可以把这个思路更进一步,对视图应用一个批操作。例如,假设有一个映射,将员工 ID 映射到员工对象, 而且建立了一个将不再聘用的所有员工的 ID。
Map<String, Employee> staffMap = . . .;
Set< String> terminatedlDs =…; //直接建立一个键集,并删除终止聘用关系的所有员工的 ID。 staffMap.keySet().removeAll(terminatedIDs);
由于键集是映射的一个视图,所以键和相关联的员工名会自动从映射中删除。
●通过使用一个子范围视图,可以把批操作限制在子列表和子集上。




8.5.5 集合与数组的转换

由于 Java 平台 API 的大部分内容都是在集合框架创建之前设计的, 所以,有时候需要在 传统的数组和比较现代的集合之间进行转换。
●如果需要把一个数组转换为集合,Arrays.asList 包装器可以达到这个目的。例如:
String[ ] values = . .
HashSet< String> staff = new HashSeto(Arrays.asList(values));
●从集合得到数组会更困难一些。当然,可以使用 toArray方法:
Object[ ] values = staff.toArray();
不过,这样做的结果是一个对象数组。尽管你知道集合中包含一个特定类型的对象,但 不能使用强制类型转换: StringQ values = (StringQ) staff.toArray0;// Error!
必须使用 toArray方法的一个变体形式,提供一个所需类型而且长度为 0 的数组。这样一来, 返回的数组就会创建为相同的数组类型: String[] values = staff.toArray(new Stringt[0]); 如果愿意,可以构造一个指定大小的数组: staff.toArray(new String[staff.size()]); 在这种情况下,不会创建新数组。
参考:https://www.jianshu.com/p/e6fbd96a6ecb







8.5.6 编写自己的算法

如果编写自己的算法(实际上,是以集合作为参数的任何方法),应该尽可能地使用接 口,而不要使用具体的实现。
在这里插入图片描述
vs
在这里插入图片描述


8.6 遗留的集合

在这里插入图片描述

8.6.1 Hashtable类

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

8.6.2 枚举

遗留集合使用 Enumeration接口对元素序列进行遍历。Enumeration接口有两个方法, 即 hasMoreElements 和 nextElement。这两个方法与 Iterator 接口的 hasNext 方法和 next 方法十 分类似。

8.6.3 属性映射

属性映射(property map) 是一个类型非常特殊的映射结构。它有下面 3 个特性:
•键与值都是字符串。
•表可以保存到一个文件中,也可以从文件中加载。
•使用一个默认的辅助表。 实现属性映射的 Java 平台类称为 Properties。
属性映射通常用于程序的特殊配置选项



8.6.4栈

从 1.0 版开始,标准类库中就包含了 Stack类,其中有大家熟悉的 push方法和 pop方法。 但是, Stack 类扩展为 Vector 类,从理论角度看,Vector 类并不太令人满意,它可以让栈使用不属于栈操作的 insert 和 remove方法,即可以在任何地方进行插入或删除操作,而不仅仅是在栈顶。

8.6.5 位集

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

开始第13章 p597的学习


标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!