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

原创粉丝点击