阅读Java核心技术Ⅰ的笔记(Java基础、第九章、集合)

第九章

  • 集合
    • 9.1 Java 集合框架
      • 9.1.1 将集合的接口与实现分离
      • 9.1.2 Collection 接口
      • 9.1.3 迭代器
      • 9.1.4 泛型实用方法
      • 9.1.5 集合框架中的接口
    • 9.2 具体的集合
      • 9.2.1 链 表
      • 9.2.2 数组列表
      • 9.2.3 散列集
      • 9.2.4 树集
      • 9.2.5 队列与双端队列
      • 9.2.6 优先级队列
    • 9.3 映射
      • 9.3.1 基本映射操作
      • 9.3.2 更新映射项
      • 9.3.3 映射视图
      • 9.3.4 弱散列映射
      • 9.3.5 链接散列集与映射(非常有用)
      • 9.3.6 枚举集与映射
      • 9.3.7 标识散列映射
    • 9.4 视图与包装器
      • 9.4.1 轻量级集合包装器
      • 9.4.2 子范围
      • 9.4.3 不可修改的视图
      • 9.4.4 同步视图
      • 9.4.5 受查视图
      • 9.4.6 关于可选操作的说明
    • 9.5 算法
      • 9.5.1 排序与混排
      • 9.5.2 二分查找
      • 9.5.4 批操作
      • 9.5.5 集合与数组的转换
      • 9.5.6 编写自己的算法
    • 9.6 遗留的集合
      • 9.6.1 Hashtable 类
      • 9.6.2 枚举
      • 9.6.3 属性映射
      • 9.6.4 栈
      • 9.6.5 位集

集合

仅介绍如何使用标准库中的集合类。

9.1 Java 集合框架

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

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

与现代的数据结构类库的常见情况一样, Java 集合类库也将接口( interface) 与实现(implementation) 分离。熟悉的数据结构队列(queue) 是如何分离的。
队列接口指出可以在队列的尾部添加元素, 在队列的头部删除元素,并且可以査找队列中元素的个数。当需要收集对象, 并按照“ 先进先出” 的规则检索对象时就应该使用队列。
阅读Java核心技术Ⅰ的笔记(Java基础、第九章、集合)_第1张图片
所以实现一个队列很简单,队列接口的最简形式可能类似下面这样。

public interface Queue<E> // a simplified form of the interface in the standard library
{
	void add(E element);
	 E remove();
	int size();
}

这个接口并没有说明队列是如何实现的。队列通常有两种实现方式: 一种是使用循环数组;另一种是使用链表(见图 9-2 )。
阅读Java核心技术Ⅰ的笔记(Java基础、第九章、集合)_第2张图片
每一个实现都可以通过一个实现了Queue接口的类表示。Java也已经我们通过使用数据结构为我们实现了循环数组队列和链表队列,循环数组队列的类是ArrayDeque,链表队列是LinkedList。
使用接口类型存放类对象的引用(多态),所以只有在构建对象的时候才需要知道使用什么具体的类。

循环数组要比链表更高效,因此多数人优先选择循环数组。然而, 通常这样做也需要付出一定的代价。循环数组是一个有界集合, 即容量有限。如果程序中要收集的对象数量没有上限, 就最好使用链表来实现。

在研究 API 文档时,会发现另外一组名字以 Abstract 开头的类, 例如, AbstractQueue。这些类是为类库实现者而设计的。在集合中接口与实现分离,基本上每个接口都有一个实现了一部分方法的abstract类,当想要实现自己的类的时候,就可以通过继承这个abstract类来实现。

9.1.2 Collection 接口

在 Java 类库中,集合类的基本接口是 Collection 接口。这个接口有两个基本方法。

public interface Collection<b {
	boolean add(E element);
	Iterator<E> iteratorQ;
	...
}

add方法用于向集合中添加元素。如果添加元素确实改变了集合就返回 true, 如果集合没有发生变化就返回 false。例如, 如果试图向集中添加一个对象, 而这个对象在集中已经存在,这个添加请求就没有实效,因为集中不允许有重复的对象。
iterator方法用于返回一个实现了 Iterator 接口的对象。可以使用这个迭代器对象依次访问集合中的元素。iterator是iterable接口中的方法。

9.1.3 迭代器

Iterator 接口包含 4 个方法:

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

如果想要査看集合中的所有元素,就请求一个迭代器,并在 hasNext 返回 true 时反复地调用 next 方法。使用for each可以更加的快捷。编译器简单地将“ foreach” 循环翻译为带有迭代器的循环。“ for each” 循环可以与任何实现了 Iterable 接口的对象一起工作, 这个接口只包含一个抽象方法:

public interface Iterable<E>
{
	Iterator<E> iterator();
}

Collection 接口扩展了 Iterable 接口。因此, 对于标准类库中的任何集合都可以使用“ for each” 循环。
在Java8中不用写循环可以调用forEachRemaining 方法并提供一 lambda
表达式(它会处理一个元素)。将对迭代器的每一个元素调用这个 lambda 表达式,直到再没有元素为止。

iterator.forEachRemaining(element -> do something with element);

元素被访问的顺序取决于集合类型。如果对 ArrayList 进行迭代访问顺序和插入的顺序相同,如果访问 HashSet 中的元素,访问顺序则是按照某种随机的次序。
Iterator 接口的 next 和 hasNext 方法与 Enumeration 接口的nextElement 和 hasMoreElements 方法的作用一样。但是设计者不喜欢这个类库累赘的方法名,所以又引入了具有较短方法名的新接口。
Java 迭代器查找操作与位置变更是紧密相连的。查找一个元素的唯一方法是调用 next, 而在执行查找操作的同时, 迭代器的位置随之向前移动。因此,应该将 Java 迭代器认为是位于两个元素之间。 当调用 next 时,迭代器就越过下一个元素,并返回刚刚越过的那个元素的引用(见图 9-3 ) 。

Iterator 接口的 remove 方法将会删除上次调用 next 方法时返回的元素。next 方法和 remove 方法的调用具有互相依赖性。不能连续的调用remove。
阅读Java核心技术Ⅰ的笔记(Java基础、第九章、集合)_第3张图片

9.1.4 泛型实用方法

下面是一个检测任意集合是否包含指定元素的泛型方法:

public static <E> boolean contains(Coll ection<E> c, Object obj) {
	for (E element : c){
		if (element,equals(obj))
		return true;
	}
	return false;
 }

Java 类库的设计者认为:这些实用方法中的某些方法非常有用, 应该将它们提供给用户使用。
事实上,Collection 接口声明了很多有用的方法,所有的实现类都必须提供这些方法。为了能够让实现者更容易地实现这个接口,Java 类库提供了一个类 AbstractCollection,它将基础方法 size 和 iterator 抽象化了,其他的都默认实现了。
对于 Java SE 8, 这种方法有些过时了。 如果这些方法是 Collection 接口的默认方法会更好。
还有一个很有用的方法:

default boolean removelf(Predicate<? super E> filter)

这个方法用于删除满足某个条件的元素。

9.1.5 集合框架中的接口

阅读Java核心技术Ⅰ的笔记(Java基础、第九章、集合)_第4张图片
集合有两个基本接口:Collection 和 Map。 Collection中的添加元素的方法是

boolean add(E element)

不过,由于映射包含键 / 值对,所以要用 put 方法来插人:

V put(K key, V value)

要从集合读取元素, 可以用迭代器访问元素。不过,从映射中读取值则要使用 get 方法:

V get(K key)

List 是一个有序集合(有序、集合)。元素会增加到容器中的特定位置。可以采用两种方式访问元素:使用迭代器访问, 或者使用一个整数索引来访问。后一种方法称为随机访问。使用迭代器访问时必须顺序地访问元素
List 接口定义了多个用于随机访问的方法,一般带索引的访问都是随机访问法(有特殊情况)。Listlterator 接口是 Iterator 的一个子接口。它定义了一个方法用于在迭代器位置前面增加一个元素:

void add(E element)

集合框架的这个方面设计得很不好。实际中有两种有序集合,其性能开销有很大差异。数组支持的有序集合可以支持迭代器的有序访问也可以随机访问,但是链表实现的有序集合所谓的随机访问也是按照链表的索引有序访问,没办法直接定位到想要的元素,所以最好使用迭代器来遍历。如果原先提供两个接口就会容易一些了。

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

if(c instanceof RandomAccess){
	//支持高效的随机访问
}else{
	//使用顺序访问算法
}

Set 接口等同于 Collection 接口,不过其方法的行为有更严谨的定义。要适当地定义集的 equals 方法:只要两个集包含同样的元素就认
为是相等的
,而不要求这些元素有同样的顺序。hashCode 方法要与equals方法保持一致。

既然方法签名是一样的,为什么还要建立一个单独的接口呢?从概念上讲, 并不是所有集合都是集。建立一个 Set 接口可以让程序员编写只接受集的方法。
集与映射也可以按照自己需要的要求进行排序,SortedSet 和 SortedMap 接口会提供用于排序的比较器对象,这两个接口定义了可以得到集合子集视图的方法。最后,Java SE 6 引人了接口 NavigableSet 和 NavigableMap, 其中包含一些用于搜索和遍历有序集和映射的方法。(理想情况下,这些方法本应当直接包含在 SortedSet 和 SortedMap接口中。)TreeSet 和 TreeMap 类实现了这些接口

9.2 具体的集合

表 9-1 展示了 Java 类库中的集合,并简要描述了每个集合类的用途除了以 Map 结尾的类之外, 其他类都实现了 Collection 接口,而以 Map 结尾的类实现了 Map 接口。
阅读Java核心技术Ⅰ的笔记(Java基础、第九章、集合)_第5张图片
阅读Java核心技术Ⅰ的笔记(Java基础、第九章、集合)_第6张图片

9.2.1 链 表

在本书中,有很多示例已经使用了数组以及动态的 ArrayList 类。然而, 数组和数组列表都有一个重大的缺陷。这就是从数组的中间位置删除一个元素要付出很大的代价,其原因是数组中处于被删除元素之后的所有元素都要向数组的前端移动(见图 9-6。) 在数组中间的位置上插入一个元素也是如此。
阅读Java核心技术Ⅰ的笔记(Java基础、第九章、集合)_第7张图片
另外一个大家非常熟悉的数据结构一链表( linked list) 解决了这个问题。在 Java 程序设计语言中,所有链表实际上都是双向链接的(doubly linked)—即每个结点还存放着指向前驱结点的引用(见图 9-7。)
阅读Java核心技术Ⅰ的笔记(Java基础、第九章、集合)_第8张图片
从链表中间删除一个元素是一个很轻松的操作, 即需要更新被删除元素附近的链接(图9-8)。
阅读Java核心技术Ⅰ的笔记(Java基础、第九章、集合)_第9张图片
链表与泛型集合之间有一个重要的区别。链表是一个有序集合,每个对象的位置十分重要。LinkedList.add 方法将对象添加到链表的尾部。将将元素添加到链表的中间时由于迭代器是描述集合中位置的, 所以这种依赖于位置的 add 方法将由迭代器负责。只有对自然有序的集合使用迭代器添加元素才有实际意义下一节将要讨论的集(set) 类型,其中的元素完全无序。因此, 在 Iterator 接口中就没有add 方法。其中包含add方法:

interface ListIterator<E> extends Iterator<E>
{
	void add(E element);
	...
}

这个方法不返回 boolean 类型的值, 它假定添加操作总会改变链表。
另外, Listlterator 接口有两个方法, 可以用来反向遍历链表。

E previous()
boolean hasPrevious();

如果多次调用 add方法, 将按照提供的次序把元素添加到链表中。它们被依次添加到迭代器当前位置之前。

Iterator iterator=list.ierator();
iterator.add("1");
iterator.add("2");
ierator.next()
iterator.add("a");
iterator.add("b");
//1 a b 2
ierator.previous()
iterator.add("a");
iterator.add("b");
//a b 1 a b 2

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

可以想象, 如果在某个迭代器修改集合时, 另一个迭代器对其进行遍历,一定会出现混乱的状况。如果迭代器发现它的集合被另一个迭代器修改了, 或是被该集合自身的方法修改了, 就会抛出一个
ConcurrentModificationException 异常。
为了避免发生并发修改的异常,请遵循下述简单规则:可以根据需要给容器附加许多的迭代器,但是这些迭代器只能读取列表。另外,再单独附加一个既能读又能写的迭代器。有一种简单的方法可以检测到并发修改的问题。集合可以跟踪改写操作(诸如添加或删除元素)的次数。每个迭代器都维护一个独立的计数值。链表只负责跟踪对列表的结构性修改,例如,添加元素、 删除元素。set 方法不被视为结构性修改

如果要查看链表中第n个元素,就必须从头开始, 越过n-1个元素。没有捷径可走。鉴于这个原因,在程序需要采用整数索引访问元素时,程序员通常不选用链表。尽管如此, LinkedList 类还是提供了一个用来访问某个特定元素的 get 方法:每次査找一个元素都要从列表的头部重新开始搜索。LinkedList 对象根本不做任何缓存位置信息的操作。效率极低

列表迭代器接口还有一个方法,可以告之当前位置的索引。nextlndex 方法返回下一次调用 next 方法时返回元素的整数索引;previouslndex 方法返回下一次调用 previous 方法时返回元素的整数索引。我们建议避免使用以整数索引表示链表中位置的所有方法。如果需要对集合进行随机访问, 就使用数组或 ArrayList, 而不要使用链表。

9.2.2 数组列表

随机访问不适用于链表, 但对数组却很有用。集合类库提供了一种大家熟悉的 ArrayList 类, 这个类也实现了 List 接口。ArrayList 封装了一个动态再分配的对象数组。对于一个经验丰富的 Java 程序员来说, 在需要动态数组时, 可能会使用 Vector 类。Vector 类的所有方法都是同步的。

9.2.3 散列集

链表和数组可以按照人们的意愿排列元素的次序。 。但是,如果想要査看某个指定的元素, 却又忘记了它的位置, 就需要访问所有元素如果集合中包含的元素很多, 将会消耗很多时间。如果不在意元素的顺序,可以有几种能够快速査找元素的数据结构。其缺点是无法控制元素出现的次序。它们将按照有利于其操作目的的原则组织数据。

散列表为每个对象计算一个整数, 称为散列码(hashcode。) 散列码是由对象的实例域产生的一个整数。。更准确地说, 具有不同数据域的对象将产生不同的散列码。表 9-2 列出了几个散列码的示例,它们是由 String 类的 hashCode 方法产生的。
阅读Java核心技术Ⅰ的笔记(Java基础、第九章、集合)_第10张图片
自己实现的 hashCode方法应该与 equals 方法兼容,即如果 a.equals(b) 为 true, a 与 b 必须具有相同的散列码。
在 Java 中,散列表用链表数组实现。每个列表被称为桶( bucket) (参看图 9-10。) 要想査找表中对象的位置, 就要先计算它的散列码, 然后与桶的总数取余, 所得到的结果就是保存这个元素的桶的索引。
阅读Java核心技术Ⅰ的笔记(Java基础、第九章、集合)_第11张图片
当然,有时候会遇到桶被占满的情况, 这也是不可避免的。这种现象被称为散列冲突( hash collision) 。这时, 需要用新对象与桶中的所有对象进行比较,査看这个对象是否已经存在。如果散列码是合理且随机分布的, 桶的数目也足够大, 需要比较的次数就会很少。

在 JavaSE 8 中, 桶满时会从链表变为平衡二叉树。如果选择的散列函数不当,会产生很多冲突,或者如果有恶意代码试图在散列表中填充多个有相同散列码的值, 这样就能提高性能。
如果大致知道最终会有多少个元素要插人到散列表中, 就可以设置桶数。通常, 将桶数设置为预计元素个数的 75% ~ 150%。最好将桶数设置为一个素数, 以防键的集聚。标准类库使用的桶数是 2 的幂, 默认值为16。。如果散列表太满, 就需要再散列 (rehashed)。如果要对散列表再散列, 就需要创建一个桶数更多的表,并将所有元素插入到这个新表中,. 然后丢弃原来的表。装填因子( load factor) 决定何时对散列表进行再散列。例如, 如果装填因子为 0.75 (默认值,) 而表中超过 75%的位置已经填人
元素
, 这个表就会用双倍的桶数自动地进行再散列。装填因子为0.75 是比较合理的

Java 集合类库提供了一个 HashSet 类,它实现了基于散列表的集。可以用 add 方法添加元素。contains方法已经被重新定义,用来快速地查看是否某个元素已经出现在集中。它只在某个桶中査找元素,而不必查看集合中的所有元素。set 的 add方法首先在集中查找要添加的对象,如果不存在,就将这个对象添加进去。只有不关心集合中元素的顺序时才应该使用HashSet。

9.2.4 树集

TreeSet 类与散列集十分类似, 不过, 它比散列集有所改进。树集是一个有序集合(有序的意思是插入时按照某种特定的顺序,例如雇员年龄的大小或者姓名的长短排序,而不是按照插入顺序) 。可以以任意顺序将元素插入到集合中。在对集合进行遍历时,每个值将自动地按照排序后的顺序呈现。
正如 TreeSet 类名所示,排序是用树结构完成的(当前实现使用的是红黑树(red-black tree)。每次将一个元素添加到树中时,都被放置在正确的排序
位置上。因此,迭代器总是以排好序的顺序访问每个元素。将一个元素添加到树中要比添加到散列表中慢, 参见表 9-3中的比较,但是,**与检查数组或链表中的重复元素相比还是快很多。**如果树中包含 n 个元素, 査找新元素的正确位置平均需要 log2n 次比较。
阅读Java核心技术Ⅰ的笔记(Java基础、第九章、集合)_第12张图片
要使用树集, 必须能够比较元素。这些元素必须实现 Comparable 接口(参见 6.1.1 节,) 或者构造集时必须提供一个 Comparator (参见 6.2.2 节和 6.3.8 节)。

是否总是应该用树集取代散列集。毕竟, 添加一个元素所花费的时间看上去并不很长,而且元素是自动排序的。到底应该怎样做将取决于所要收集的数据。如果不需要对数据进行排序, 就没有必要付出排序的开销。更重要的是,对于某些数据来说,对其排序要比散列函数更加困难。散列函数只是将对象适当地打乱存放, 而比较却要精确地判别每个对象。

要想具体地了解它们之间的差异, 还需要研究一个收集矩形集的任务。如果使用TreeSet, 就需要提供Comparator。如何比较两个矩形呢? 比较面积吗? 这行不通。 可能会有两个不同的矩形, 它们的坐标不同, 但面积却相同。树的排序必须是全序。也就是说, 任意两个元素必须是可比的, 并且只有在两个元素相等时结果才为 0。相反地,Rectangle 类已经定义了散列函数, 它直接对坐标进行散列

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

9.2.5 队列与双端队列

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

9.2.6 优先级队列

优先级队列PriorityQueue(priority queue) 中的元素可以按照任意的顺序插入,却总是按照排序的顺序进行检索。也就是说,无论何时调用 remove 方法,总会获得当前优先级队列中最小的元素。然而,优先级队列并没有对所有的元素进行排序。如果用迭代的方式处理这些元素,并不需要对它们进行排序。优先级队列使用了一个优雅且高效的数据结构,称为堆(heap)。堆是一
个可以自我调整的二叉树,对树执行添加( add) 和删除(remore) 操作, 可以让最小的元素移动到根,而不必花费时间对元素进行排序。
**使用优先级队列的典型示例是任务调度。**每一个任务有一个优先级,任务以随机顺序添加到队列中。每当启动一个新的任务时,都将优先级最高的任务从队列中删除(由于习惯上将 1 设为“ 最高” 优先级,所以会将最小的元素删除)

9.3 映射

集是一个集合,它可以快速地查找现有的元素。但是,要查看一个元素, 需要有要查找元素的精确副本。这不是一种非常通用的査找方式。通常, 我们知道某些键的信息,并想要查找与之对应的元素。 映射(map) 数据结构就是为此设计的。

9.3.1 基本映射操作

Java 类库为映射提供了两个通用的实现:HashMap 和 TreeMap。这两个类都实现了 Map 接口。散列映射对键进行散列, 树映射用键的整体顺序对元素进行排序, 并将其组织成搜索树。散列或比较函数只能作用于键。与键关联的值不能进行散列或比较。
应该选择散列映射还是树映射呢? 与集一样, 散列稍微快一些, 如果不需要按照排列顺序访问键, 就最好选择散列。

如果在映射中没有与给定键对应的信息, get 将返回 null。null 返回值可能并不方便。有时可以有一个好的默认值, 用作为映射中不存在的键。然后使用 getOrDefault 方法。键必须是唯一的。不能对同一个键存放两个值。如果对同一个键两次调用 put 方法, 第二个值就会取代第一个值。实际上,put 将返回用这个键参数存储的上一个值。
要迭代处理映射的键和值, 最容易的方法是使用 forEach 方法。可以提供一个接收键和值的 lambda 表达式。映射中的每一项会依序调用这个表达式。

scores.forEach((k, v) ->
System.out.println("key=" + k + ", value:" + v));

9.3.2 更新映射项

处理映射时的一个难点就是更新映射项。正常情况下,可以得到与一个键关联的原值,完成更新, 再放回更新后的值。例如:看到一个单词(word) 时,我们将计数器增 1,如下所示:

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

这是可以的, 不过有一种情况除外:就是第一次看到 word 时。在这种情况下,get 会返回 null, 因此会出现一个 NullPointerException 异常。
作为一个简单的补救, 可以使用 getOrDefault 方法:

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

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

counts.putlfAbsent(word, 0);
counts.put(word, counts.get(word)+ 1); // Now we know that get will succeed

不过还可以做得更好。merge 方法可以简化这个常见的操作。如果键原先不存在,下面的调用:

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

将把 word 与 1 关联,否则使用 Integer::sum 函数组合原值和 1 (也就是将原值与 1 求和。

9.3.3 映射视图

集合框架不认为映射本身是一个集合。不过, 可以得到映射的视图( View )—这是实现了Collection 接口或某个子接口的对象
有 3 种视图: 键集、 值集合(不是一个集) 以及键 / 值对集。键和键 / 值对可以构成一个集, 因为映射中一个键只能有一个副本。 下面的方法:

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

需要说明的是, keySet 不是 HashSet 或 TreeSet, 而是实现了 Set 接口的另外某个类的对象。Set 接口扩展了 Collection 接口。因此, 可以像使用集合一样使用 keySet。
如今遍历Map只需要使用 forEach 方法:

counts.forEach((k,v) -> {
	do somethingwith k, v 
});

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

9.3.4 弱散列映射

设计 WeakHashMap 类是为了解决一个有趣的问题。如果有一个值,对应的键已经不再使用了, 将会出现什么情况呢? 假定对某个键的最后一次引用已经消亡,不再有任何途径引用这个值的对象了。但是,由于在程序中的任何部分没有再出现这个键,所以,这个键 / 值对无法从映射中删除。为什么垃圾回收器不能够删除它呢? 难道删除无用的对象不是垃圾回收器的工作吗?
遗憾的是,事情没有这样简单。 垃圾回收器跟踪活动的对象。只要映射对象是活动的,其中的所有桶也是活动的, 它们不能被回收。使用WeakHashMap 完成这件事情。当对键的唯一引用来自散列条目时, 这一数据结构将与垃圾回收器协同工作一起删除键 / 值对。

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

9.3.5 链接散列集与映射(非常有用)

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

链接散列映射将用访问顺序, 而不是插入顺序, 对映射条目进行迭代。每次调用 get 或put, 受到影响的条目将从当前的位置删除, 并放到条目链表的尾部(只有条目在链表中的位置会受影响, 而散列表中的桶不会受影响。一个条目总位于与键散列码对应的桶中)。要想构造这样一个的散列映射表, 请调用

LinkedHashMapcK, V>(initialCapacity, loadFactor, true)

**访问顺序对于实现高速缓存的“ 最近最少使用” 原则十分重要。**例如, 可能希望将访问频率高的元素放在内存中, 而访问频率低的元素则从数据库中读取。当在表中找不到元素项且表又已经满时, 可以将迭代器加入到表中, 并将枚举的前几个元素删除掉。这些是近期最少使用的几个元素
甚至可以让这一过程自动化。即构造一 LinkedHashMap 的子类,然后覆盖下面这个方法:

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

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

Map<K, V> cache = new LinkedHashMap<>(128, 0.75F, true) {
	protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
		return size() > 100; 
	}
};

另外,还可以对 eldest 条目进行评估,以此决定是否应该将它删除。例如,可以检査与这个条目一起存在的时间戳。

9.3.6 枚举集与映射

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

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);

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

EnumMap<Weekday, Employee> personlnCharge = new EnumMap(Weekday.class);

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

9.3.7 标识散列映射

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

9.4 视图与包装器

看一下图 9-4 和图 9-5 可能会感觉: 用如此多的接口和抽象类来实现数量并不多的具体集合类似乎没有太大必要。然而,这两张图并没有展示出全部的情况。通过使用视图( views) 可以获得其他的实现了 Collection 接口和 Map 接口的对象。映射类的 keySet 方法就是一个这样的示例。keySet 方法返回一个实现 Set接口的类对象, 这个类的方法对原映射进行操作。这种集合称为视图。
视图技术在集框架中有许多非常有用的应用。下面将讨论这些应用。

9.4.1 轻量级集合包装器

Java9引入了一些静态方法,可以生成给定元素的集或者列表。以及给定键值对的映射。例如

List<String> names=List.of("a","b","c");
Set<Integer> numbers=Set.of(2,3,4);

会生成包含3个元素的一个列表和一个集。对于映射,需要指定键和值,如下所示:

Map<String,Interger> map=Map.of("a",1,"b",2,"c",3);

元素、键和值不能为null。
List和set接口有11个方法,分别有0到10个参数,另外还有一个参数可变的of方法。提供这种特定性是为了提高效率。
对于Map接口,则无法提供一个参数可变的版本,因为参数类型会在值和键之间进行交替。不过有一个ofEntries,能接受任意多个Map.Entry对象例如:

import static java.util.Map.*;
...
Map<String,Integer> scores=ofEntries(
	entry("peter",2);
	entry("a",3);
	entry("b",4);

of和ofEntries方法可以生成某些类的对象,这些类对于每个元素会有一个实例变量,或者有一个后背数组提供支持。
通过of创建的对象是不可修改的。不能调用set改变他们的内容,如果使用的话会导致一个UnsupportedOperationException异常。
如果需要一个可更改的集合,可以把这个不可修改的集合传递到构造器:

ArrayList<String> list=new ArrayList<>(List.of("a","b","c"));

这个方法调用

Col1ections.nCopies(n, anObject)

将返回一个实现了 List 接口的不可修改的对象, 并给人一种包含n个元素, 每个元素都像是一个 anObject 的错觉。
例如下面创建包含100个字符串的list,每个串都被设置为"DEFAULT"

List<String> settings = Collections.nCopies(100, "DEFAULT");

存储代价很小,对象只存储一次。这是视图技术的一种巧妙应用。
还有一个方法Arrays.asList,他会返回一个可更改但是大小不可变的列表。也就是说在这个列表上可以调用set,但是不能使用add或者remove方法,另外还有Collections.emptySet和Collections.singleton。

9.4.2 子范围

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

List group2 = staff.subList(10, 20);

**第一个索引包含在内, 第二个索引则不包含在内。**这与 String类的 substring 操作中的参数情况相同。
可以将任何操作应用于子范围,并且能够自动地反映整个列表的情况,可以删除整个子范围:

group2.clear(); // staff reduction

现在,元素自动地从 staff 列表中清除了,并且 group2 为空

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

SortedSet< E> subSet(E from, E to)
SortedSet< E> headSet(E to)
SortedSet< E> tail Set(E from)

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

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

返回映射视图, 该映射包含键落在指定范围内的所有元素。
Java SE 6 引人的 NavigableSet 接口赋予子范围操作更多的控制能力。可以指定是否包括边界:

NavigableSet<E> subSet(E from, boolean fromlnclusive, E to, boolean toInclusive)
NavigableSet<E> headSet(E to, boolean tolnclusive)
Navigab1eSet<E> tail Set(E from, boolean fromlnclusive)

9.4.3 不可修改的视图

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

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<>();
...
1ookAt(Collections.unmodifiableList(staff));

Collections.unmodifiableList 方法将返回一个实现 List 接口的类对象。
不可修改视图并不是集合本身不可修改。仍然可以通过集合的原始引用(在这里是 staff)对集合进行修改。并且仍然可以让集合的元素调用更改器方法。

unmodifiableCollection 方法(与本节稍后讨论的 synchronizedCollection 和 checkedCollection 方法一样)将返回一个集合, 它的 equals 方法不调用底层集合的 equals 方法相反, 它继承了 Object 类的 equals 方法, 这个方法只是检测两个对象是否是同一个对象。如果将集或列表转换成集合, 就再也无法检测其内容是否相同了。

9.4.4 同步视图

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

Map<String, Employee> map = Collections.synchronizedMap(new HashMap<String, Employee>();

现在,就可以由多线程访问 map 对象了。像 get 和 put 这类方法都是同步操作的,即在另一个线程调用另一个方法之前,刚才的方法调用必须彻底完成

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!
//转为受查视图
List<String> safestrings = Collections.checkedList(strings,String,class);

视图的 add 方法将检测插人的对象是否属于给定的类。如果不属于给定的类,就立即抛出一个 ClassCastException。

9.4.6 关于可选操作的说明

通常,视图有一些局限性, 即可能只可以读、 无法改变大小、只支持删除而不支持插人,这些与映射的键视图情况相同。如果试图进行不恰当的操作,受限制的视图就会抛出一个 UnsupportedOperationException。**是否应该将“ 可选” 方法这一技术扩展到用户的设计中呢? 我们认为不应该。**尽管集合
被频繁地使用, 其实现代码的风格也未必适用于其他问题领域。

9.5 算法

泛型集合接口有一个很大的优点, 即算法只需要实现一次。例如将 max 方法实现为能够接收任何实现了 Collection 接口的对象。

public static <T extends Comparable〉T max (Collections c){
	if (c.isEmpty()) throw new NoSuchEIementException();
	Iterator<T> iter = c.iterator();
	T largest = iter.next();
	while (iter.hasNextO) { 
		T next = iter.next();
		if (largest.compareTo(next) < 0)
		largest = next; 
	}
	return largest; 
}

9.5.1 排序与混排

Collections 类中的 sort 方法可以对实现了 List 接口的集合进行排序。这个方法假定列表元素实现了 Comparable 接口。如果想采用其他方式对列表进行排序,可以使用 List 接口的 sort方法并传入一个 Comparator 对象。
如果想按照降序对列表进行排序, 可以使用一种非常方便的静态方法 Collections.reverseOrder(。) 这个方法将返回一个比较器, 比较器则返回 b.compareTo(a)。例如

staff.sort(Comparator.reverseOrder())

这个方法将根据元素类型的 compareTo 方法给定排序顺序, 按照逆序对列表 staff 进行排序。同样

staff.sort(Comparator.comparingDouble(Employee::getSalary).reversed())

Java 程序设计语言它直接将所有元素转人一个数组,对数组进行排序,然后,再将排序后的序列复制回列表集合类库中使用的排序算法比快速排序要慢一些,快速排序是通用排序算法的传统选择。但是,归并排序有一个主要的优点:稳定, 即不需要交换相同的元素。
排序可以传递什么类型的列表呢?根据文档说明,列表必须是可修改的, 但不必是可以改变大小的

  • 如果列表支持 set 方法,则是可修改的。
  • 如果列表支持 add 和 remove 方法, 则是可改变大小的。

Collections 类有一个算法 shuffle, 其功能与排序刚好相反, 即随机地混排列表中元素的顺序。如果提供的列表没有实现 RandomAccess 接口,shuffle 方法将元素复制到数组中,然后打乱数组元素的顺序,最后再将打乱顺序后的元素复制回列表。

9.5.2 二分查找

集合必须是排好序的, 否则算法将返回错误的答案,如果集合没有采用 Comparable 接口的compareTo 方法进行排序, 就还要提供一个比较器对象。

9.5.4 批操作

很多操作会“ 成批” 复制或删除元素。以下调用

coll1.removeAll(coll2);

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

coll1.retainAll(coll2);

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

9.5.5 集合与数组的转换

如果需要把一个数组转换为集合,Arrays.asList 包装器可以达到这个目的。
从集合得到数组会更困难一些。当然,可以使用 toArray 方法。不过,这样做的结果是一个对象数组。尽管你知道集合中包含一个特定类型的对象,但
不能使用强制类型转换
实际上,必须使用toArray 方法的一个变体形式,提供一个所需类型而且长度为 0 的数组。

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

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

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

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

9.5.6 编写自己的算法

如果编写自己的算法(实际上,是以集合作为参数的任何方法,) 应该尽可能地使用接口,而不要使用具体的实现。
如果编写了一个返回集合的方法,可能还想要一个返回接口,而不是返回类的方法。

9.6 遗留的集合

阅读Java核心技术Ⅰ的笔记(Java基础、第九章、集合)_第13张图片

9.6.1 Hashtable 类

Hashtable 类与 HashMap 类的作用一样,实际上,它们拥有相同的接口。与 Vector 类的方法一样。Hashtable 的方法也是同步的。如果需要并发访问, 则要使用 ConcurrentHashMap。

9.6.2 枚举

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

9.6.3 属性映射

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

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

实现属性映射的 Java 平台类称为 Properties
如下创建一个Properties类的对象,并设置属性值写入文件。

public void test7(){
        Properties properties = new Properties();
        properties.setProperty("a","1");
        properties.setProperty("filepath","src/properties.txt");
        try {
            //src前面不需要带斜杠
            FileOutputStream fileOutputStream = new FileOutputStream("src/com/test.txt");
            properties.store(fileOutputStream,"test.txt");
        } catch (FileNotFoundException e) {
            Throwable throwable=new Throwable("找不到文件");
            throwable.initCause(e);
            throwable.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

输出
阅读Java核心技术Ⅰ的笔记(Java基础、第九章、集合)_第14张图片
读取一个属性文件

 public void test8(){
        try {
            Properties properties = new Properties();
            FileInputStream fileInputStream = new FileInputStream("src/com/test.txt");
            properties.load(fileInputStream);
            //get返回一个Object
            String s= (String) properties.get("a");
            //getProperty返回一个字符串
            String filepath = properties.getProperty("filepath");
            //如果没有这个键,就使用默认值zjx
            String name=properties.getProperty("name","zjx");
            System.out.println(s);
            System.out.println(filepath);
            System.out.println(name);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

9.6.4 栈

push 方法和 pop方法。

9.6.5 位集

Java 平台的 BitSet 类用于存放一个位序列。如果需要高效地存储位序列(例如,标志)就可以使用位集。

bucketOfBits.get(i)

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

bucketOfBits.set(i)

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

bucketOfBits.clear(i)

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

作为位集应用的一个示例,这里给出一个采用“ Eratosthenes 筛子” 算法查找素数的实现.是要遍历一个拥有 200 万个位的位集。首先将所有的位置为“ 开” 状态,然后,将已知素数的倍数所对应的位都置为“ 关” 状态。经过这个操作
保留下来的位对应的就是素数。

你可能感兴趣的:(java基础)