第17章容器深入研究¶
1.完整的容器分类法¶
2.填充容器¶
与Arrays版本一样,此fill()方法也是只复制同一个对象引用来填充整个容器的,并且只对List对象有用,但是所产生的列表可以传递给构造器或addAll()方法:
这个示例展示了两种用对单个对象的引用来填充Collection的方式,第一种是使用Collection.nCopies()创建传递给构造器的List,这里填充的是ArrayList。第二种使用的fill()方法的用处更有限,因为它只能替换已经在List中存在的元素,而不能添加新的元素。
2.1 一种Generator解决方案¶
这些元素的顺序与它们的插入顺序相同,因为LinkedHashSet维护的是保持了插入顺序的链接列表。
2.2 Map生成器¶
定义第一个Pair类。
2.3 使用Abstract类¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 | |
3.Collection的功能方法¶
Collection所有操作:
- boolean add\(T\)
- boolean addAll\(Collection<? extends T>\)
- void clear()
- boolean contains\(T\)
- boolean isEmpty()
- Iterator iterator()
- boolean remove\(Object\)
- boolean removeAll\(Collection<?>\)
- boolean retainAll\(Collection<?>\):交集
- int size()
- Object[] toArray():
- T[] toArray\(T\[\] a\)
4.可选操作¶
最常见的未获支持的操作,都来源于背后由固定尺寸的数据结构支持的容器。当你用Arrays.asList()将数组转换为List时,就会得到这样的容器。
把Arrays.asList()的结果作为构造器的参数传递给任何Collection,这样可以生成允许使用所有的方法的普通容器
5.List的功能方法¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | |
6.Set和存储顺序¶
- Set\(interface\):存入
Set的每个元素都必须是唯一的,因为Set不保存重复元素,加入Set的元素必须定义equals()方法以确保对象的唯一性。Set与Collection有完全一样的接口。Set接口不保证维护元素的次序。 - HashSet:为快速查找而设计的
Set,存入HashSet的元素必须定义hasCode()。 - TreeSet:保持次序的Set,底层为树结构,使用它可以从
Set中提取有序的序列,元素必须实现Comparable接口。 - LinkedHashSet:具有
HashSet的查询速度,且内部使用链表维护元素的顺序。于是在使用迭代器遍历Set时,结果会按元素插入的次序显示,元素也必须定义hashCode()方法。
你必须为散列存储和树型存储都创建一个equals()方法。但是hashCode()只有在这个类将会被置于HashSet或者LinkedHashSet中时才是必须的。
对于没有重新定义hashCode()方法的SetType或TreeType,如果将它们放置到任何散列实现中都会产生重复值,这样就违反了Set的基本契约。如果尝试着在TreeSet中使用没有实现Comparable的类型,将会抛出一个异常。
6.1 SortedSet¶
- Object first():返回容器中的第一个元素
- Object last():返回容器中的最末一个元素
- Sorted subSet\(fromElement,toElement\):生成此
Set的子集,范围从fromElement(包含)到toElement(不包含)。 - Sorted headSet\(toElement\):生成此
Set的子集,由小于toElement的元素组成。 - Sorted tailSet\(fromElement\):生成此
Set的子集,由大于或等于fromElement的元素组成。
7.队列¶
除了优先级队列,Queue将精确地按照元素被置于Queue中的顺序产生它们。
7.1 优先级队列¶
7.2双向队列¶
在LinkedList中包含支持双向队列的方法,但是在Java标准类库中没有任何显式的用于双向队列的接口(本书基于JavaSE5,JavaSE6增加了Deque的接口)。
8.Map¶
8.1 性能¶
- HashMap:
Map基于散列表的实现(它取代了Hashtable),插入和查询键值对的开销是固定的。可以通过构造器设置容量和负载因子,以调整容器的性能。 - LinkedHashMap:类似于
HashMap,但是迭代遍历的顺序是插入次序,或者是**最近最少使用(LRU)**的次序。只比HashMap慢一点,而在迭代访问时反而更快,因为它使用链表维护内部次序。 - TreeMap:基于红黑树的实现。查看“键”或“键值对”时,它们会被排序(次序由Comparable或Comparator决定)。
TreeMap的特点在于,所得到的结果是经过排序的。TreeMap是唯一的带有subMap()方法的Map,它可以返回一个子树。 - WeakHashMap:
弱键(weak key)映射,允许释放映射所指向的对象,这是为解决某类特殊问题而设计的。如果映射之外没有引用指向某个“键”,则次“键”可以被垃圾收集器回收。 - ConcurrentHashMap:一种线程安全的
Map,它不涉及同步加锁。 - IdentityHashMap:使用==替代
equals()对“键”进行比较的散列映射。专为解决特殊问题而设计的。
任何键都必须具有一个equals()方法;如果键被用于散列Map,那么它必须还具有恰当的hashCode()方法;如果键被用于TreeMap,那么它必须实现Comparable。
8.2 SortedMap¶
使用SortedMap,可以确保键处于排序状态。
- Comparator comparator():返回当前
Map使用的Comparator;或者返回null,表示以自然方式排序。 - T firstKey():返回Map中的第一个键。
- T lastKey():返回Map中最末一个键。
- SortedMap subMap\(fromKey,toKey\):生成此Map的子集,范围由
fromKey到toKey的键确定。 - SortedMap headMap\(toKey\):由键小于
toKey的所有键值对组成。 - SortedMap tailMap\(fromKey\):由键大于或等于
fromKey的所有键值对组成。
8.3 LinkedHashMap¶
LinkedHashMap散列化所有的元素,但是在遍历键值对时,却又以元素的插入顺序返回键值对。此外,可以在构造器中设定LinkedHashMap,使之采用基于访问的最近最少使用(LRU)算法,于是没有被访问过的元素就会出现在队列的前面。
9.散列和散列码¶
正确的equals()方法必须满足下列5个条件: 1. 自反性。对任意x,x.equals\(x\)一定返回true。 2. 对称性。对任意x和y,如果y.equals(x)返回true,则x.equals(y)也返回true。 3. 传递性。对任意x、y、z,如果有x.equals(y)返回true,则x.equals(z)一定返回true。 4. 一致性。对任意x和y,如果对象中用于等价比较的信息没有改变,那么无论调用x.equals(y)多少次,返回的结果应该保持一致,要么一直是true,要么一直是false。 5. 对任何不是null的x,x.equals(null)一定返回false。
同时重载hashCode()和equals():
9.1 理解hashCode()¶
9.2 为速度而散列¶
散列将键保存在某处,以便能够很快找到。存储一组元素最快的数据结构是数组,所以使用它来表示键的信息。数组并不保存键本来。而是通过键对象生成一个数字,将其作为数组的下标。这个数字就是散列码,由定义在Object中的,且可能由你的类覆盖的hashCode()方法生成。
为解决数组容量被固定的问题,不同的键可以产生相同的下标。也就是说,可能会有冲突。因此,数组多大就不重要了,任何键总能在数组中找到它的位置。
于是查询一个值的过程首先就是计算散列码,然后使用散列码查询数组。如果能够保证没有冲突,那可就有一个完美的散列函数,但是这种情况只是特例。通常,冲突由外部链接处理:数组并不直接保存值,而是保存值的list。然后对list中的值使用equals()方法进行线性的查询。这部分的查询自然比较慢,但是,如果散列函数好的话,数组的每个位置就只有较少的值。因此,不是查询整个list,而是快速地跳到数组的某个位置,只对很少的元素进行比较。这便是HashMap会如此快的原因。
由于散列表中“槽位”通常称为桶位(bucket),因此我们将表示实际散列表的数组命名为bucket,为使散列分布均匀,桶的数量通常使用质数。注意,为了能够自动处理冲突,使用一个LinkedList的数组;每一个新的元素只是直接添加到list末尾的某个特定桶位中。
9.3 覆盖hashCode()¶
首先,你无法控制bucket数组的下标值的产生。这个值依赖于具体的HashMap对象的容量,而容量的改变与容器的充满程度和负载因子有关。hashCode()生成的结果,经过处理后成为桶位的下标。
设计hashCode()时最重要的因素就是:无论何时,对同一个对象调用hashCode()都应该生成同样的值。如果将一个对象用put()添加进HashMap时产生一个hashCode()值,而用get()取出时却产生了另一个hashCode值,那么聚无法重新取得该对象了。所以,如果你的hashCode()方法依赖于对象中易变的数据,用户就要当心了,因为此数据变化时,hashCode()就会生成一个不同的散列码,相当于产生了一个不同的键。
此外,也不应该使hashCode()依赖于具有唯一性的对象信息,尤其是使用this的值,这只能产生很糟糕的hashCode()。因为这样无法生成一个新的键,使之与put()中原始的键值对中的键相同。
Effective Java 为怎样写出一份像样hashCode()给出了基本的指导: 1.给int变量result赋予某个非零值常量,例如17 2.为对象内每个有意义的域f(即每个可以做equals()操作的域)计算出一个int散列码c:
| 域类型 | 计算 | | boolean | c=\(f?0:1\) | | byte、char、short或int | c=\(int\)f | | long | c=\(int\)\(f^f>>>32\) | | float | c=Float.floatToIntBits\(f\) | | double | long l= Double.doubleToLongBits();c=\(int\)\(l^\(l>>>32\)) | | Object,其equals()调用这个域的equals() | c=f.hashCode() | | 数组 | 对每个元素应用上述规则 |
3.合并计算得到的散列码
result = 37*result+c;
4.返回result. 5.检查hashCode()最后生成的结果,确保相同的对象有相同的散列码。

