谈一谈HashMap类--实现原理,扩容机制,容量为2的次幂
一、Java中的hashCode()和equals()
1、 hashCode()的存在主要是用于查找的快捷性,如Hashtable,HashMap等,hashCode()是用来在散列存储结构中确定对象的存储地址的;
2、如果两个对象相同,就是指对象调用equals()方法返回true,那么这两个对象的hashCode()方法的返回值一定要相同;
3、如果对象的equals()方法被重写,那么对象的hashCode()方法也尽量重写,并且hashCode()方法使用的对象的变量信息,一定要和equals方法中使用的一致,否则就会违反上面提到的第2点;
4、 两个对象的hashCode()相同,并不一定表示两个对象就相同,也就是不一定适用于equals(java.lang.Object) 方法,只能够说明这两个对象在散列存储结构中,如Hashtable,他们“存放在同一个篮子里“。
再归纳一下就是hashCode()是用于查找使用的,而equals()是用于比较两个对象是否相等的。
二、HashMap类的实现原理
1、HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
2、在java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。
3、HashMap的存取:
1)利用key的hashCode重新hash计算出当前对象的元素在数组中的下标(索引计算公式:(length-1)& hash);
2)存储时,如果出现hash值相同的key,此时有两种情况。(1)如果key相同,则覆盖原始值;(2)如果key不同(出现冲突),则将当前的key-value放入链表中
3)获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。
4)理解了以上过程就不难明白HashMap是如何解决hash冲突的问题,核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。
三、HashMap的扩容机制
当hashmap中的元素越来越多的时候,碰撞的几率也就越来越高(因为数组的长度是固定的),所以为了提高查询的效率,就要对hashmap的数组进行扩容,数组扩容这个操作也会出现在ArrayList中,所以这是一个通用的操作,很多人对它的性能表示过怀疑,不过想想我们的“均摊”原理,就释然了,而在hashmap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。
那么hashmap什么时候进行扩容呢?当hashmap中的元素个数超过数组大小*loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,也就是说,默认情况下,数组大小为16,那么当hashmap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知hashmap中元素的个数,那么预设元素的个数能够有效的提高hashmap的性能。比如说,我们有1000个元素new HashMap(1000), 但是理论上来讲new HashMap(1024)更合适,不过即使是1000,hashmap也自动会将其设置为1024。 但是new HashMap(1024)还不是更合适的,因为0.75*1000 < 1000, 也就是说为了让0.75 * size > 1000, 我们必须这样new HashMap(2048)才最合适,既考虑了&的问题,也避免了resize的问题。
/**
* jdk版本:1.8.0_111。 * 使用HashMap的空参构造创建一个HashMap集合,初始容量为16;负载因子loadFactor为0.75; * 当集合的元素个数超过16*0.75=12时集合容量扩大一倍。*/ public class Demo01 { public static void main(String[] args) throws Exception { HashMap<String, Object> map = new HashMap<String, Object>(); System.out.println(map.size()); // 0 System.out.println(getCapacity(map)); // 16 map.put("abc0", 1); map.put("abc16", 1); map.put("abc27", 1); map.put("abc49", 1); System.out.println(map.size()); // 4 System.out.println(getCapacity(map)); // 16 String key = null; for (int i = 0; i < 8; i++) { key = "a" + i; map.put(key, key); } System.out.println(map.size()); // 12 System.out.println(getCapacity(map)); // 16 Object obj = map; map.put("a8", "a8"); System.out.println(map.size()); // 13 System.out.println(getCapacity(map)); // 32 System.out.println(obj == map); // true,扩容后引用地址没变 } public static Integer getCapacity(Map<?, ?> map) throws Exception { Method method; method = map.getClass().getDeclaredMethod("capacity"); method.setAccessible(true); return (int) method.invoke(map); } }
再看下面的一个例子:
/**
* jdk版本:jdk1.7.0_60。 * 创建一个HashMap集合,初始容量为4,加载因子loadFactor为0.75; * 我们理应认为:当集合的元素个数超过4*0.75=3时集合容量扩大一倍。 * 但是当使用jdk1.7.0_60时,结果并非如此。 */ public class Demo2 { public static void main(String[] args) throws Exception { HashMap<Integer, Object> map = new HashMap<>(4, 0.75f); System.out.println(map.size()); // 0 System.out.println(getCapacity(map)); // 0 map.put(0, 1); map.put(4, 1); map.put(8, 1); System.out.println(map.size()); // 3 System.out.println(getCapacity(map)); // 4 map.put(1, 1); System.out.println(map.size()); // 4 System.out.println(getCapacity(map)); // 4 map.put(2, 1); System.out.println(map.size()); // 5 System.out.println(getCapacity(map)); // 4 map.put(3, 1); System.out.println(map.size()); // 6 System.out.println(getCapacity(map)); // 4 map.put(5, 1); System.out.println(map.size()); // 7 System.out.println(getCapacity(map)); // 8 } public static Integer getCapacity(Map<?, ?> map) throws Exception { Method method; method = map.getClass().getDeclaredMethod("capacity"); method.setAccessible(true); return (int) method.invoke(map); } }
查看jdk1.7.0_60HashMap源码:问题的关键在于标红的代码。
void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length); hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex); }
四、为什么HashMap容量为2的次幂?
看了一些资料,以下是自己的理解:
HashMap底层实现为数组+单链表,元素的存取是根据数组索引来操作的。那么HashMap是如何计算元素在数组中的索引呢?
首先,HashMap的元素是包含key、value的键值对,HashMap是根据key值,加上一些算法计算得到该元素在数组中的索引。具体算法如下:
// 传入key,得到对应的hash值 static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } /** * @param key 键值对的key * @param capicity HashMap集合的容量 * @return */ static int getIndex(Object key, int capicity) { int hash = hash(key); // 传入key,得到对应的hash值 int index = (capicity - 1) & hash; // 得到元素在数组中的索引 return index; }
综上可知,HashMap集合是根据 (capicity - 1) & key的hash值 来计算元素在数组中的索引。
假设HashMap集合的容量capacity=15,则元素在数组中的索引 index = (capacity-1) & hash = 14 & hash = 0b1110 & hash,则index只有8种结果:0、2、4、6、8、10、12、14,意思就是数组的某些位置不能存值,浪费空间,也加大了hash冲突的可能。
如果HashMap集合的容量capacity=2的n次方,则元素在数组中的索引 index = (2n-1) & hash = 0b11...11(n个1) & hash,则index有2n种结果,即是0、1、2 ... 2n-1,所以数组的每个位置都会存值,基本上均匀地分布,有效利用空间,也减少了hash冲突,提高HashMap集合的性能。
事实上,只有当HashMap集合的容量capacity=2n时,(capacity-1) & hash 与 hash % capacity 的结果是等效的。
写个小案例体会一下:
public class Demo1 { public static void main(String[] args) { String key = null; int capicity = 15; // 容量 int[] array = new int[capicity]; for (int i = 0; i < 10000; i++) { key = "abc1" + i; int index = getIndex(key, capicity); if (index < capicity) { array[index] += 1; } } //capicity=16: [642, 643, 600, 590, 556, 538, 572, 554, 609, 607, 655, 665, 692, 711, 674, 692] //capicity=8: [1251, 1250, 1255, 1255, 1248, 1249, 1246, 1246] //capicity=4: [2499, 2499, 2501, 2501] //capicity=15: [1285, 0, 1190, 0, 1094, 0, 1126, 0, 1216, 0, 1320, 0, 1403, 0, 1366] //capicity=14: [1242, 1233, 0, 0, 1128, 1092, 0, 0, 1264, 1272, 0, 0, 1366, 1403] System.out.println(Arrays.toString(array)); } // 传入key,得到对应的hash值 static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } /** * 得到元素在数组中的索引 * @param key 键值对的key * @param capicity HashMap集合的容量 * @return */ static int getIndex(Object key, int capicity) { int hash = hash(key); // 传入key,得到对应的hash值 int index = (capicity - 1) & hash; // 得到元素在数组中的索引 return index; } }
参考资料:
posted on 2019-03-06 21:42 wenbin_ouyang 阅读(696) 评论(0) 编辑 收藏 举报