》》完整的容器分类法
#### 下面是集合类库图:
补充说明: 虚框表示 abstract 类。
》》填充容器
### 一种 Generator 解决方案
*** 事实上,所有的 Collection 子类型都有一个接收另一个 Collection 对象的构造器,用所
接收的 Collection 对象中的元素来填充新的容器。
**** 泛型便利方法可以减少使用类时所必需的类型检查数量。
**** 适配器模式
**** LinkedHashSet 维护的是保持了插入顺序的链接列表
### Map 生成器
***** 可以使用工具来创建任何用于 Map 或 Collection 的生成数据集,然后通过构造器
或 Map.putAll( ) 和 Collection.addAll( ) 方法来初始化 Map 和 Collection 。
### 使用 Abstract 类
**** 享元模式
%%% 你可以在普通的解决方案需要过多的对象,或者产生普通对象太占空间时
使用享元。
%%% 享元模式使得对象的一部分可以被具体化,因此,与对象中的所有事物都
包含在对象内部不同,我们可以在更加高效的外部表中查找对象的一部分或
整体(或者通过某些其他节省空间的计算来产生对象的一部分或整体)。
》》Collection 的功能方法
**** 如果想检查 Collection 中的元素,那就必须使用迭代器。
》》可选操作
****执行各种不同的添加和移除的方法在 Collection 接口中都是可选操作。这意味着实现类
并不需要为这些方法提供功能定义。
**** Java 容器类库的一个重要目标:容器应该易学易用。
**** 如果一个操作是未获支持的,那么在实现接口的时候可能就会导致 UnsupporedOperation-
Exception 异常,而不是将产品程序交给客户以后才出现此异常,这种情况是有道理的。
毕竟,它表示编程上有错误:使用了不正确的接口实现。
**** 值得注意的是,未获支持的操作只有在运行时才能探测到,因此它们表示动态类型检查。
### 未获支持的操作
**** 最常见的未获支持的操作,都来源于背后由固定尺寸的数据结构支持的容器。当你用
Arrays.asList( ) 将数组转换为 List 时,就会得到这样的容器。你还可以通过使用
Collections 类中 “ 不可修改”的方法,选择创建任何会抛出 UnsupporedOperationException
的容器(包括 Map)。
**** Arrays.asList( ) 会生成一个 List , 它基于一个固定大小的数组,仅支持那些不会改变数组
大小的操作。
**** 任何会引起底层数据结构的尺寸进行修改的方法都会产生一个 UnsupporedOperationException
异常,以表示对未获支持操作的调用(一个编程错误)。
**** " 常量 "容器对象
**** 对于将容器作为参数接受的方法,其文档应该指定哪些可选方法必须实现。
》》List 的功能方法
**** 基本的 List 很容易使用:大多数时候只是调用 add( ) 添加对象,使用 get( ) 一次取出一个元素,
以及调用 iterator( ) 获取用于该序列的 Iterator 。
》》Set 和 存储顺序
**** 当你创建自己的类型时,要意识到 Set 需要一种方式来维护存储顺序,而存储顺序如何维持,
则是在 Set 的不同实现之间会有所变化。因此,不同的 Set 实现不仅具有不同的行为,而且它
们对于可以在特定的 Set 中放置的元素的类型也有不同的要求:
%%% Set ( interface ) : 存入 Set 的每个元素都必须是唯一的,因为 Set 不保存重复的元素。
加入 Set 的元素必须定义 equals( ) 方法以确保对象的唯一性。
Set 和 Collection 有完全一样的接口。Set 接口不保证维护元素的
次序。
%%% HashSet : 为快速查找而设计的 Set 。存入 HashSet 的元素必须定义 hashCode( )
%%% TreeSet : 保证次序的 Set , 底层为树结构。使用它可以从 Set 中提取有序的序列。
元素必须实现 Comparable 接口。
%%% LinkedHashSet : 具有 HashSet 的查询速度,且内部使用链表维护元素的顺序(插入的次序)。
于是在使用迭代遍历 Set 时,结果会按元素插入的次序显示。元素也必须定义
hashCode( ) 方法。
**** 对于良好的编程风格而言,你应该在覆盖 equals( ) 方法时,总是同时覆盖 hashCode( )
方法。
**** 你通常希望 compareTo( ) 方法可以产生与 equals( ) 方法一致的自然排序。如果 equals( )
对于某个特定的比较产生 true , 那么 compareTo( ) 对于该比较应该返回 0 , 如果
equals( ) 对于某个比较产生 false , 那么 compareTo( ) 对于该比较应该返回非 0 值 。
**** LinkedHashSet 按照元素的插入顺序保存元素;
TreeSet 按照排序顺序维护元素(按照 compareTo( )的实现方式,这里维护的是降序 )
### SortedSet
**** SortedSet 中元素可以保证处于排序状态,这使得它可以通过在 SortedSet 接口中的下列
方法提供附加的功能:Comparator comparator( ) 返回当前 Set 使用的 Comparator ;
或者返回 null , 表示以自然方式排序。
**** 注意,SortedSet 的意思是 “ 按对象的比较函数对元素排序 ”,而不是指 “ 元素插入的次序 ‘。
插入顺序可以用 LinkedHashSet 来保存。
》》队列
**** Queue 在 Java SE5 中仅有的两个实现是 LinkedList 和 PriorityQueue ,它们的差异在于
排序行为而不是性能。
### 优先级队列
***** 优先级队列中每个对象都包含一个字符串和一个主要的以及次要的优先级值。排序顺序
也是通过实现 Comparable 而进行控制的。
### 双向队列
》》理解 Map
***** 映射表(也称为关联数组)的基本思想是它维护的是键---值(对)关联,因此你可以使用
键来查找值。
***** 标准的 Java 类库中包含了 Map 的几种基本实现,包括:
HashMap , TreeMap , LinkedHashMap , WeakHashMap , ConcurrentHashMap ,
IdentityHashMap 。
它们都有同样的基本接口 Map , 但是行为特性各不相同,这表现在效率 、 键值对的
保存及呈现次序、对象的保存周期、映射表如何在多线程程序中工作和判定 " 键 " 等价的
策略等方面。
***** 为了使用 get( ) 方法,你需要传递想要查找的 key , 然后它将与之相关联的值作为结果
返回,或者在找不到的情况下返回 null 。get( ) 方法使用的可能是能想象到的效率最差的方
式来定位值的:从数组的头部开始,使用 equals( ) 方法依次比较键。但这里的关键是简单性
而不是效率。
***** 性能
%%% 性能是映射表中的一个重要问题,当在 get( ) 中使用线性搜索时,执行速度会相当地慢,
而这正是 HashMap 提高速度的地方。
%%% HashMap 使用了特殊的值,称作散列码,来取代对键的缓慢搜索。
¥¥¥ 散列码是 “ 相对唯一”的、用以代表对象的 int 值 , 它是通过将该对象的某些
信息进行转换而生成的。
¥¥¥ hashCode( ) 是根类 Object 中的方法,因此,所有 Java 对象都能产生散列码。
%%% HashMap 就是使用对象的 hashCode( )进行快速查询的,此方法能够显著提高性能。
%%% HashMap
Map 基于散列表的实现(它取代了 Hashtable )。插入和查询“ 键值对 ” 的开销是
固定的。可以通过构造器来设置容量和负载因子,以调整容器的性能。
%%% LinkedHashMap
类似于 HashMap ,但是迭代遍历它时,取得 “ 键值对 ” 的顺序是其插入次序,
或者是最近最少使用(LRU)的次序。只比 HashMap 慢一点;而在迭代访问时反而
更快,因为它使用链表维护内部次序。
%%% TreeMap
基于红黑树的实现。查看 “ 键 ”或 “ 键值对 ” 时,它们会被排序(次序由 Comparable
或 Comparator 决定)。TreeMap 的特点在于,所得到的结果是经过排序的。 TreeMap
是唯一的带有 subMap( ) 方法的 Map ,它可以返回一个树。
%%% WeakHashMap
弱键映射,允许释放映射所指向的对象;这是解决某类特殊问题而设计的。如果
映射之外没有引用指向某个 “ 键 ” ,则此 “ 键 ” 可以被垃圾回收器回收。
%%% ConcurrentHashMap
一种线程安全的 Map ,它不涉及同步加锁。
%%% IdentityHashMap
使用 == 代替 equals( ) 对 “ 键 ” 进行比较的散列映射。专为解决特殊问题而设计的。
%%% 散列是映射中存储元素时最常用的方式。
%%% 任何键都必须具有一个 equals( ) 方法‘;如果键被用于散列 Map , 那么它还必须具有
恰当的 hashCode( ) 方法;如果键被用于 TreeMap , 那么它必须实现 Comparable 。
***** SortedMap
%%% 使用 SortedMap(TreeMap 是其现阶段的唯一实现),可以确保键处于排序状态。
***** LinkedHashMap
%%% 为了提高速度,LinkedHashMap 散列化所有的元素,但是在遍历键值对时,却
又以元素的插入顺序返回键值对。
此外,可以在构造器中设定 LinkedHashMap , 使之采用基于访问的最近最少使用
(LRU)算法,于是没有被访问过的(可被看作需要删除的)元素就会在队列的前面。对于
需要定期清理元素以节省空间的程序来说,此功能使得程序很容易得以实现。
》》散列与散列码
%%% Object 的 hashCode()方法生成散列码,默认的是使用对象的地址计算散列码。
%%% 在修改 hashCode( ) 方法的时候,同时你要覆盖 equals()方法。必须保证两者的输出结果一致。
%%% 正确的 equals() 方法必须满足下列 5 个条件:
一、自反性。对任意 x , x.equals(x) 一定返回 true 。
二、对称性。对任意 x 和 y ,如果 y.equals(x) 返回 true , 则 x.equals(y)也返回 true 。
三、传递性。对任意 x 、y 、z , 如果有 x.equals(y) 返回 true , y.equals(z) 返回 true ,则
则 x.equals(z) 一定返回 true 。
四 、 一致性。对任意 x 和 y , 如果对象中用于等价比较的信息没有改变,那么无论调用
x.equals(y) 多少次,返回的结果应该保持一致,要么一直是 true , 要么一直是 false 。
五、 对任何不是 null 的 x , x.equals(null) 一定返回 false 。
%%% 默认的 Object.equals( ) 只是比较对象的地址。
%%% 如果要使用自己的类作为 HashMap 键,必须同时重载 hashCode() 和 equals() 。
**** 理解 HashMap
%%% 如果不为你的键覆盖 hashCode() 和 equals() ,那么使用散列的数据结构(HashSet ,
HashMap , LinkedHashSet , LinkedHashMap)就无法正确处理你的键。然而,要
很好的解决此问题,你必须了解这些数据结构的内部构造。
%%% 使用散列的目的在于:想要使用一个对象来查找另一个对象。
**** 为速度而散列
%%% 散列的价值在于速度:散列使得查询得以快速进行。由于瓶颈位于键的查询速度,因此
解决方案之一就是保持键的排序状态,然后使用 Collections.binarySearch() 进行查询。
%%% 散列则更进一步,它将键保存在某处,以便能很快找到。存储一组元素最快的数据结构
是数组,所以使用它来表示键的信息(请小心留意,我是说键的信息,而不是键本身)。
%%% 数组并不保存键本身。而是通过键对象生成一个数字,将其作为数组的下标。这个数字
就是散列码,由定义在 Object 中的、且可能由你的类覆盖的 hashCode( ) 方法(在计算机
科学的术语中称为散列函数)生成。
%%% 为了解决数组容量被固定的问题,不同的键可以产生相同的下标。也就是说,可能会
有冲突。因此,数组多大就不重要了,任何键总能在数组中找到它的位置。
%%% 查询一个值得过程首先是计算散列码,然后使用散列码查询数组。如果能够保证没有
冲突(如果值得数量是固定的,那么就有可能),那可就有一个完美的散列函数,但是
情况只是特例。
通常,冲突由外部链接处理:数组并不直接保存值,而是保存值得 list 。然后对 list
中的值使用 equals( ) 方法进行线性的查询。这部分的查询自然会比较慢,但是,如果
散列函数好的话,数组的每个位置就只有较少的值。因此,不是查询整个 list ,而是
快速的跳到某个位置,只对很少的元素进行比较。这便是 HashMap 会如此快的原因。
%%% 由于散列表中的“槽位(slot)” 通常称为“桶位(bucket)”,因此我们将表示实际散列表
的数组命名为 bucket 。
为使得散列均匀,桶的数量通常使用质数。
注意,为了能够自动处理冲突,使用了一个 LinkedList 的数组,每一个新的元素只是
直接添加到 list 末尾的某个特定桶位中。即使 Java 不允许你创建泛型数组,那你也可以
创建指向这种数组的引用。
**** 覆盖 HashCode( )
%%% bucket 数组的下标值得产生,依赖于具体的 HashMap 对象的容量,而容量的改变与容器
充满程度和负载因子有关。hashCode() 生成的结果,经过处理后成为桶位的下标。
%%% 设计 hashCode( ) 时重要的因素就是:无论何时,对同一个对象调用 hashCode( ) 都应该
产生同样的值。如果在将一个对象用 put( ) 添加进 hashMap( ) 时产生一个 hashCode( ) 值,
而用 get( ) 取出时却产生了另一个 hashCode( ) 值,那么就无法重新取得该对象了。
所以,如果你的 hashCode( ) 方法依赖于对象中易变的数据,用户就要当心了,因为此
数据发生变化时, hashCode( ) 就会生成一个不同的散列码,相当于产生了一个不同的键。
此外,也不应该使 hashCode( ) 依赖于具有唯一性的对象信息,尤其是使用 this 的值,
这只能产生很糟糕的 hashCode( ) 。因为这样做无法生成一个新的键,使之与 pu() 中原始
的键值对中的键相同。
%%% 要想使得 hashCode( ) 实用,它必须速度快,并且必须有意义。也就是说,它必须基于对象
的内容生成散列码。散列码不必是独一无二的(应该更关注生成速度,而不是唯一性),但是
通过 hashCode( ) 和 equals( ) ,必须能够完全确定对象的身份。
%%% 散列码的生成范围并不重要,只要是 int 即可。
%%% 好的 hashCode() 应该产生分布均匀的散列码。如果散列码都集中在一块,那么 HashMap
或者 HashSet 在某些区域的负载会很重,这样就不如分布均匀的散列函数块。
%%% 为新类编写正确的 hashCode( ) 和 equals( ) 是很需要技巧的。
》》选择接口的不同实现
%%% 尽管实际上有四种容器:Map 、 List 、Set 和 Queue ,但是每种接口
都有不止一个实现版本。
%%% 在Java 类库中不同类型的 Queue 只在它们接受和产生数值的方式上有所差异。
%%% 容器之间的区别通常归结为由什么在背后“支持”它们。也就是说,所使用的接口
是由一个什么样的数据结构实现的。
例如,因为 ArrayList 和 LinkedList 都实现了 List 接口,所以无论选择哪一个,
基本的 List 操作都是相同的。然而,ArrayList 底层由数组支持;而 LinkedList 是由
双向链表实现的,其中每个对象包含数据的同时还包含指向链表中前一个和后一个元素
的引用。
%%% Set 可被实现为 TreeSet 、 HashSet 或者 LinkedHashSet 。每一种都有不同的行为:
*** HashSet 是最常用的,查询速度最快的;
*** LinkedHashSet 保持元素插入的次序;
*** TreeSet 基于 TreeMap ,生成一个总是处于排序状态的 Set
%%% 有时某个特定容器的不同实现会拥有一些共同的操作,但是这些操作的性能却并不
相同。在这种情况下,你可以基于使用某个特定操作的频率,以及你需要的执行速度来在它们
中间进行选择。对于类似的情况, 一种查看容器实现之间的差异的方式是使用性能测试。
**** 性能测试框架
%%% 为了防止代码重复以及提供测试的一致性,将测试过程的基本功能放置到一个测试
框架中。
%%% 建立一个基类,从中可以创建一个匿名内部类列表,其中每个匿名内部类都用于每种
不同的测试,它们每个都被当作测试过程的一部分而被调用。这种方式使得你可以很方便地
添加或移除新的测试种类。
**** 对 List 的选择
%%% 每个测试都需要仔细地考虑,以确保可以产生有意义的结果。
%%% 注意,对于每个测试,你都必须准确地计算将要发生的操作的数量以及从 test( ) 种
返回的值,因此计时是正确的。
**** 微基准测试的危险
%%% 在编写所谓的微基准从测试时,你必须要当心,不能做太多的假设,并且要将你的测试
窄化,以使得它们尽可能地只在感兴趣的事项上花费精力。你还必须仔细地确保你的测试运行
足够长的时间,以产生有意义的数据,并且要考虑到某些 Java HotSpot 技术只有在程序运行
一段时间之后才会踢爆问题(这对于短期运行的程序来说也很重要)。
%%%剖析器可以把性能分析工作做得比你好。Java 提供了一个剖析器,另外还有很多第三方
的自由/开源的和常用的剖析器可用。
**** 对 Set 的选择
%%% HashSet 的性能基本上总是比 TreeSet 好,特别是在添加和查询元素时,而这两个操作
也是最重要的操作。
%%% TreeSet 存在的唯一原因是它可以维持元素的排序顺序;所以,只有当需要有一个排好序
的 Set 时,才应该使用 TreeSet 。因为其内部结构支持排序,并且因为迭代是我们更有可能执行
的操作,所以,用 TreeSet 迭代通常比用 HashSet 要快。
**** 对 Map 的选择
%%%除了 IdentityHashMap ,所有的 Map 实现的插入操作都会随着 Map 尺寸的变大而明显变
慢。但是,查找的代价通常比插入要小得多,这是个好消息。因为我们执行查找元素的操作要比
执行插入元素的操作要多得多。
%%% Hashable 的性能大体上与 HashMap 相当。因为 HashMap 是用来替代 Hashable 的,因此
它们使用了相同的底层存储和查找机制。
%%% TreeMap 通常比 HashMap 要慢。TreeMap 是一种创建有序列表的方式。
树的行为是:总是保证有序,并且不必进行特殊的排序。
%%% LinkedHashMap 在插入时比 HashMap 慢一点,因为它维护散列数据结构的同时还要维护
链表(以保持插入顺序)。正是由于这个列表,使得其迭代速度更快。
》》实用方法
%%% Java 中有大量用于容器的卓越的使用方法,它们被表示为 java.util.Collections 类内部的
静态方法。
**** List 的排序和查询
%%% List 排序与查询所使用的方法与对象数组所使用的相应方法有相同的名字与语法,只是用
Collections 的 static 方法代替 Arrays 的方法而已。
%%% 与使用数组进行查找和排序一样,如果使用 Comparator 进行排序,那么 binarySearch()
必须使用相同的 Comparator 。
**** 设定 Collection 或 Map 为不可修改
%%% 无论哪一种情况,在将容器设为只读之前,必须填入有意义的数据。装载数据后,就应该
使用“ 不可修改的 ” 方法返回的引用去替换掉原本的引用。这样,就不必担心无意中修改了只读
的内容。
**** Collection 或 Map 的同步控制
%%% 关键字 synchronized 是多线程议题中的重要部分。
%%% Collections 类有办法能够自动同步整个容器。
%%% 快速报错
》》 Java容器有一种保护机制,能够防止多个进程同时修改同一个容器的内容。
》》 Java 容器类库采用“ 快速报错 ” 机制。它会探查容器上的任何除了你的进程
所进行的操作以外的所有变化,一旦它发现其他进程修改了容器,就会立刻抛出
ConcurrentModificationException 异常。这就是“ 快速报错 ” 的意思,即,不是使用
复杂的算法在事后来检查问题。
》》持有引用
%%% java.lang.ref 类库包含了一些组类,这些类为垃圾回收提供了更大的灵活性。当存在
可能会耗尽内存的大对象的时候,这些类显得特别有用。
有三个继承自抽象类 Reference 的类: SoftReference 、 WeakReference 和
PlantomReference 。
当垃圾回收器正在考察的对象只能通过某个 Reference 对象才 “ 可获得 ” 时,上述
这些不同的派生类为垃圾回收器提供了不同级别的间接性指示。
%%% 对象是可获得的,是指此对象可在程序中的某处找到。这意味着你在栈中有一个
普通的引用,而它正指向此对象;也可能是你的引用指向某个对象,而那个对象含有另
一个指向正在讨论的对象;也可能有更多的中间链接。如果一个对象是“ 可获得的 ” ,
垃圾回收器就不会释放它,因为它仍然为你的程序所用。如果一个对象不是“ 可获得的 ”
那么你的程序将无法使用到它,所以将其回收是安全的。
%%% 如果想继续持有某个对象的引用,希望以后还能够访问到该对象,但是也希望能够
允许垃圾回收器释放它,这时就应该使用 Reference 对象。这样,你可以继续使用该对象,
而在内存消耗殆尽的时候又允许释放该对象。
%%% SoftReference 、 WeakReference 和 PlantomReference 由强到弱排列,对应不同
级别的 “ 可获得性 ”。
SoftReference 用以实现内存敏感的高速缓存;
WeakReference 是为实现“规范映射”而设计的,它不妨碍垃圾回收器回收映射的
“ 键 ”(或“ 值 ”) 。“ 规范映射 ”中对象的实例可以在程序的多处被
同时使用,以节省存储空间。
PlantomReference 用以调度回收前的清理工作,它比 Java 终止机制更灵活。
%%% 使用 SoftReference 和 WeakReference 时,可以选择是否要将它们放入
ReferenceQueue (用作“ 回收前清理工作 ”的工具)。而 PlantomReference 只能依赖于
ReferenceQueue 。
%%% 补充: ReferenceQueue 总是生成一个包含 null 对象的 Reference 。
**** WeakHashMap
%%% 容器类中有一种特殊的 Map ,即 WeakHashMap ,它被用来保存 WeakReference 。
它使得规范映射更易于使用。在这种映射中,每个值只保存一份实例以节省存储空间。当
程序需要那个“ 值 ” 的时候,便在映射中查询现有的对象,然后使用它(而不是重新再创建)。
映射可将值作为初始化中的一部分,不过通常是在需要的时候才生成“ 值 ” 。
%%% 这是一种节约存储空间的技术,因为 WeakHashMap 允许垃圾回收器自动清理键和
值,所以它显得十分便利。对于向 WeakHashMap 添加键和值得操作,则没有什么特殊要求。
映射会自动使用 WeakReference 包装它们。允许清理元素的触发条件是,不再需要此键了。
》》Java 1.0 / 1.1 的容器
****Vector 和 Enumeration
**** Hashtable
**** Stack
**** BitSet
%%% 如果想要高效率地存储大量“ 开 / 关 ” 信息, BitSet 是很好的选择。不过它的效率仅是
对空间而言;如果需要高效的访问时间, BitSet 比本地数组稍慢一点。
%%% BitSet 的最小容量是 long : 64位。如果存储的内容比较小:例如 8位,那么 BitSet
就浪费了一些空间。因此如果空间对你很重要,最好撰写自己的类,或者直接采用数组来存储
你的标志信息(只有在创建包含开关信息列表的大量对象,并且促使你做出决定的依据仅仅是
性能和其他度量因素时,才属于这种情况。如果你做出这个决定只是你认为某些对象太大了,
那么你最终会产生不需要的复杂性,并会浪费掉大量的时间)。
%%% 普通的容器都会随着元素的加入而扩充其容量,BitSet 也是。
》》总结
%%% 容器类库对于面向对象语言来说是最重要的类库。大多数编程工作对容器的使用比对
其他类库中的构件都要多。
%%% 你必须对散列操作足够的了解,从而能够编写自己的 hashCode() 方法(并且你必须
知道何时需要这么做),你还必须对各种不同的容器实现有足够的了解,这样才能够为你的
需要进行恰当的选择。