一、概述

散列算法有两个主要的实现方式:开散列和闭散列,HashMap采用开散列实现。

HashMap中,键值对(key-value)在内部是以Entry(HashMap中的静态内部类)实例的方式存储,散列表table是一个Entry数组,保存Entry实例。

对于冲突的情况,在开散列中,如果若干个entry计算得到相同散列地址(具体是由indexFor(hash(key.hashCode()),length)求得),这些entry被组织成一个链表,并以table[i]为头指针。

HashMap的数据结构大致可以用下图表示(以HashMap<String,String>的实例为例):

二、散列函数

HashMap采用简单的除法散列,其散列公式可表示为:

一般来讲,采用除法散列,m的值应该尽量避免某些特殊值,例如m不应该为2的幂。

如果m=2^p,那么h(k)的结果就是k的p个最低位,这样就会与k的比特位产生关联,更容易产生冲突,不能很好的保证散列函数的结果在[0...m-1]之间均匀分布。所以除非已知各种最低p为排列是等可能的,否则m选择应该考虑到关键字的所有位。

但是HashMap中提供了hash(int h)函数,这个函数以key.hashCode为参数,对其做进一步的处理,处理过程中较好的解决了以上的因素的影响。大致保证了每一个hashCode具有有限的冲突次数(通过移位运算和异或操作具体怎么达到这个目的?我也没有在深入去挖,感兴趣的同学可以来一起探讨学习下。。。)。

这样一来,某个key散列地址计算过程实际就是:

indexFor(hash(key.hashCode()),length)

可见,这里的hash(key.hashCode())结果相当于上面的散列公式中的k,lenght相当于m。

以下为hash(int h)和indexFor(int h, int length)源代码,更能说明问题:

    /**
* Applies a supplemental hash function to a given hashCode, which
* defends against poor quality hash functions. This is critical
* because HashMap uses power-of-two length hash tables, that
* otherwise encounter collisions for hashCodes that do not differ
* in lower bits. Note: Null keys always map to hash 0, thus index 0.
*/
static int hash(int h) {
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
} /**
* Returns index for hash code h.
*/
static int indexFor(int h, int length) {
return h & (length-1);
}

注意indexFor(int h, int length)的处理方式:

在length为2的幂的情况下,h & (length-1) 等效于h%length。这里length为table的长度,HashMap保证了无论是在初始化时还是在后续resize操作过程中,length都是2的幂。

三、冲突解决机制

在需要插入<key,value>键值对(内部对应插入Entry实例)时,执行put操作。

两个相同的key必然计算出相同的散列地址(相同的indexFor(hash, table.length)结果),HashMap中不接受相同的key,对原有的key进行put操作实际上是进行覆盖value的操作。

两个不同的key仍有可能计算出相同的散列地址(例如前例中key为"d"和"u"),此时产生冲突。

HashMap中的冲突解决机制比较简单,将这些冲突的entry节点以链表的方式挂靠到table[i]处。插入时以参数(hash, key, value, e)创建新的Entry实例,e就是位于table[i]处的链表的第一个entry节点,e作为新创建的entry的next元素,所以新创建的entry直接插入到了链表的头部充当新的头结点。

从源代码层面分析来看,put操作调用addEntry()方法,后者继续调用HashMap中静态内部类Entry<K,V>的构造函数。

    public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
} modCount++;
addEntry(hash, key, value, i);
return null;
} void addEntry(int hash, K key, V value, int bucketIndex) {
   Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
if (size++ >= threshold)
resize(2 * table.length);
} static class Entry<K,V> implements Map.Entry<K,V> {
  ...
/**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
  ... }

四、rehash

当键值对的数量>=设定的阀值(capacity * load factor(0.75))时,为保证HashMap的性能,会进行重散列(rehash)。

HashMap中,重散列主要有两步:1、扩充table长度。2、转移table中的entry,从旧table转移到新的table。

table长度以2倍的方式扩充,一直到最大长度2^30。

entry转移的过程是真正意义上的重散列,在此过程中,对原来的每个entry的key重新计算新的散列地址,旧table中相同位置的entry极有可能会被散列到新table中不同的位置,这主要是因为table的length变化的原因。

在源代码中主要涉及resize()和transfer()两个方法。

    void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
} Entry[] newTable = new Entry[newCapacity];
transfer(newTable);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
} /**
* Transfers all entries from current table to newTable.
*/
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;
do {
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}

五、一些总结

1、capacity(table数组长度)必须为2的幂,初始容量(initial capacity)默认为16。即使是以传入参数initialCapacity的方式构造实例(HashMap(int initialCapacity, float loadFactor)),构造过程中内部也会将capacity修整为与initialCapacity最接近并且不小于它的2的幂的数作为capacity来实例化。

2、装填因子loadFactor默认为0.75。

3、如果key为null,这始终会被散列到table[0]的桶中,即使是rehash的过程也是一样。非null的key也有可能会被散列到table[0]的位置,例如上图中key=“f”,而且相同的key在在不同的时间可能会被散列到不同的位置,这与rehash有关。

4、HashMap以链表的方式解决冲突,插入键值对(put操作)时,新增的entry会被插入到链表的头部,也就是会插入到table[i]的位置。

5、与其他集合类一样,由于fail-fast特性的存在,利用遍历器(Iterator)进行遍历操作时应该采用遍历器自身的方法进行结构化的修改(例如remove操作),不应采用其他方式对其数据内容进行修改。

HashMap中的散列函数、冲突解决机制和rehash的更多相关文章

  1. jQuery库(noConflict)冲突解决机制

    很多JSFramework库选择使用$符号作为一个函数或变量名,而在实际的项目开发,模板语言,则有可能"$"符号是模板语言keyword.例如Veclocity模板语言,$它是ke ...

  2. Map之HashMap的get与put流程,及hash冲突解决方式

    在java中HashMap作为一种Map的实现,在程序中我们经常会用到,在此记录下其中get与put的执行过程,以及其hash冲突的解决方式: HashMap在存储数据的时候是key-value的键值 ...

  3. HashMap的底层实现以及解决hash值冲突的方式

    class HashMap<K,V> extends AbstractMap<K,V> HashMap  put() HashMap  get() 1.put() HashMa ...

  4. .Net中DLL冲突解决(真假美猴王)

    <西游记>中真假美猴王让人着实难以区分,但是我们熟知了其中的细节也不难把他们剥去表象分别出来.对问题不太关心的可以直接调到文中关于.Net文件版本的介绍 问题 最近在编译AKKA.net ...

  5. Android中View类OnClickListener和DialogInterface类OnClickListener冲突解决办法

    Android中View类OnClickListener和DialogInterface类OnClickListener冲突解决办法 如下面所示,同时导入这两个,会提示其中一个与另一个产生冲突. 1i ...

  6. 在JSP中使用jQuery的冲突解决(收集整理)

    在JSP中使用<jsp:include page="somethingPage.jsp"></jsp>来嵌套页面的时候,会出现jQuery之间的冲突 解决办 ...

  7. Eclipse中Egit冲突解决

    Eclipse中Egit冲突解决 Git 作为进来最流行的分布式版本控制软件来说应用的十分广泛.EGit就是一款Eclipse上的Git插件.在使用Egit提交项目时,有时会产生冲突,需要对代码进行m ...

  8. Mybatis系列(二):优化MyBatis配置文件中的配置和解决字段名与实体类属性名不相同的冲突

    原文链接:http://www.cnblogs.com/xdp-gacl/p/4264301.html     http://www.cnblogs.com/xdp-gacl/p/4264425.ht ...

  9. Android 中 DrawerLayout + ViewPager 怎么解决滑动冲突?

    DrawerLayout 是 Android 官方的侧滑菜单控件,而 ViewPager 相信大家都很熟悉了.今天这里就讲一下当在 DrawerLayout 中嵌套 ViewPager 时,要如何解决 ...

随机推荐

  1. 那些年~~~我们的C#笔试内测题目

    <深入.NET平台和C#编程>内部测试题-笔试试卷 一 选择题 1) 以下关于序列化和反序列化的描述错误的是( C). a) 序列化是将对象的状态存储到特定存储介质中的过程 b) 二进制格 ...

  2. hql(Hibernate Query Language)

    1.Criteria查询对查询条件进行了面向对象封装,符合编程人员的思维方式,不过HQL(Hibernate Query Language)查询提供了更加丰富的和灵活的查询特性,因此Hibernate ...

  3. jquery mobile-按钮控件

    jQuery Mobile 中的按钮会自动获得样式,这增强了他们在移动设备上的交互性和可用性.我们推荐您使用 data-role="button" 的 <a> 元素来创 ...

  4. 安卓开发-intent在Activity之间数据传递

    安卓开发-intent在Activity之间数据传递 [TOC] intent实现普通跳转 使用intent的setclass方法,示例(由此界面跳转到NewActivity界面) //使用setOn ...

  5. pipeline结合GridSearchCV的一点小介绍

    clf = tree.DecisionTreeClassifier() ''' GridSearchCV search the best params ''' pipeline = Pipeline( ...

  6. npm install安装时忘记--save解决方法

    title: npm install安装时忘记--save解决方法 date: 2017-05-07 20:17:54 tags: npm categories: --- 网上还有一个解决方案就是: ...

  7. WebSphere--用户简要表

     Application Server 含有 com.ibm.servlet.personalization.userprofile 软件包中的类,这些类使维护关于 Web 站点访问者的持久信息和利用 ...

  8. Java常用类--处理日期

    Date Date类在java.util包中.使用Date类的无参数构造方法创建的对象可以获取本地当前时间.一般来说,也只使用这个.因为date的很多方法都已经不推荐使用了,所以Date的功能大大的消 ...

  9. 通俗易懂的分析如何用Python实现一只小爬虫,爬取拉勾网的职位信息

    源代码:https://github.com/nnngu/LagouSpider 效果预览 思路 1.首先我们打开拉勾网,并搜索"java",显示出来的职位信息就是我们的目标. 2 ...

  10. php中curl模拟post提交多维数组(转载)

    原文地址:http://www.cnblogs.com/mingaixin/archive/2012/11/09/2763265.html 今天需要用curl模拟post提交参数,请求同事提供的一个接 ...