HashMap中的数据结构与get,put源码解析
2016-10-24 16:19
639 查看
HashMap 执行流程:
首先构造方法:
public HashMap() {
this.loadFactor =DEFAULT_LOAD_FACTOR;// all otherfields defaulted
}
public HashMap(intinitialCapacity) {
this(initialCapacity,DEFAULT_LOAD_FACTOR);
}
public HashMap(intinitialCapacity,floatloadFactor)
{
}
public HashMap(Map<?extends K, ?extends V>m) {
}
通过重载方法HashMap传入两个参数,1.初始化容量,2.装载因子
那么就介绍下几个名词:
1. capacity,表示的是hashmap中桶的数量,初始化容量initCapacity为16,第一次扩容会扩到64,之后每次扩容都是之前容量的2倍,所以容量每次都是2的次幂,
(为什么HashMap的容量是2的次幂呢?
因为在源码中我们发现在通过hash值寻找put的index时进行的是一个位运算(n-1)&hash,位运算是基于二进制的,所以是2的次幂;
通过位运算可以很快的找到put的index位置,所以hashMap的插入效率很高。
比如初始容量为16,一个待插入元素hash值为6,那么我们一般要插入的位置就是index=6,也就是6%16我们进行的是取余操作。那么试试位运算(16-1)&6
&位与运算:有0则0
0111 15
0110 6
_______
0110 6
通过取余和位与运算我们都得到了想要的结果,那么位运算的高效率肯定会被采纳。
当容量为2的n次幂时,减1后与任何数进行与运算都可以快速的得到取余结果,也就是index的值。
)
//默认初始化容量为16
static final int DEFAULT_INITAL_CAPACITY=1<<4; //aka 16
//中间会进行扩容操作,但是最大容量为2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;
2. loadFactor,装载因子,衡量hashmap一个满的程度,初始化为0.75
//默认的装载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
实时装载因子是size/capacity;
3. threshold,hashmap扩容的一个标准,每当size大于这个标准时就会进行扩容操作,threeshold等于capacity*loadfacfactor
/**
The next sizevalue at which to resize (capacity * load factor).
**/
int
threshold;
4. size,表示HashMap中存放Node的数量,就是所有的键值对数量。
这个程序运行结果是3,从这里我们可以看出键已经有了的话是不会在添加而是覆盖的,而且可以允许null做值,null做键,当然值得注意的是str,str1,在这里他size不考虑地址,只考虑内容,尽管str和str1指向的地址不同,但是put进去仍然是覆盖不是添加。马上我们会根据源码进行分析。
在初始化过程中,如果初始化容量和装载因子都是用户自己设置的,那么会判断初始化容量,如果小于0会抛出异常,大于了2的30次方也就是最大容量时,会定为最大容量,判断装载因子,如果小于等于0或者他不是一个数字(通过Float.isNaN(loadFactor)),抛出异常,在最后也初始化了threshold的值
当然如果通过传Map进行初始化
会调用putMapEntries(Map<?extends K, ? extends V> m, boolean evict)方法
在这里面首先通过获取map的size,来判断是否需要扩容,之后循环便利每一个元素放入hashmap中,使用的方法是putVal(hash(key), key, value, false, evict);,这个方法也是我们平时调用map.put(Kkey,V value)的核心。
我们也会看到,putAll方法的原理也是这个函数。
在第一个参数我们传的是hash(Key),
不同的key有着不同的hashCode(),只要hashCode()相同,hash一定相同,但是反之不成立,不同对象的hashCode()的hash是可能相同的,这就是所谓的hash冲突
那么为什么会出现相同的hash呢?
那就是(h=key.hashCode())^(h>>>16)
虽然每个元素的hashCode()是唯一的,但是他的二进制右移(>>>是带符号,>>不带符号)16位就会出现重复的。h>>>16这样只有超过2的16次方hash(key)才会有作用,也就是说在2的16次方内都为0。之后和hashCode进行异或。hash()就是为了让均匀分布,他会让1111 0000 0000 0000变得1111 1110 1110 1111
让”1”变得均匀点
下面我们看下hashmap的结构示意图
我们可以看到 每一个元素就是一个Node<K,V>,这个Node<K,V>实现了Map.Entry<K,V> 接口。在jdk1.7中,它是一个HashMapEntry<K,V>
在JDK1.8之前,HashMap采用桶+链表实现,本质就是采用数组+单向链表组合型的数据结构。它之所以有相当 快的查询速度主要是因为它是通过计算散列码来决定存储的位置。HashMap通过key的hashCode来计算hash值,不同的hash值就存在数组中不同的位置,当多个元素的hash值相同时(所谓hash冲突),就采用链表将它们串联起来(链表解决冲突),放置在该hash值所对应的数组位置上。在JDK1.8中,HashMap的存储结构已经发生变化,它采用数组+链表+红黑树这种组合型数据结构。当hash值发生冲突时,会采用链表或者红黑树解决冲突。当同一hash值的结点数小于8时,则采用链表,否则,采用红黑树。
我们现在对hashmap的存储结构现在应该有了一个初步了解了吧,那么我们就来看下,我们每次进行put(key)时到底是hashMap是如何处理的。
put中调用了
final VputVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict)
该方法中有Node<K,V>[]tab; Node<K,V> p;
tab就是数组,而p是每个桶
如果tab刚开始是null或者大小为0,则进行扩容操作resize(),返回值为Node<K,V>[],直接赋值给tab,初始化tab。
初始化之后通过位与运算(求余)找到put的index,如果该位置没有元素也就是tab[index]==null,那么tab[i] =newNode(hash, key, value, null);即put成功
当然我们知道hash冲突是有的,所以当tab[index]!=null时,也就发生了hash冲突
第一个if其实考虑的是重复键,第二个if我们可以看到绿色的注释说的是在map中已经存在key了,所以这两步是对于已有key情况下的节点put的一个处理。
如果不是重复的,那么就看p是否是树节点,因为jdk1.8中采用的是红黑树,所以要考虑树节点,如果是树节点就进行树节点的put,e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key,value);对于树节点的插入我们这里就不多做解释了
如果上述情况都不是,那就是hash冲突并且使用链表处理了;。
通过e=p.next进行一个链表遍历,,
如果等于null也就是说遍历到了末尾也没发现重复的key,那么就是就执行一个插入操作,是一个尾插法,jdk1.8之前是头插法,jdk1.8是尾插法,
那么为什么jdk1.8是头插,之前为头插法呢
1.8为什么尾插我觉得大家通过上面这段话应该都可以在知道原因吧,因为我已经遍历到了链表尾部了,尾插不就更省事吗?
可是有些人问了1.7是单独的数组加链表,那不应该也尾插吗?这就有一个效率问题了,因为jdk1.8每当节点>8时就会变为树,而树的遍历会更加快速,
而链表遍历最多也就是7次,效率还是很高的,可是1.7就不是这样了,如果你有10000个节点,那你如果尾插的话就需要遍历10000次,这是非常耗时的,所以1.7采用的是头插法。
再插入过程中,如果桶中节点个数大于树的阈值TREEIFY_THRESHOLD-1,就会进行树化。从链表变为红黑树
最后进行一个判断,看size是否到达了扩容标准,如果达到了进行扩容resize();
resize():
如果为空,则按threshold分配空间,(默认是数组=16,装载因子=0.75f,阈值=16*0.75),否则,加倍后,每个容器中的元素在新table中要么呆在原索引处,
要么有一个2的次幂的位移(这也是保证了hashmap中的元素分配均匀)
到这里我们平常所用的put方法就结束了
下面我们看下get方法
get方法比较简单
主要是getNode(int hash,Object key)
直接判断hashmap中的桶是否为空,并且看tab[index]是否为空,如果为空则返回null
否则检查tab[index]处Node的属性,看key是否相等,相等返回改Node,不是则遍历该桶中的节点。
利用first.next遍历,如果是树节点则getTreeNode(hash,key),是链表节点的话遍历链表寻找。
首先构造方法:
public HashMap() {
this.loadFactor =DEFAULT_LOAD_FACTOR;// all otherfields defaulted
}
public HashMap(intinitialCapacity) {
this(initialCapacity,DEFAULT_LOAD_FACTOR);
}
public HashMap(intinitialCapacity,floatloadFactor)
{
}
public HashMap(Map<?extends K, ?extends V>m) {
}
通过重载方法HashMap传入两个参数,1.初始化容量,2.装载因子
那么就介绍下几个名词:
1. capacity,表示的是hashmap中桶的数量,初始化容量initCapacity为16,第一次扩容会扩到64,之后每次扩容都是之前容量的2倍,所以容量每次都是2的次幂,
(为什么HashMap的容量是2的次幂呢?
因为在源码中我们发现在通过hash值寻找put的index时进行的是一个位运算(n-1)&hash,位运算是基于二进制的,所以是2的次幂;
通过位运算可以很快的找到put的index位置,所以hashMap的插入效率很高。
比如初始容量为16,一个待插入元素hash值为6,那么我们一般要插入的位置就是index=6,也就是6%16我们进行的是取余操作。那么试试位运算(16-1)&6
&位与运算:有0则0
0111 15
0110 6
_______
0110 6
通过取余和位与运算我们都得到了想要的结果,那么位运算的高效率肯定会被采纳。
当容量为2的n次幂时,减1后与任何数进行与运算都可以快速的得到取余结果,也就是index的值。
)
//默认初始化容量为16
static final int DEFAULT_INITAL_CAPACITY=1<<4; //aka 16
//中间会进行扩容操作,但是最大容量为2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;
2. loadFactor,装载因子,衡量hashmap一个满的程度,初始化为0.75
//默认的装载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
实时装载因子是size/capacity;
3. threshold,hashmap扩容的一个标准,每当size大于这个标准时就会进行扩容操作,threeshold等于capacity*loadfacfactor
/**
The next sizevalue at which to resize (capacity * load factor).
**/
int
threshold;
4. size,表示HashMap中存放Node的数量,就是所有的键值对数量。
String str="abc"; String str1=new String("abc"); Map<String, String> map=new HashMap<>(); map.put("11", "22"); map.put("11", "22"); map.put(null, "22"); map.put(str,"1"); map.put(str1,"1"); System.out.println(map.size());
这个程序运行结果是3,从这里我们可以看出键已经有了的话是不会在添加而是覆盖的,而且可以允许null做值,null做键,当然值得注意的是str,str1,在这里他size不考虑地址,只考虑内容,尽管str和str1指向的地址不同,但是put进去仍然是覆盖不是添加。马上我们会根据源码进行分析。
/** * Constructs an empty <tt>HashMap</tt> with the specified initial * capacity and load factor. * * @param initialCapacity the initial capacity * @param loadFactor the load factor * @throws IllegalArgumentException if the initial capacity is negative * or the load factor is nonpositive */ 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); this.loadFactor = loadFactor; this.threshold = tableSizeFor(initialCapacity); }
在初始化过程中,如果初始化容量和装载因子都是用户自己设置的,那么会判断初始化容量,如果小于0会抛出异常,大于了2的30次方也就是最大容量时,会定为最大容量,判断装载因子,如果小于等于0或者他不是一个数字(通过Float.isNaN(loadFactor)),抛出异常,在最后也初始化了threshold的值
当然如果通过传Map进行初始化
public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); }
/** * Implements Map.putAll and Map constructor * * @param m the map * @param evict false when initially constructing this map, else * true (relayed to method afterNodeInsertion). */ final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) { int s = m.size(); if (s > 0) { if (table == null) { // pre-size float ft = ((float)s / loadFactor) + 1.0F; int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY); if (t > threshold) threshold = tableSizeFor(t); } else if (s > threshold) resize(); for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) { K key = e.getKey(); V value = e.getValue(); putVal(hash(key), key, value, false, evict); } } }
会调用putMapEntries(Map<?extends K, ? extends V> m, boolean evict)方法
在这里面首先通过获取map的size,来判断是否需要扩容,之后循环便利每一个元素放入hashmap中,使用的方法是putVal(hash(key), key, value, false, evict);,这个方法也是我们平时调用map.put(Kkey,V value)的核心。
public void putAll(Map<? extends K, ? extends V> m) { putMapEntries(m, true); }
我们也会看到,putAll方法的原理也是这个函数。
在第一个参数我们传的是hash(Key),
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
不同的key有着不同的hashCode(),只要hashCode()相同,hash一定相同,但是反之不成立,不同对象的hashCode()的hash是可能相同的,这就是所谓的hash冲突
那么为什么会出现相同的hash呢?
那就是(h=key.hashCode())^(h>>>16)
虽然每个元素的hashCode()是唯一的,但是他的二进制右移(>>>是带符号,>>不带符号)16位就会出现重复的。h>>>16这样只有超过2的16次方hash(key)才会有作用,也就是说在2的16次方内都为0。之后和hashCode进行异或。hash()就是为了让均匀分布,他会让1111 0000 0000 0000变得1111 1110 1110 1111
让”1”变得均匀点
下面我们看下hashmap的结构示意图
我们可以看到 每一个元素就是一个Node<K,V>,这个Node<K,V>实现了Map.Entry<K,V> 接口。在jdk1.7中,它是一个HashMapEntry<K,V>
在JDK1.8之前,HashMap采用桶+链表实现,本质就是采用数组+单向链表组合型的数据结构。它之所以有相当 快的查询速度主要是因为它是通过计算散列码来决定存储的位置。HashMap通过key的hashCode来计算hash值,不同的hash值就存在数组中不同的位置,当多个元素的hash值相同时(所谓hash冲突),就采用链表将它们串联起来(链表解决冲突),放置在该hash值所对应的数组位置上。在JDK1.8中,HashMap的存储结构已经发生变化,它采用数组+链表+红黑树这种组合型数据结构。当hash值发生冲突时,会采用链表或者红黑树解决冲突。当同一hash值的结点数小于8时,则采用链表,否则,采用红黑树。
我们现在对hashmap的存储结构现在应该有了一个初步了解了吧,那么我们就来看下,我们每次进行put(key)时到底是hashMap是如何处理的。
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
put中调用了
final VputVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict)
/** * Implements Map.put and related methods * * @param hash hash for key * @param key the key * @param value the value to put * @param onlyIfAbsent if true, don't change existing value * @param evict if false, the table is in creation mode. * @return previous value, or null if none */ final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
该方法中有Node<K,V>[]tab; Node<K,V> p;
tab就是数组,而p是每个桶
如果tab刚开始是null或者大小为0,则进行扩容操作resize(),返回值为Node<K,V>[],直接赋值给tab,初始化tab。
初始化之后通过位与运算(求余)找到put的index,如果该位置没有元素也就是tab[index]==null,那么tab[i] =newNode(hash, key, value, null);即put成功
当然我们知道hash冲突是有的,所以当tab[index]!=null时,也就发生了hash冲突
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p;
if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; }
第一个if其实考虑的是重复键,第二个if我们可以看到绿色的注释说的是在map中已经存在key了,所以这两步是对于已有key情况下的节点put的一个处理。
如果不是重复的,那么就看p是否是树节点,因为jdk1.8中采用的是红黑树,所以要考虑树节点,如果是树节点就进行树节点的put,e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key,value);对于树节点的插入我们这里就不多做解释了
如果上述情况都不是,那就是hash冲突并且使用链表处理了;。
for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; }
通过e=p.next进行一个链表遍历,,
如果等于null也就是说遍历到了末尾也没发现重复的key,那么就是就执行一个插入操作,是一个尾插法,jdk1.8之前是头插法,jdk1.8是尾插法,
那么为什么jdk1.8是头插,之前为头插法呢
1.8为什么尾插我觉得大家通过上面这段话应该都可以在知道原因吧,因为我已经遍历到了链表尾部了,尾插不就更省事吗?
可是有些人问了1.7是单独的数组加链表,那不应该也尾插吗?这就有一个效率问题了,因为jdk1.8每当节点>8时就会变为树,而树的遍历会更加快速,
而链表遍历最多也就是7次,效率还是很高的,可是1.7就不是这样了,如果你有10000个节点,那你如果尾插的话就需要遍历10000次,这是非常耗时的,所以1.7采用的是头插法。
再插入过程中,如果桶中节点个数大于树的阈值TREEIFY_THRESHOLD-1,就会进行树化。从链表变为红黑树
++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null;
最后进行一个判断,看size是否到达了扩容标准,如果达到了进行扩容resize();
resize():
如果为空,则按threshold分配空间,(默认是数组=16,装载因子=0.75f,阈值=16*0.75),否则,加倍后,每个容器中的元素在新table中要么呆在原索引处,
要么有一个2的次幂的位移(这也是保证了hashmap中的元素分配均匀)
到这里我们平常所用的put方法就结束了
下面我们看下get方法
public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; } /** * Implements Map.get and related methods * * @param hash hash for key * @param key the key * @return the node, or null if none */ final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; if ((e = first.next) != null) { if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }
get方法比较简单
主要是getNode(int hash,Object key)
直接判断hashmap中的桶是否为空,并且看tab[index]是否为空,如果为空则返回null
否则检查tab[index]处Node的属性,看key是否相等,相等返回改Node,不是则遍历该桶中的节点。
利用first.next遍历,如果是树节点则getTreeNode(hash,key),是链表节点的话遍历链表寻找。
相关文章推荐
- java 8 Hashmap深入解析 —— put get 方法源码
- java 8 Hashmap深入解析 —— put get 方法源码
- java 8 Hashmap深入解析 —— put get 方法源码
- java 8 Hashmap深入解析 —— put get 方法源码
- JDK源码之解读hashMap 的put和get方法的实现原理
- HashMap源码解析——get方法
- JDK源码之解读hashMap 的put和get方法的实现原理
- java hashmap源码学习二 put&get
- HashMap源码解析《一》put方法
- HashMap的put源码解析
- HashMap源码解析《三》get,remove方法
- Map容器——HashMap及常用API,及put,get方法解析,哈希码的产生和使用
- HashMap源码解析——put方法
- HashMap底层详解-001-数据结构、put、get
- 基于源码(jdk1.7)对HashMap的get()和put()的小结
- 随笔:深入理解HashMap——put和get方法的源码分析
- Java8 HashMap put方法源码解析
- hashmap的数据结构以及put和get
- 数据结构-04 HashMap源码解析
- [数据结构]--jdk1.8中HashMap源码解析