HashMap源码解析
来源:互联网 发布:英文版手机淘宝 编辑:程序博客网 时间:2024/06/03 03:16
一 概念
1、简介
Map是一种key、value形式的键值对,将hash表和Map结合即形成了HashMap。
HashMap就是将key做hash算法,然后将hash值映射到内存地址,直接取得key所对应的数据。在HashMap中,底层数据结构使用的是数组,所谓的内存地址即数组的下标索引。
HashMap的数组是以Entry数组的形式存放的,为了解决碰撞冲突,Entry本身又是一个链表的结构。
2、成员变量
Entry<K,V>[] table:用于存放Map中的Entry元素,默认长度为16
int size:Map的大小,即存储元素的多少
float loadFactor:装载因子,默认为0.75
int threshold:阀值,用来控制Map的扩容边界,一般等于实际容量*loadFactor
int modCount:修改次数,用于快速失败
3、构造方法
HashMap(int initialCapacity, float loadFactor)
HashMap(int initialCapacity);
HashMap();
HashMap(Map<? extends K, ? extends V> m);
4、成员方法
1)clear()
清除map中的所有元素
2)containsKey(Object key)
判断map中是否包含该键
3)containsValue(Object value)
判断map中是否包含该value
4)entrySet()
返回此映射中包含的映射关系的 Set 视图,Map.Entry表示映射关系
5)get(Object key)
根据key获取其对应的value值,若不存在则返回null
6)isEmpty()
查看Map是否为空
7)keySet()
将Map中所有的键存入到set集合中并返回
8)put(K key, V value)
往Map中添加一对键值对
9)putAll(Map<? extends K, ? extends V> m)
往Map中添加多个键值对
10)remove(Object key)
根据key值移除已存在的键值对并返回,若不存在返回null
11)size()
返回map中键值对的数量
12)values()
获取集合中的所有的值
5、特点
1)线程非安全,并且允许key与value都为null值;
2)不保证其内部元素的顺序,而且随着时间的推移,同一元素的位置也可能改变(resize的情况);
3)put、get操作的时间复杂度为O(1);
4)遍历其集合视角的时间复杂度与其容量(capacity,槽的个数)和现有元素的大小(entry的个数)成正比,所以如果遍历的性能要求很高,不要把capactiy设置的过高或把平衡因子(loadfactor,当entry数大于capacity*loadFactor时,会进行resize,resize会导致key进行rehash)设置的过低。
5)由于HashMap是线程非安全的,这也就是意味着如果多个线程同时对一个HashMap的集合试图做迭代时有结构的上改变(添加、删除entry,只改变entry的value的值不算结构改变),那么会报ConcurrentModificationException,专业术语叫fail-fast,尽早报错对于多线程程序来说是很有必要的。
6、HashMap的高性能需要保证一下几点
1)hash算法必须是高效的;
2)hash值到内存地址(数组索引)的算法是快速的;
3)根据内存地址(数组索引)可以直接取得对应的值
二 源码解析
1、hash算法解析
final int hash(Object k) { int h = hashSeed; if (0 != h && k instanceof String) { return sun.misc.Hashing.stringHash32((String) k); } h ^= k.hashCode(); // 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);}
首先,hash函数中有个随机的hashSeed,用来来降低冲突发生的几率
然后如果是字符串,用了sun.misc.Hashing.stringHash32((String)k);来获取索引值
其次,会调用Object类的hashCode()方法;
最后,通过一系列无符号右移操作,来把高位与低位进行异或操作,来降低冲突发生的几率,右移的偏移量20,12,7,4。因为Java中对象的哈希值都是32位的,所以这几个数就是把高位与低位做异或运算。
2、hash值到内存地址映射算法解析
/** * 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"; return h & (length-1); }
在哈希表容量(也就是buckets或slots大小)为length的情况下,为了使每个key都能在冲突最小的情况下映射到[0,length)(注意是左闭右开区间)的索引(index)内,一般有两种做法:
a.让length为素数,然后用hashCode(key) mod length的方法得到索引
b.让length为2的n次方数,然后用hashCode(key)&(length-1)的方法得到索引
HashTable用的是方法1,HashMap用的是方法2。
因为HashMap的length为2的指数倍,所以length-1所对应的二进制位都为1,然后在与hashCode(key)做与运算,即可得到[0,length)内的索引但是这里有个问题,如果hashCode(key)的大于length的值,而且hashCode(key)的二进制位的低位变化不大,那么冲突就会很多,举个例子:
Java中对象的哈希值都32位整数,而HashMap默认大小为16,那么有两个对象那么的哈希值分别为:0xABAB0000与0xBABA0000,它们的后几位都是一样,那么与16异或后得到结果应该也是一样的,也就是产生了冲突。造成冲突的原因关键在于16限制了只能用低位来计算,高位直接舍弃了,所以我们需要额外的哈希函数而不只是简单的对象的hashCode方法了。
3、初始化HashMap
private void inflateTable(int toSize) { // Find a power of 2 >= toSize int capacity = roundUpToPowerOf2(toSize); threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); table = new Entry[capacity]; initHashSeedAsNeeded(capacity);}final boolean initHashSeedAsNeeded(int capacity) { boolean currentAltHashing = hashSeed != 0; boolean useAltHashing = sun.misc.VM.isBooted() && (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD); boolean switching = currentAltHashing ^ useAltHashing; if (switching) { hashSeed = useAltHashing ? sun.misc.Hashing.randomHashSeed(this) : 0; } return switching;}
1)roundUpToPowerOf2(toSize)返回一个比toSize大的最小的2的n次方 数capacity。
2)容量值计算阀值,并创建Entry数组。
4、存元素
public V put(K key, V value) { if (table == EMPTY_TABLE) { inflateTable(threshold); } if (key == null) return putForNullKey(value); int hash = hash(key); 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;}
首先,如果table为空,则进行扩容,key为null,则把value放在table[0]的位置。
其次,根据hash值和table数组的长度计算新插入元素在数组中的索引。
最后,如果table索引处Entry不为空,则找到key对应的Entry并将旧值替换,如果Entry为空,则增加增加一个新的Entry对象并赋值与该数组索引。
注:HashMap只允许一个为null的key。
5、取元素
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; } int hash = (key == null) ? 0 : hash(key); for (Entry<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;}
6、扩容
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, initHashSeedAsNeeded(newCapacity)); table = newTable; threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);}void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; for (Entry<K,V> e : table) { while(null != e) { Entry<K,V> next = e.next; if (rehash) { e.hash = null == e.key ? 0 : hash(e.key); } int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next; } }}
HashMap每次扩容会增加一倍的容量,transfer方法会将已存在的值重新计算数组的下标然后存入新的数组。
Hashmap的扩容是比较浪费时间的,在平时使用中,如果能估计出大概的Hashmap的容量,可以合理的设置装载因子loadFactory和entry数组初始长度,可以避免热死则操作,提高put的效率。
7、解决hash冲突的办法
一般解决hash冲突的办法有:开放定址发(线程探测再散列|二次探测再散列|伪随机探测再散列)、再哈希法、链地址发、建立一个公共溢出区
HashMap采用的是链地址法。即在冲突的位置上建立一个链表,然后将冲突的元素插入到链表尾端。
三 线程安全
1、多线程存在的问题
1)同时使用put方法添加元素,当两个key发生了碰撞时(hash值一样),这个key会添加到数组的同一位置,最终会使其中一个线程put的数据被覆盖
2)多线程扩容过程中会出现死循环
2、线程安全的HashMap
1)Map m = Collections.synchronizedMap(newHashMap(…));
通过这种方式可以得到一个线程安全的map。
2)HashTable
3)ConcurrentHashMap
四 遍历
1、entrySet()方式
Map<String,String> map = new HashMap<String,String>();map.put("01", "zhangsan");map.put("02", "lisi");map.put("03", "wangwu");//通过entrySet()方法将map集合中的映射关系取出(这个关系就是Map.Entry类型)for(Entry<String, String> entry:map.entrySet()){ String key = entry.getKey();//通过关系对象获取key String value = entry.getValue();//通过关系对象获取value System.out.println("key: "+key+"-->value: "+value);}
2、keySet()方式
Map<String,String> map = new HashMap<String,String>();map.put("01", "zhangsan");map.put("02", "lisi");map.put("03", "wangwu");Set<String> keySet = map.keySet();//先获取map集合的所有键的Set集合for(String key:keySet){ String value = map.get(key);//有了键可以通过map集合的get方法获取其对应的值。 System.out.println("key: "+key+"-->value: "+value);//获得key和value值}
虽然使用keyset及entryset来进行遍历能取得相同的结果,但两者的遍历速度是有差别的
keySet():迭代后只能通过get()取key
entrySet():迭代后可以e.getKey(),e.getValue()取key和value。返回的是Entry接口
说明:keySet()的速度比entrySet()慢了很多,也就是keySet方式遍历Map的性能不如entrySet性能好
为了提高性能,以后多考虑用entrySet()方式来进行遍历。
参考链接:http://blog.csdn.net/liu826710/article/details/9001254
https://wenku.baidu.com/view/03eda688f46527d3240ce0fa.html
- Android源码解析 -- HashMap
- HashMap源码解析
- HashMap源码解析
- HashMap源码解析
- HashMap源码解析
- HashMap源码解析
- HashMap源码解析
- Java HashMap 源码解析
- Java HashMap 源码解析
- Java HashMap 源码解析
- 源码解析HashMap
- Java:HashMap源码解析
- HashMap 源码解析
- HashMap源码解析
- HashMap 源码解析
- Java源码解析-hashmap
- HashMap源码解析
- HashMap源码解析
- loj #6194. 「美团 CodeM 复赛」排列(组合数学)
- Hash Code Hacker
- redis五中数据类型操作
- ssh2扩展执行远程登录命令以及执行多个命令
- 做一个简单的共享元素Activity跳转
- HashMap源码解析
- 深入理解ES6--块级作用域(let const)
- 希尔排序
- redis 持久化的两种方式
- 事务笔记
- 1001. A+B Format (20)
- GitHub 博客自定义域名配置(阿里)
- 4岁开始升牛,9岁死----分支简单算法
- Java线程状态