存取之好 —— HashMap原理、源码、实践

HashMap是一种非常惯用的数据结构,作为一个施用开发人员,对其原理、兑现加剧了解有助于更高效率进展数据存取。正文所用的jdk版本为1.5。

施用HashMap

《Effective JAVA》中以为,99%的情况下,当你覆盖了equals步骤后,请必须覆盖hashCode步骤默许情况下,这两者会采取Object的“原生”兑现模式,即:


Java代码
.protected native int hashCode();
.public boolean equals(Object obj) {
3. return (this == obj);
4.}
protected native int hashCode();
public boolean equals(Object obj) {
return (this == obj);
}


hashCode步骤的定义用到了native关键字,示意它是由C或C++采取较为底层的形式兑现的,你可以以为回来了该对象的内存地址;而缺省equals则以为唯有当两者摘引同一个对象时,才以为它们是相等的。如其你只是覆盖了equals()而从新定义hashCode(),在读取HashMap的时分,除非你运用一个与你保留引述完全相同的对象作为key值,不然你将得不到该key所对应的值。

另一方面,你应当尽可能避免施用“可变”的作为HashMap的键。如若你将一个对象作为键值并保留在HashMap中,以后又改变了其状态,那么HashMap就会发生纷乱,你所保留的值或者丢掉虽然遍历聚合或许可以找出)。可参照http://www.ibm.com/developerworks/cn/java/j-jtp02183/

HashMap存取机制

Hashmap事实上是一个数组和链表的组合体,利用数组来模拟一个个桶(类似于Bucket Sort)以高速存取不同hashCode的key,至于雷同hashCode的不同key,再调用其equals步骤从List中提掏出和key所相对应的value。

JAVA中hashMap的初始化重要是为initialCapacity和loadFactor这两个属性赋值。前者示意hashMap顶用界别不同hash值的key空间长度,后者是指定了当hashMap中的元素超过剩少的时分,开始自动扩容,。默许情况下initialCapacity为16,loadFactor为0.75,它示意一开始hashMap可以寄存16个不同的hashCode,填充到第12个的时分,hashMap自动将其key空间的长度扩容到32,依此类推;这点可以从源码受看出来:


Java代码
.void addEntry(int hash, K key, V value, int bucketIndex) {
2. Entry e = table[bucketIndex];
3. table[bucketIndex] = new Entry(hash, key, value, e);
4. if (size++ >= threshold)
5. resize( * table.length);
6.}
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry e = table[bucketIndex];
table[bucketIndex] = new Entry(hash, key, value, e);
if (size++ >= threshold)
resize( * table.length);
}


而每当hashMap扩容后,内部的每个元素寄存的位置都市发生变化(由于元素的终极位置是其hashCode对key空间长度取模而得),因而resize步骤中又会调用transfer函数,用于重新分配内部的元素;这个历程变成rehash,是非常耗费性能的,因此在可预知元素的个数的情况下,通常应当避免运用缺省的initialCapacity,而是经过结构函数为其指定一个值。比如我们可能会想要将数据库查询所得1000条记要以某个特定字段(例如ID)为key缓存在hashMap中,为了提高效率、避免rehash,可以直接指定initialCapacity为2048。

另一个值得注意的地方是,hashMap其key空间的长度一定为2的N次方,这一点可以从一下子源码受看出来:


Java代码
.int capacity = ;
.while (capacity < initialCapacity)
3. capacity <<= ;
int capacity = ;
while (capacity < initialCapacity)
capacity <<= ;


即令我们在结构函数中拇指定的initialCapacity不是2的平方数,capacity仍是会被赋值为2的N次方。

为啥Sun Microsystem的高级工程师要将hashMap key空间的长度设为2的N次方呢?这边参照R.W.Floyed付出权衡散列思维的三个基准



一个好的hash算法的计算应该是是非非常快的
一个好的hash算法应该是摩擦极小化
如其存在摩擦,应该是摩擦匀称



为了将各元素的hashCode保留至长度为Length的key数组中,正常采取取模的形式,即index = hashCode % Length。不可逆转的,存在多个不同对象的hashCode被安置在同一位置,这便是我们平常所谓的“矛盾”。如若仅仅是思考元素匀称化与矛盾极小化,好似应当将Length取为素数(虽然显然的理论来支持这一点,但数学家们透过大量的实践得出结论,对素数取模的发生结果的无干性要大于其它数目字)。为此,Craig Larman and Rhett Guthrie《Java Performence》中对此也大加人身攻击。为了弄清楚这个问题,Bruce Eckel(Thinking in JAVA的笔者专门采访了java.util.hashMap的笔者Joshua Bloch,并将他采取这种设计的缘故放到了网上(http://www.roseindia.net/javatutorials/javahashmap.shtml) 。

如上设计的缘故取决于,取模演算在包括JAVA在内的多数语言中的效率都非常低下,而除数为2的N次方时,取模演算将退化作容易的位演算,其效率显然晋升依照Bruce Eckel付出的数据,大约可以提拔~8倍) 。见见JDK中是何以兑现的:


Java代码
.static int indexFor(int h, int length) {
2. return h & (length-);
3.}
static int indexFor(int h, int length) {
return h & (length-);
}


key空间长度为2的N次方时,计算hashCode为h的元素的目录可以用简略的与操作来顶替拙笨的取模操作!假想某个对象的hashCode为35(二进制为100011),而hashMap采取默许的initialCapacity(16),那么indexFor计算所得结果将会是100011 & 1111 = 11,即十进制的是否恰好是35 Mod 16。

上边步骤有一个问题,乃是它的计算结果仅有对象hashCode的低位决议,而要职通统障蔽了;之上面为例,19(10011)、35(100011)、67(1000011)等便具有雷同的结果。针对这个问题, Joshua Bloch采取了“防御性编程”的解决方法,在运用各对象的hashCode头里对其进展二次Hash,参阅JDK中的源码:


Java代码
.static int hash(Object x) {
2. int h = x.hashCode();
3. h += ~(h << );
4. h ^= (h >>> 14);
5. h += (h << );
6. h ^= (h >>> );
7. return h;
8. }
static int hash(Object x) {
int h = x.hashCode();
h += ~(h << );
h ^= (h >>> 14);
h += (h << );
h ^= (h >>> );
return h;
}


采取这种旋转Hash函数的重要目的是让原有hashCode的要职信息也能被充分利用,且统筹计算效率以及数据统计的特点,其具体的原理已超出了正文的领域。

加速Hash效率的另一个有效路径编纂很好的自定义对象的HashCode,String的兑现采取如次的计算方法:


Java代码
.for (int i = ; i < len; i++) {
.h = 31*h + val[off++];
3.}
.hash = h;
for (int i = ; i < len; i++) {
h = 31*h + val[off++];
}
hash = h;


这种步骤HashCode的计算方法或者最早出现在Brian W. Kernighan和Dennis M. Ritchie的《The C Programming Language》中,被认为是性价比最高的算法(又被称为times33算法,由于C中乘数恒量为33,JAVA中改成31),实质上,包括List在内的绝大多数的对象都是用这种步骤计算Hash值。

另一种比较非一般的hash算法号称布隆过滤器,它以牺牲微细精密度为代价,换来储存空间的大量节约惯用譬如判断用户名反复是不是在黑名单上上等等,可以参照李开复的数学之系列第13篇(http://googlechinablog.com/2006/08/blog-post.html)

Fail-Fast机制

家喻户晓,HashMap不是线程保险聚合。但在某些容错能力较好的运用中,如若不料单单由于%的可能性而去经受hashTable的同步费用,则可以思考利用一下HashMap的Fail-Fast机制,其具体兑现如次


Java代码
.Entry nextEntry() {
.if (modCount != expectedModCount)
3. throw new ConcurrentModificationException();
4. ……
5.}
Entry nextEntry() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
……
}


内中modCount为HashMap的一个范例变量,而且宣言为volatile,示意任何线程都可以看到该变量被其它线程批改的结果(依据JVM内存储器模型的优化,每一个线程都市存一份自个儿的工作内存储器,此工作内存储器的内容与本土内存储器并非时时刻刻都同步,因而可能会出现线程间的批改足见的问题) 。运用Iterator开始迭代时,会将modCount的赋值给expectedModCount,在迭代过程中,经过历次比较两者是不是相等来判断HashMap是不是在内部或被其它线程批改。HashMap的多数批改步骤都市改变ModCount,参照下边的源码:

Java代码
.public V put(K key, V value) {
2. K k = maskNull(key);
3. int hash = hash(k);
4. int i = indexFor(hash, table.length);
5. for (Entry e = table[i]; e != null; e = e.next) {
6. if (e.hash == hash && eq(k, e.key)) {
7. V oldValue = e.value;
8. e.value = value;
9. e.recordAccess(this);
10. return oldValue;
11. }
12. }
13. modCount++;
14. addEntry(hash, k, value, i);
15. return null;
16. }
public V put(K key, V value) {
K k = maskNull(key);
int hash = hash(k);
int i = indexFor(hash, table.length);
for (Entry e = table[i]; e != null; e = e.next) {
if (e.hash == hash && eq(k, e.key)) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, k, value, i);
return null;
}

以put步骤为例,历次HashMap中平添元素都市以致modCount自增。其它譬如remove、clear步骤也都包孕相仿的操作。
上头可以看出,HashMap所采取的Fail-Fast机制本质上是一种乐观锁机制,透过检察状态——没有问题则忽略——有问题则抛出异常的形式,来避免线程同步的花消下部付出一个在单线程环境发出生Fast-Fail的例证


Java代码
.class Test {
2. public static void main(String[] args) {
3. java.util.HashMap map=new java.util.HashMap();
4. map.put(new Object(), "a");
5. map.put(new Object(), "b");
6. java.util.Iteratorit=map.keySet().iterator();
7. while(it.hasNext()){
8. it.next();
9. map.put("", "");
10. System.out.println(map.size());
11. }
12.}
class Test {
public static void main(String[] args) {
java.util.HashMap map=new java.util.HashMap();
map.put(new Object(), "a");
map.put(new Object(), "b");
java.util.Iterator

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值