集合系列--HashMap实现详解
2013-05-30 21:17
567 查看
1、HashMap特点
HashMap是基于哈希表的map接口的非同步实现,key不可重复,但value可以,其中key和value都可以为空,且此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
2、HashMap的数据结构
hashmap 是一个链表散列的数据结构,即数组和链表的结合体,对于是怎样结合,我们首先看一下hashmap 的源码:
当创建一个HashMap是,系统会自动的创建一个table数组来保存HashMap中的键值对,并持有指向下一个元素的引用,这样就构成了链表,我们看一下HashMap的存储实现:当系统调用HashMap的put方法保存元素 eg:
当程序执行这句话时,系统将调用“语文“的hashCode()方法得到其hashCode值,然后根据hashCode的值来决定该元素的存储位置,我们来看一下HashMap的put实现:
从put源码可以看出,将一个key-value存入HashMap中时,会先根据key的hashCode()的返回值决定entry的存储位置,如果两个entry的hashCode()相同,那么他们的存储位置相同,然后比较两个entry是否equals,如果equals的结果返回true,则新添加的entry的value值将会覆盖原entry的value值,但key将不变,如果返回false,新添加的entry将会指向原entry形成链表,新添加的entry将会位于entry链的头部,其中addEntry方法根据计算出的hash值将entry保存在table的i索引出,我们来看一下addEntry(hash,key,value,i)的源码:
从源码可以看出,系统总是将新加入的Entry对象放在table数组的bucketIndex索引处,如果该索引出没有对象,则新放入的Entry对象将指向该索引,如果该索引出已经有其他Entry对象,则新添加的Entry将指向原来的Entry形成Entry链如图:
3、HashMap的性能
首先看一下HashMap的构造方法:
HashMap();构造一个厨师容量为16,负载因子为0.75的HashMap
HashMap(int initialCapacity);构建一个初始容量为initialCapacity,负载因子为0.75的HashMap
HashMap(int initialCapacity,float loadFactor);构建一个初始容量为initialCapacity,负载因子为loadFactor的HashMap
我们来看一下第三个构造函数的源码:
由
可以看出,HashMap的实际容量并不一定是指定的initialcapacity,他们只有当所指定的大小为2的n次幂的时候才相等,如果所指定的大小不为2的n次幂时,实际容量为大于所指定容量的最小的2的n次幂值。比如所指定的容量大小为5,那么实际容量为大于5的最小的2的n次幂值为8,所以HashMap的实际容量为8,。
对于默认的负载因子为0.75,这是时间和空间成本上的一种折中,增大负载因子可以减少hash表(Entry数组)所占用的内存空间,但会增加查询数据的时间开销,减少负载因子会提高数据查询性能,但会增加hash表所占用的内存空间。
在HashMap中通过threshold字段来判断HashMap的最大容量
threshold就是在此loadFactor和capacity对应下允许的最大元素数目,超过这个数目就重新resize,将数组扩充为原来的一倍:
比如说:原容量为16,负载因子为0.75,那么
threshold = 16*0.75 = 12,也就是说,当HashMap中元素个数超过12时,就把数组扩充为16*2 = 32,这样是比较耗性能的,他得重新计算各个元素在数组中的位置。
4、HashMap的读取实现
当 HashMap 的每个 bucket 里存储的 Entry 只是单个 Entry ——也就是没有通过指针产生 Entry 链时,此时的 HashMap 具有最好的性能:当程序通过 key 取出对应 value 时,系统只要先计算出该 key 的 hashCode() 返回值,在根据该 hashCode 返回值找出该 key 在 table 数组中的索引,然后取出该索引处的 Entry,最后返回该 key 对应的 value 即可。看 HashMap 类的 get(K key) 方法代码:
从上面代码中可以看出,如果 HashMap 的每个 bucket 里只有一个 Entry 时,HashMap 可以根据索引、快速地取出该 bucket 里的 Entry;在发生“Hash 冲突”的情况下,单个 bucket 里存储的不是一个 Entry,而是一个 Entry 链,系统只能必须按顺序遍历每个 Entry,直到找到想搜索的 Entry 为止——如果恰好要搜索的 Entry 位于该 Entry 链的最末端(该 Entry 是最早放入该 bucket 中),那系统必须循环到最后才能找到该元素。
参考文章:HashMap实现原理
HashMap是基于哈希表的map接口的非同步实现,key不可重复,但value可以,其中key和value都可以为空,且此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
2、HashMap的数据结构
hashmap 是一个链表散列的数据结构,即数组和链表的结合体,对于是怎样结合,我们首先看一下hashmap 的源码:
/** * The table, resized as necessary. Length MUST Always be a power of two. */ transient Entry[] table; static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next; final int hash; …… }
当创建一个HashMap是,系统会自动的创建一个table数组来保存HashMap中的键值对,并持有指向下一个元素的引用,这样就构成了链表,我们看一下HashMap的存储实现:当系统调用HashMap的put方法保存元素 eg:
hashMap.put("语文",80.0);
当程序执行这句话时,系统将调用“语文“的hashCode()方法得到其hashCode值,然后根据hashCode的值来决定该元素的存储位置,我们来看一下HashMap的put实现:
public V put(K key, V value) { // HashMap允许存放null键和null值。 // 当key为null时,调用putForNullKey方法,将value放置在数组第一个位置。 if (key == null) return putForNullKey(value); // 根据key的keyCode重新计算hash值。 int hash = hash(key.hashCode()); // 搜索指定hash值在对应table中的索引。 int i = indexFor(hash, table.length); // 如果 i 索引处的 Entry 不为 null,通过循环不断遍历 e 元素的下一个元素。 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; } } // 如果i索引处的Entry为null,表明此处还没有Entry。 modCount++; // 将key、value添加到i索引处。 addEntry(hash, key, value, i); return null; }
从put源码可以看出,将一个key-value存入HashMap中时,会先根据key的hashCode()的返回值决定entry的存储位置,如果两个entry的hashCode()相同,那么他们的存储位置相同,然后比较两个entry是否equals,如果equals的结果返回true,则新添加的entry的value值将会覆盖原entry的value值,但key将不变,如果返回false,新添加的entry将会指向原entry形成链表,新添加的entry将会位于entry链的头部,其中addEntry方法根据计算出的hash值将entry保存在table的i索引出,我们来看一下addEntry(hash,key,value,i)的源码:
void addEntry(int hash, K key, V value, int bucketIndex) { // 获取指定 bucketIndex 索引处的 Entry Entry<K,V> e = table[bucketIndex]; // 将新创建的 Entry 放入 bucketIndex 索引处,并让新的 Entry 指向原来的 Entry table[bucketIndex] = new Entry<K,V>(hash, key, value, e); // 如果 Map 中的 key-value 对的数量超过了极限 if (size++ >= threshold) // 把 table 对象的长度扩充到原来的2倍。 resize(2 * table.length); }
从源码可以看出,系统总是将新加入的Entry对象放在table数组的bucketIndex索引处,如果该索引出没有对象,则新放入的Entry对象将指向该索引,如果该索引出已经有其他Entry对象,则新添加的Entry将指向原来的Entry形成Entry链如图:
3、HashMap的性能
首先看一下HashMap的构造方法:
HashMap();构造一个厨师容量为16,负载因子为0.75的HashMap
HashMap(int initialCapacity);构建一个初始容量为initialCapacity,负载因子为0.75的HashMap
HashMap(int initialCapacity,float loadFactor);构建一个初始容量为initialCapacity,负载因子为loadFactor的HashMap
我们来看一下第三个构造函数的源码:
public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); // Find a power of 2 >= initialCapacity int capacity = 1; while (capacity < initialCapacity) capacity <<= 1; this.loadFactor = loadFactor; threshold = (int)(capacity * loadFactor); table = new Entry[capacity]; init(); }
由
while (capacity < initialCapacity) capacity <<= 1;
可以看出,HashMap的实际容量并不一定是指定的initialcapacity,他们只有当所指定的大小为2的n次幂的时候才相等,如果所指定的大小不为2的n次幂时,实际容量为大于所指定容量的最小的2的n次幂值。比如所指定的容量大小为5,那么实际容量为大于5的最小的2的n次幂值为8,所以HashMap的实际容量为8,。
对于默认的负载因子为0.75,这是时间和空间成本上的一种折中,增大负载因子可以减少hash表(Entry数组)所占用的内存空间,但会增加查询数据的时间开销,减少负载因子会提高数据查询性能,但会增加hash表所占用的内存空间。
在HashMap中通过threshold字段来判断HashMap的最大容量
threshold = (int)(capacity * loadFactor);
threshold就是在此loadFactor和capacity对应下允许的最大元素数目,超过这个数目就重新resize,将数组扩充为原来的一倍:
if (size++ >= threshold) resize(2 * table.length);
比如说:原容量为16,负载因子为0.75,那么
threshold = 16*0.75 = 12,也就是说,当HashMap中元素个数超过12时,就把数组扩充为16*2 = 32,这样是比较耗性能的,他得重新计算各个元素在数组中的位置。
4、HashMap的读取实现
当 HashMap 的每个 bucket 里存储的 Entry 只是单个 Entry ——也就是没有通过指针产生 Entry 链时,此时的 HashMap 具有最好的性能:当程序通过 key 取出对应 value 时,系统只要先计算出该 key 的 hashCode() 返回值,在根据该 hashCode 返回值找出该 key 在 table 数组中的索引,然后取出该索引处的 Entry,最后返回该 key 对应的 value 即可。看 HashMap 类的 get(K key) 方法代码:
public V get(Object key) { // 如果 key 是 null,调用 getForNullKey 取出对应的 value if (key == null) return getForNullKey(); // 根据该 key 的 hashCode 值计算它的 hash 码 int hash = hash(key.hashCode()); // 直接取出 table 数组中指定索引处的值, for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; // 搜索该 Entry 链的下一个 Entr e = e.next) // ① { Object k; // 如果该 Entry 的 key 与被搜索 key 相同 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) return e.value; } return null; }
从上面代码中可以看出,如果 HashMap 的每个 bucket 里只有一个 Entry 时,HashMap 可以根据索引、快速地取出该 bucket 里的 Entry;在发生“Hash 冲突”的情况下,单个 bucket 里存储的不是一个 Entry,而是一个 Entry 链,系统只能必须按顺序遍历每个 Entry,直到找到想搜索的 Entry 为止——如果恰好要搜索的 Entry 位于该 Entry 链的最末端(该 Entry 是最早放入该 bucket 中),那系统必须循环到最后才能找到该元素。
参考文章:HashMap实现原理
相关文章推荐
- 深入Java集合学习系列:HashMap的实现原理
- 深入Java集合学习系列:HashMap的实现原理
- 深入Java集合学习系列:HashMap的实现原理
- 深入Java集合学习系列:HashMap的实现原理
- 深入Java集合学习系列:HashMap的实现原理
- 深入Java集合学习系列:HashMap的实现原理
- 深入Java集合学习系列:HashMap的实现原理
- 深入Java集合学习系列:HashMap的实现原理
- 深入Java集合学习系列:HashMap的实现原理
- 深入Java集合学习系列:HashMap的实现原理
- 深入Java集合学习系列:HashMap的实现原理
- 深入Java集合学习系列:HashMap的实现原理
- 深入Java集合学习系列:HashMap的实现原理
- 深入Java集合学习系列:HashMap的实现原理
- 集合系列--TreeMap实现详解
- 深入Java集合学习系列:HashMap的实现原理
- 深入Java集合学习系列:HashMap的实现原理
- 深入Java集合学习系列:HashMap的实现原理
- 深入Java集合学习系列:HashMap的实现原理
- Java集合系列:-----------08HashMap的底层实现