深入了解HashMap(1)
来源:互联网 发布:志鸿优化答案查询 编辑:程序博客网 时间:2024/06/06 06:06
- hashCode 的生成
- hashCode 最佳实现方式
- String中的Hashcode方法
- HashMap
- 构造函数
- put方法解析
- hash 值的确定
- HashMap中 table 长度
- index 转换
- get 方法解析
- HashMap 与 HashTable 的区别
- hashCode 的生成
小序: 这个是深入理解HashMap 的第一篇, 因为小生不擅长多线程方面的分析. 所以在这片博客中不涉及HashMap 多线程的讨论, 待小生深入理解了<< java并发>> 再来献丑也不迟.
小生是一名初学者, 如果文中有不准确的地方, 还望诸位前辈多多包涵, 批评指正.
hashCode 的生成
Hashcode 在基于 key-value的集合如:HashMap 相关类中扮演很重要的角色。此外在 HashSet 集合中也会运用到,使用合适的hashcode方法检索的时间复杂度, 在最好情况下是
一个差劲的 hashCode 算法不仅会降低基于哈希集合的性能,而且会导致异常结果。Java应用中有多种不同的方式来生成 hashCode。
hashCode 最佳实现方式
Josh Bloch 在他的《Effective Java》告诉我们重写hashcode方法的最佳实践方式。
一个好的 hashcode 方法通常最好是不相等的对象产生不相等的hash值,理想情况下,hashcode方法应该把集合中不相等的实例均匀分布到所有可能的hash值上面。下面是具体做法.
1.把某个非
2.对于对象中的每个域 f
,做如下操作, 为该域计算 int 类型的哈希值 c::
- 如果该域是 boolean 类型,则计算(f ? 1 : 0)
- 如果该域是 byte、char、short 或者 int 类型,则计算(int)f
- 如果该域是long类型,则计算 (int)(f ^ (f>>>32))
- 如果该域是float类型,则计算Float.floatToIntBits(f)
- 如果该域是double类型,则计算Double.doubleToLongBits(f)
,然后重复第三个步骤。
- 如果该域是一个对象引用,并且该类的 equals
方法通过递归调用 equals
方法来比较这个域,同样为这个域递归的调用 hashCode
,如果这个域为null,则返回0。
- 如果该域是数组,则要把每一个元素当作单独的域来处理,递归的运用上述规则,如果数组域中的每个元素都很重要,那么可以使用 Arrays.hashCode
方法。
把上面每一次计算得到的hash值c合并到result中
// result 初始化为一个非零值. result = 31 * result + c
String中的Hashcode方法
String
的 hashcode
的算法就充分利用了字符串内部字符数组的所有字符。生成 hashCode 的算法的在 string
类中看起来像如下所示:
public int hashCode() { int h = hash; if (h == 0 && count > 0) { for (int i = 0; i < count; i++) { h = 31 * h + charAt(i); } hash = h; } return h;}
这里的 s
是指该字符串, n
是指字符串的长度.
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
至于为什么要用这种方式. 《Effective Java》中也做出了相应的解释.
The value 31 was chosen because it is an odd prime. If it were even and the multiplication overflowed, information would be lost, as multiplication by 2 is equivalent to shifting. The advantage of using a prime is less clear, but it is traditional. A nice property of 31 is that the multiplication can be replaced by a shift and a subtraction for better performance: 31 * i == (i << 5) - i. Modern VMs do this sort of optimization automatically.
大意是: 选 31 是因为它是一个奇素数, 使用它的时候优点还是不清楚,但是大家走这么干,31 的乘法可以用位运算和减法代替。例如31 * i相当于是 i 左移 5 位减去 i,即
HashMap
对于HashMap
大家一定都不陌生,不管是高级的, 还是初级的程序员基本上使用过。很多公司面试的时候都会聊起,既然HashMap
这么重要,今天我们就一起谈谈这个牛逼的数据结构.
先来看Java 语言中 HashMap 的数据结构, 有图有真相. (图片来自网络, 不知道原创是谁, 但是还是说一下, 对读者的尊重.)
可以看出 HashMap 就是一个数组加一组链表, 互相取长补短, 提高效率(如果不知道数组与链表的优缺点, 直接去面壁就好).
再来一张更加直观的图, 体现了 Entry 的存在, 一个 Entry 封装了 hash
, key
, value
, next
. 不过在 jdk1.8 中, 命名为 Node, 观其大略, 不必纠结于细枝末节.
接下来说一点专(zhuang)业(bi)的东西.
简单地说,HashMap 的 key 做 hash 算法,并将 hash 值映射到内存地址,直接取得/写入 key 对应的 value。 HashMap 大体上就做这样一件事.
HashMap的高性能需要保证以下几点(这些 Sun 公司的大神们都帮我们搞定了, 我们只需看看大神们是怎么做到的, 观其大略即可 = - ):
- key hash 的算法必须是高效.
- hash 值映射到内存地址(数组索引)的算法是快速的.
- 根据内存地址(数组索引)可以直接取得对应的值.
构造函数.
HashMap 提供了四个构造函数, 不过没有必要纠结于实现, 因为不同的 jar 版本具体细节实现相差较大, 但都是对 loadFactor
以及 table
这两个属性初始化. 这里以 Android API 25 中的 HashMap 为例. (晚生是做 Android 开发的, 还请大家谅解)
/** * The load factor for the hash table. */final float loadFactor; /** * The table, resized as necessary. Length MUST Always be a power of two. */transient HashMapEntry<K,V>[] table = (HashMapEntry<K,V>[]) EMPTY_TABLE;// 就贴一个构造函数吧. 实在是没有什么必要, 如果需要自己去看源码吧, 不同的 jar, 不同的 api, 实现都不同./** * Constructs an empty <tt>HashMap</tt> with the default initial capacity * (16) and the default load factor (0.75). */public HashMap() { this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); // 分别为4, 0.75}
1.loadFactor
加载因子.
loadFactor,即散列表的实际元素数目(n)/ 散列表的容量(m)。另外,laodFactor 越大,存储长度越小,查询时间越长。loadFactor 越小,存储长度越大,查询时间短。hashmap默认的是 0.75. 负载因子衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。对于使用链表法的散列表来说,查找一个元素的平均时间是
2.capacity
表的长度(也称桶容量).
注释中写的比较明白, 数组的长度必须是
3.threshold
表容量上限, 当元素的个数大于该数值时, 扩充容量. 多数情况下等于 capacity * loadFactor. (观其大略即可.)
put()
方法解析
在说put(key, value)
方法之前, 我们先了解下与 key
对象相关的几个值的概念.
- hashCode. Key 对象的 hashCode.
- hash. hash 值, 通过 hashCode 映射得到.
- index. HashMap 中数组的下标, 通过 hash 值取余运算得到.
了解了这几个概念之后, 我们来看一下put() 方法
public V put(K key, V value) { //确定表的容量, 首次添加元素时候会调用. if (table == EMPTY_TABLE) { inflateTable(threshold); } if (key == null) return putForNullKey(value); // 获取key 对象的 hash 值. hash 值不同的jdk, sdk 版本会有不同的实现, 通过一次哈希函数, 让散列值更加散列, 我们只需要了解这个就足够了. int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key); // 通过 hash值, 获取相应的索引. int i = indexFor(hash, table.length); // 是否存在相同元素, 如果相同替换. // 如果hash值相同, 但是 key 不同, 则出现 hash 冲突. 也就是说, 不同的对象出现相同的 hash值. for (HashMapEntry<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++; // 添加新的元素, 解决 hash 冲突. addEntry(hash, key, value, i); return null;}
hash 值的确定.
先来聚焦一下这个hash值获取的方法:
// 获取key 对象的 hash 值.// 不过不可见. int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);
用 key
的hashCode , 通过某一个 hash 函数做映射, 来确定 hash值. 不同的版本用的 hash 函数不同, 这里不做深究, 随便找两个栗子:
// 这个是 api 15 的 HashMap, api 25 的不可见= =private static int secondaryHash(int h) { // Doug Lea's supplemental hash function // 通过多次异或运算来实现该版本的 hash 函数. h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4);}
// 这个是选自 api 21 的 hash函数. private static int secondaryHash(int h) { // Spread bits to regularize both segment and index locations, // using variant of single-word Wang/Jenkins hash. h += (h << 15) ^ 0xffffcd7d; h ^= (h >>> 10); h += (h << 3); h ^= (h >>> 6); h += (h << 2) + (h << 14); return h ^ (h >>> 16);}
来看一下官方的解释.
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 or upper bits.
大意是: 为了进一步防止hash冲突. = =
总结一下put() 方法的整个过程: 先通过 key 的 hashCode 做一次 hash 函数, 求出 key 的 hash值, 然后用hash值 求出 hashIndex(这个是桶的index), 找到对应的位置, 遍历该位置对应的 Entry 链表, 如果找到 hash值 相同的 Entry, 那么替换 value; 如果找不到, 那么就添加一个新的 Entry.
NOTE: 这个put() 的过程不包括当size > threshold
时, 扩展桶容量的过程.
HashMap中 table 长度
接下来我们聚焦这一部分代码 (api 25) 的实现 :
//确定表的容量, 首次添加元素时候会调用.if (table == EMPTY_TABLE) { inflateTable(threshold);}
深入下去, 看看具体是怎么实现的. inflateTable()
方法中重新确定了 capacity 的大小.
/** * Inflates the table. */private void inflateTable(int toSize) { // Find a power of 2 >= toSize // 通过 roundUpToPowerOf() 重新确定容量大小, 满足 capacity = 2^n; int capacity = roundUpToPowerOf2(toSize); ... threshold = (int) thresholdFloat; table = new HashMapEntry[capacity];}
roundUpToPowerOf2
中, 调用了 Integer 的一个有关位运算的方法. 保留 number 的最高位为1, 其他位全部清零, 然后右移一位(相当于乘 2 操作).
private static int roundUpToPowerOf2(int number) { // assert number >= 0 : "number must be non-negative"; int rounded = number >= MAXIMUM_CAPACITY ? MAXIMUM_CAPACITY : (rounded = Integer.highestOneBit(number)) != 0 ? (Integer.bitCount(number) > 1) ? rounded << 1 : rounded : 1; return rounded;}
Integer 的功能方法中, 因为整型是 32 位的, 所以通过五次移位和位与运算, 以及一次减法运算. 可以返回最高位数字为1, 其余位数为 0 的位数.
public static int highestOneBit(int i) { // HD, Figure 3-1 // 从最高为开始, 地位全部置1 i |= (i >> 1); i |= (i >> 2); i |= (i >> 4); i |= (i >> 8); i |= (i >> 16); // 保留最高位为1, 其余的全部为0. // roundUpToPowerOf2() 方法中会做右移一位的操作. return i - (i >>> 1);}
I am sorry, 贴出来的源码略多, 不过引用下 Linus Torvalds 的话, “reading the fucking source code!”.
总结下: 把 HashMap 的容量由initialCapacity, 转换为 2 的幂次方, 然后初始化内部的 tables
数组.
index 转换
我们再来看一下将hash 值, 转换为 index 值.
// 通过 hash值, 获取相应的索引.int i = indexFor(hash, table.length);
这一部分代码貌似不同的版本解决方式都是相同的. 通过位运算取余数.
/** * Returns index for hash code h. */static int indexFor(int h, int length) { // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2"; // 因为length 的数值为 2^n 所以, 可以通过这样的方式来取余数. return h & (length-1);}
length 为 HashMap 的长度, 满足
h & (length -1 ) = h % length
小生不才, 常识来解释一下. 我们都知道计算机中都是用二进制来表示数值. 也就是说 h 和 length 在计算机中可以表示为:
// 假设这里用 16 位来表示一个 int 类型h = 0b 1001 1110 0010 1101length = 0b 0000 0000 0000 1000
我们看一看出, h % length 的余数为 0b 0101
也就是 h 的后三位. 因为前十二位是肯定可以被 length 整除的. 所以我们只要求出 h 的后三位即可.
// 这个其实就是取 h 的后三位.h & (length - 1)
这样子就可以得到余数.
get() 方法解析
get()
方法也会调用 hash 函数计算 hash值, 然后计算数组的index. 先聚焦代码:
PS: 代码来自Android api-25
public V get(Object key) { if (key == null) return getForNullKey(); Entry<K,V> entry = getEntry(key); return null == entry ? null : entry.getValue();}
final Entry<K,V> getEntry(Object key) { if (size == 0) { return null; } // 计算 hash值 int hash = (key == null) ? 0 : sun.misc.Hashing.singleWordWangJenkinsHash(key); // 通过 index 所以找到链表. for (HashMapEntry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } return null;}
相比于 put()
, get()
方法就简单多了, 先通过 key 的 hashCode, 计算出 key 的 hashIndex, 找到了相应的元素返回, 找不到返回null.
HashMap 与 HashTable 的区别.
仿佛印象中区别就是一个线程安全一个线程不安全, 然后这个时候有人问, 还有呢, 直接懵逼了… 不过说这里 HashTable 已经不推荐使用了, 如果是考虑多线程的话, 官方推荐使用 java.util.concurrent 包下的相关类.
为了防止这种情况再次发生, 我们来了解下这两种不同的实现:
- HashMap 线程不安全, HashTable 线程安全. (这个最基本的).
- HashMap 允许 Key, Value 为
null
. 当Key 为 null 时, Value 存储在 HashMap 的table[0]
中(NOTE:table[0]
中也会存放 Key 不为null
的元素); 而对于HashTable, 如果 Key 或 Value 任意一个为空, 直接NoPointerException
- 在 JDK1.8 和 api-25 中, HashTable 不限制 table 的大小, 确定 index 直接通过
index = hash % table.length
. (其他版本还是忽略, 观其大略) - HashMap 扩展容量每次是 *2, HashTable 则不是(我就看了2个版本, api-21是*2, JDK.1.8 是 *2; 而 api-25 是* 1.5)
- 还有其他的一些细节问题, 版本不同会有差异, 不在深究(吾生也有涯, 而知也无涯, 以有涯追无涯, 殆矣!)
- 深入了解HashMap(1)
- 深入了解HashMap
- 深入了解java中的hashMap
- 深入了解GNED.....1
- 深入了解MediaServer-1
- 深入了解MediaServer-1
- 深入了解MediaServer-1
- 深入了解MediaServer-1
- 深入了解C语言(1)
- 深入了解javaScipt--String(1)
- 深入了解phalanger(1)
- Java基础-了解HashMap
- HashMap的基本了解
- HashMap快速了解
- hashMap了解<一>
- java hashmap源代码了解
- 深入了解:++与+1、--与-1
- 深入了解php4(1)--回到未来
- 插件编写
- java冒泡、简单插入、选择排序
- WireShark教程 – 黑客发现之旅(5) – (nmap)扫描探测
- 5月集训Day6考试
- jvm内存管理-堆内存分配
- 深入了解HashMap(1)
- NYOJ 数独
- Android 对话框弹出(支持Android 6.0及其以上)
- 上传iOS应用时 ERROR ITMS-90096: Your binary is not optimized for iPhone 5。。。
- JavaScript之异步
- MySql--基本查询
- VC/C#调用lazarus写的dll
- TensorFlow基础(一)
- 常见排序算法