jdk8 32位_HashMap源码分析 jdk8

    HashMap可以说是我们在实际开发中最经常使用到的集合类,并且在面试中也是必问的知识点。这篇文章主要是根据JDK8的HashMap来进行分析。

一、HashMap源码分析

·  HashMap结构

public class HashMap<K, V> extends AbstractMap<K, V>implements Map<K, V>, Cloneable, Serializable {

    上图中可以看出,HashMap类继承自AbstractMap,并且实现了Map、Cloneble接口。查看Map接口的引用可以发现,除了HashMap外还有大量的类实现了该接口,比如ConcurrenHashMap、WeakHashMap、LinkedHashMap等。

·  HashMap内部变量

//hashMap默认初始容量static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16//最大容量 2的30次static final int MAXIMUM_CAPACITY = 1 << 30;//默认加载(扩容)因子static final float DEFAULT_LOAD_FACTOR = 0.75f;//树化阈值,当一个数组中的元素大于8个时,会转换成红黑树static final int TREEIFY_THRESHOLD = 8;//红黑树重新转换为链表阈值,当一个数组中的元素小于6时,会从红黑树变为链表static final int UNTREEIFY_THRESHOLD = 6;
//对于整个hashMap转换红黑树的阈值,当整个数组最大下标大于64时并且达到树化阈值时,会转为红黑树
static final int MIN_TREEIFY_CAPACITY = 64;
//存储元素的数组transient Node<K, V>[] table;//entrySet,主要是用来迭代transient SetK, V>> entrySet;//map中元素数量transient int size;//修改次数,通过这个字段判断有没有并发操作,如果有并发操作会抛出异常transient int modCount;//扩容临界值 元素大于该值时,需要扩容
int threshold;//加载因子,默认是上述指定的0.75,如果指定了加载因子就为指定的变量final float loadFactor;

    不用纠结这里的默认初始容量大小-16和加载因子-0.75,只不过这是经过大量计算所得出的最优解。

·  HashMap构造函数

    无参构造函数,默认设置加载因子为0.75。

//无参构造函数 加载因子设置默认值0.75public HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR;}
//指定初始容量大小的构造函数public HashMap(int initialCapacity) {this(initialCapacity, DEFAULT_LOAD_FACTOR);}

    指定初始容量大小,加载因子的构造函数。不过为了保证容量是在二的幂次上的,因此如果是自己指定的容量大小,会查找离指定的容量大小最近的一个二的幂次的数。

//指定初始容量大小、加载因子的构造函数public HashMap(int initialCapacity, float loadFactor) {//初始容量不能小于0,否则会抛出异常    if (initialCapacity < 0)throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity); //初始容量设置如果超过最大值,就指定为最大值 if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY; //加载因子不能小于0,否则抛出异常 if (loadFactor <= 0 || Float.isNaN(loadFactor))throw new IllegalArgumentException("Illegal load factor: " +
loadFactor); //指定加载因子参数 this.loadFactor = loadFactor; //计算扩容临界值 this.threshold = tableSizeFor(initialCapacity);}
//计算扩容临界值 找出距离cap最近的2的整数次幂的数值,比如cap为10,那么扩容临界值为16static final int tableSizeFor(int cap) {//假设cap为10 此时n=9    int n = cap - 1;    //n = 1001 |(1001>>>1) ==> n = 1001 | 100 = 1101 = 1+4+8 = 13    n |= n >>> 1;    //n = 1101 | (1101>>>2) ==> n = 1101 | 11 = 1111 = 15    n |= n >>> 2;    //n = 1111 | (1111>>>4) ==> n = 1111 | 0000 = 1111 = 15    n |= n >>> 4;    //n = 1111 | 0000 = 15    n |= n >>> 8;    //n = 1111 | 0000 = 15    n |= n >>> 16;    //最小返回2的0,最大返回2的30次,初次之外,返回的是计算结果+1    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;}

    指定初始化集合的构造函数,这时候加载因子为默认设置为0.75。

//指定初始化集合public HashMap(Map extends K, ? extends V> m) {//加载因子为默认0.75    this.loadFactor = DEFAULT_LOAD_FACTOR;    //存放元素    putMapEntries(m, false);}

·  HashMap内部类

(1)数组+单向链表结构

//链表结构内部类 维护了一个单向链表static class Node<K, V> implements Map.Entry<K, V> {//元素的hash值    final int hash;    //元素的key    final K key;    //元素的value    V value;    //下一个结点    Node<K, V> next;    Node(int hash, K key, V value, Node<K, V> next) {this.hash = hash;        this.key = key;        this.value = value;        this.next = next;    }public final K getKey() {return key;    }public final V getValue() {return value;    }public final String toString() {return key + "=" + value;    }public final int hashCode() {return Objects.hashCode(key) ^ Objects.hashCode(value);    }public final V setValue(V newValue) {V oldValue = value;        value = newValue;        return oldValue;    }//判断结点中是否有该元素    public final boolean equals(Object o) {if (o == this)return true;        //若o是Entry结构,需要校验key和value        if (o instanceof Map.Entry) {
Map.Entry, ?> e = (Map.Entry, ?>) o; if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))return true; }return false; }
}

fbdec5e53861156fbe571dc093afe5bf.png

(2)数组+红黑树结构

//内部红黑树结构static final class TreeNode<K, V> extends LinkedHashMap.Entry<K, V> {//父类结点    TreeNode<K, V> parent;    //左孩子结点    TreeNode<K, V> left;    //右孩子结点    TreeNode<K, V> right;    //前一个结点    TreeNode<K, V> prev;    boolean red;    TreeNode(int hash, K key, V val, Node<K, V> next) {super(hash, key, val, next);    }//查找红黑树的根结点    final TreeNode<K, V> root() {for (TreeNode<K, V> r = this, p; ; ) {if ((p = r.parent) == null)return r;            r = p;        }

}

}

68ef171abd212cac1520753f8f3c49b2.png

    红黑树数据结构的思想,在今日的下一篇的文章中有详细描述,这里不再具体阐述。

·  HashMap添加

public V put(K key, V value) {return putVal(hash(key), key, value, false, true);}

    putVal方法为put方法的核心方法,在调用putVal方法之前会先计算key的hash值。计算方法如下:

//计算key的hash值static final int hash(Object key) {int h;    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}

    hash值中,如果key为null,那么hash值就为0,否则hash值为key的hashCode值和key的hashCode右移16位进行异或运算,即通过hashCode值的高低位进行异或,一般hashCode值为32位二进制。

    计算hash值的目的主要是为了减少hash冲突,为下面确定key值在数组中的存放位置进行基础运算。这里也可以看到key值支持null值的存放,如果为null,他的hash值就为1。

//往map中存放元素final V putVal(int hash, K key, V value, boolean onlyIfAbsent,               boolean evict) {
Node<K, V>[] tab; Node<K, V> p; int n, i; //当前数组不存在,即map中的数组是在被使用时才进行加载的 - 懒加载 if ((tab = table) == null || (n = tab.length) == 0)//扩容/初始化 n = (tab = resize()).length; //经过hash值和数组容量-1进行与运算,计算当前key存放的位置。若为空,则构建链表结点用于存放。 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)//当前数组后面挂载的树形结构 即 红黑树,调用红黑树中的插入方法。成功就返回null,有值返回重复的值 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)//进行树化 treeifyBin(tab, hash); break; }//若当前值存在,即key相同,value也相同 if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))break; p = e; }
}//e不为空,说明链表或红黑树中有相同的值,则覆盖值 if (e != null) {V oldValue = e.value; if (!onlyIfAbsent || oldValue == null)
e.value = value; afterNodeAccess(e); //返回旧值 return oldValue; }
}//这里说明没有重复的值,操作+1 ++modCount; //判断容器中当前容器容量是否超过临界值 if (++size > threshold)//进行扩容 resize(); afterNodeInsertion(evict); return null;}

    putVal()作为核心插入元素,逻辑比较清晰。插入操作主要的流程为,先根据hashCode值的高低位进行异或运算求得hash值,再用hash值和数组最大下标即数组容量-1进行与运算可求出该key对应的数组下标。

    确定key所在位置后,需要判断元素是否存在,如果存在则不为null,不存在为null。如果为红黑树结构,则调用红黑树的插入方法进行插入元素。如果为链表结构,需要对链表进行循环查找,不存在则挂载到链表最后面。最后不管是红黑树结构或是链表结果,如果要插入的数值存在map中,则需要进行覆盖操作。

    插入成功后,判断是否需要扩容,进行扩容。

·  HashMap扩容

    扩容方法中存在两种扩容,一个是正常的扩容,即元素容量达到该扩容的大小时进行扩容。第二个是对刚初始化玩的map进行插入操作时,会进行初始化扩容,这里也可以看出map中数组的初始化是在被使用时才会进行加载-懒加载。

//扩容核心方法final Node<K, V>[] resize() {//扩容前的数组    Node<K, V>[] oldTab = table;    //原来数组长度    int oldCap = (oldTab == null) ? 0 : oldTab.length;    //原来的扩容临界值    int oldThr = threshold;    //初始化新的数组 和 新的扩容临界值    int newCap, newThr = 0;    //原数组长度大于0 - 即原数组有数据或非首次初始化数组    if (oldCap > 0) {//原数组已经达到最大容量,则不进行扩容,临界值设置为最大值。        if (oldCap >= MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;            return oldTab;            //在判断中计算新值 即 新值是原值<<1=原数组长度*2  新值必须小于最大值并且大于等于16        } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)//新的临界值为原来的临界值*2 newThr = oldThr << 1; } else if (oldThr > 0)//如果老的临界值大于0,指定新的临界值 newCap = oldThr; else {//初始化操作 数组容量为16,扩容临界值为16*0.75=12 newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); }//进入第一个if但是赋值失败的情况 - 即初始化容量小于16时的情况 if (newThr == 0) {//新的临界值为 扩容后的新容量 * 加载因子 float ft = (float) newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float) MAXIMUM_CAPACITY ?
(int) ft : Integer.MAX_VALUE); }//改变临界值 threshold = newThr; //初始化数组 Node<K, V>[] newTab = (Node<K, V>[]) new Node[newCap]; //新的数组为刚初始化后的数组 table = newTab; if (oldTab != null) {//遍历老数组,这里的目的是为了把原来的元素复制到新的地方 for (int j = 0; j < oldCap; ++j) {//记录老数组中的数据作为一个临时变量 Node<K, V> e; if ((e = oldTab[j]) != null) {//置null,方便回收 oldTab[j] = null; //链表结点只存在一个结点 if (e.next == null)//重新计算hash值,并放到对应的数组中的链表 newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode)//如果之前的数据中的数据是红黑树,那么将他移到新的数组位置中 ((TreeNode<K, V>) e).split(this, newTab, j, oldCap); else {//进入这里则为链表 Node<K, V> loHead = null, loTail = null; Node<K, V> hiHead = null, hiTail = null; Node<K, V> next; //这里主要就是遍历原来的链表,并放到新的数组中 do {
next = e.next; if ((e.hash & oldCap) == 0) {if (loTail == null)
loHead = e; else loTail.next = e; loTail = e; } else {if (hiTail == null)
hiHead = e; else hiTail.next = e; hiTail = e; }
} while ((e = next) != null); if (loTail != null) {
loTail.next = null; newTab[j] = loHead; }if (hiTail != null) {
hiTail.next = null; //链表数组下标位置确定为 原来容量+原来下标 newTab[j + oldCap] = hiHead; }
}
}
}
}return newTab;}

    扩容的过程源码中可以看到,扩容后的数组大小为原来的2倍,扩容后扩容临界值的大小也为原来的2倍。并且扩容后需要进行元素的复制,将链表或红黑色放到新的数组中,数组下标的计算为原数组容量+原下标。可以看到这里扩容的扩展和jdk7相比少了很多计算的过程。

//红黑树扩容时操作final void split(java.util.HashMap<K, V> map, Node<K, V>[] tab, int index, int bit) {//扩容时当前结点    TreeNode<K, V> b = this;    //原数组位置的结点    TreeNode<K, V> loHead = null, loTail = null;    //新数组位置的结点    TreeNode<K, V> hiHead = null, hiTail = null;    int lc = 0, hc = 0;    //遍历红黑树    for (TreeNode<K, V> e = b, next; e != null; e = next) {//当前结点的下一个结点        next = (TreeNode<K, V>) e.next;        //置为null 方便gc        e.next = null;        //扩容后位置与原位置相同        if ((e.hash & bit) == 0) {//结点复制 要么为第一个结点,要么为下一个结点            if ((e.prev = loTail) == null)
loHead = e; else loTail.next = e; loTail = e; ++lc; } else {//结点复制 要么为第一个结点,要么为下一个结点 if ((e.prev = hiTail) == null)
hiHead = e; else hiTail.next = e; hiTail = e; ++hc; }
}//原数组结点位置不为空 if (loHead != null) {//判断树结点元素个数是否小于等于6 if (lc <= UNTREEIFY_THRESHOLD)//转为链表 tab[index] = loHead.untreeify(map); else {
tab[index] = loHead; if (hiHead != null)//老的红黑树结构被改变,重新转换为红黑树 loHead.treeify(tab); }
}//新数组结点位置不为空,判断和上面一样 if (hiHead != null) {//判断是否需要转换为链表 if (hc <= UNTREEIFY_THRESHOLD)
tab[index + bit] = hiHead.untreeify(map); else {
tab[index + bit] = hiHead; if (loHead != null)//是否需要重新转换为红黑树 hiHead.treeify(tab); }
}
}

·  红黑树的树化

//链表转换为红黑树final void treeifyBin(Node<K, V>[] tab, int hash) {int n, index;    Node<K, V> e;    //数组为空 或者 数组最大下标小于64时,会先扩容 而不是转为红黑树    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize(); //确定转换红黑树的链表结点的位置 else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K, V> hd = null, tl = null; do {//构建树结点 TreeNode<K, V> p = replacementTreeNode(e, null); if (tl == null)//指定头结点 hd = p; else {//当前阶段的前一个结点设置为上一个结点 p.prev = tl; //上一个结点的下一个结点设置为当前结点 tl.next = p; }
tl = p; } while ((e = e.next) != null); if ((tab[index] = hd) != null)//以hd为头结点构建红黑树 hd.treeify(tab); }
}
//构建红黑树 核心代码final void treeify(Node<K, V>[] tab) {
TreeNode<K, V> root = null; //调用该方法的树结点,进行遍历 for (TreeNode<K, V> x = this, next; x != null; x = next) {//next为当前遍历结点的下一个结点 next = (TreeNode<K, V>) x.next; //初始化左右孩子结点 x.left = x.right = null; //若根节点为空 if (root == null) {//没有父节点 x.parent = null; //并且根节点为黑色 x.red = false; //设置根节点 root = x; } else {//当前循环结点的key K k = x.key; //当前循环结点的hash int h = x.hash; Class> kc = null; //循环树结点,这里主要是根据二分查找找到该放置的位置 for (TreeNode<K, V> p = root; ; ) {int dir, ph; K pk = p.key; //确定dir,当前循环值大于要查找结点的值,则查询左孩子结点,为-1 if ((ph = p.hash) > h)
dir = -1; else if (ph < h)//当前循环值小于要查找的结点的值,则查询右孩子结点,为1 dir = 1; //自定义查找 else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk); //当前循环结点 根据dir确定是左孩子结点还是右孩子结点 TreeNode<K, V> xp = p; if ((p = (dir <= 0) ? p.left : p.right) == null) {//指定要插入的结点的父亲结点为该循环结点 x.parent = xp; if (dir <= 0)
xp.left = x; else xp.right = x; //平衡插入红黑树 root = balanceInsertion(root, x); break; }
}
}
}//调整红黑树。root结点设置为map数组中的头结点 moveRootToFront(tab, root);}
//将红黑树的根结点设置元素中的第一个元素static <K, V> void moveRootToFront(Node<K, V>[] tab, TreeNode<K, V> root) {int n;    //根结点不能为空,并且hashMap数组不能为空    if (root != null && tab != null && (n = tab.length) > 0) {//计算根结点hash值在数组中的位置        int index = (n - 1) & root.hash;        //获取数组中的第一个结点        TreeNode<K, V> first = (TreeNode<K, V>) tab[index];        //指定的根节点 不为第一个元素,进行替换操作        if (root != first) {
Node<K, V> rn; //根节点替换 tab[index] = root; //获取根结点的前一个元素 TreeNode<K, V> rp = root.prev; if ((rn = root.next) != null)//把root从链表中删除 ((TreeNode<K, V>) rn).prev = rp; if (rp != null)
rp.next = rn; //将原来的根节点指向新的根节点 if (first != null)
first.prev = root; root.next = first; root.prev = null; }//根节点校验,放置并发时破坏结构 assert checkInvariants(root); }
}

·  HashMap查询

//根据key查询valuepublic V get(Object key) {
Node<K, V> e; return (e = getNode(hash(key), key)) == null ? null : e.value;}

    和插入一样,先根据key的hashCode的高低位进行异或运算取得hash值。

//查询元素方法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 &&
((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;}

    查询过程中会根据hash值和数组下标进行与运算求的下标位置,然后优先判断是否为头结点,如果不是再根据是树结点还是链表节点进行遍历查询。

//红黑树中查询方法final TreeNode<K, V> find(int h, Object k, Class> kc) {//这个p为调用方法时传下来的根结点    TreeNode<K, V> p = this;    do {int ph, dir;        K pk;        //pl-根结点的左孩子结点 pr-根结点的右孩子结点        TreeNode<K, V> pl = p.left, pr = p.right, q;        if ((ph = p.hash) > h)//hash值大于要查找的值 说明查找结点为当前结点左孩子            p = pl;        else if (ph < h)//hash值小于要查找的值 说明查找结点为当前结点右孩子            p = pr;        else if ((pk = p.key) == k || (k != null && k.equals(pk)))//说明hash值相同,判断key是否相同            return p;        else if (pl == null)//左孩子为空了,则指定下一个要遍历的值为右孩子            p = pr;        else if (pr == null)//右孩子为空了,则指定下一个要遍历的值为左孩子            p = pl;        else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)//判断查询左孩子还是查找右孩子 p = (dir < 0) ? pl : pr; else if ((q = pr.find(h, k, kc)) != null)//调用红黑树插入时的查找方法进行查询 return q; else //指定为下一个遍历为左孩子 p = pl; } while (p != null); return null;}

    查找红黑树中的元素的过程和思想,其实就是二分索引的思想。他与插入红黑树时确定插入的位置的思想一样,想了解的朋友可以看今日推送的第二条文章的红黑树内容有具体过程分析。查找的过程,总体不是很难。

·  HashMap删除

public final boolean remove(Object key) {return removeNode(hash(key), key, null, false, true) != null;}

    先计算hash值。

//删除元素 核心方法final Node<K, V> removeNode(int hash, Object key, Object value,                            boolean matchValue, boolean movable) {
Node<K, V>[] tab; Node<K, V> p; int n, index; //数组不为空,并且hash值对应的数组中的数据也不为空 if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K, V> node = null, e; K k; V v; //确定要删除的元素是否为头结点 if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p; else if ((e = p.next) != null) {//查找下一个结点 if (p instanceof TreeNode)//红黑树查找结点 node = ((TreeNode<K, V>) p).getTreeNode(hash, key); else {do {//链表依次遍历查找结点 if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e; break; }
p = e; } while ((e = e.next) != null); }
}if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {if (node instanceof TreeNode)//红黑树删除结点方法 ((TreeNode<K, V>) node).removeTreeNode(this, tab, movable); else if (node == p)//如果在链表中,为头结点,则重新设置头结点为原头结点的下一个结点 tab[index] = node.next; else //更改结点指向 p.next = node.next; //修改记录+1 ++modCount; //当前容量大小减1 --size; //通过HashMap的子类方法实现 主要为删除结点处理 afterNodeRemoval(node); return node; }
}return null;}

、HashMap总结

    JDK8开始,HashMap有了本质上的变化,底层的数据结构采用了数组+链表/红黑树的形式。之所以采用了红黑树,是为了解决hash冲突严重时,链表过长导致查询性能降低的情况。

    在源码中我也分析了什么时候会采用数组+链表,什么时候采用数组+红黑树的情况。根据是根据2个关键阈值参数,并不只是链表长度大于8时就会转换为红黑树。如果当map中数组下标小于64时会优先扩容。只有当数组下标大于等于64的情况,并且链表长度大于8使,会将链表进行树化。在扩容时,如果当红黑树的结点长度小于等于6个时,会重新退回到链表结构。这里6和8我没有深入了解,应该是保证时间和空间最好的权衡。

    除了进行树化的3个阈值之外,还需要注意的是threshold这个值,因为在构造函数源码中可以看到,threshold这个值在构造函数中是作为存放初始化时的初始容量来使用的,因为map中采用了懒加载来初始化数组,只有当调用了putVal方法才会调用扩容方法来对数组进行初始化。初始化后的该值才被作为扩容临界值 = 容量大小(16) * 扩容加载因子(0.75),超过该值时需要进行扩容。另外在初始化时,确定数组大小是根据距离指定的容量大小的2的次幂最接近的数(计算过程其实就是用指定的容量大小进行4次移位和或运算),如果未指定,就使用默认值16。

    Map中可以指定null值作为key和value,并且null值的hash值永远为1,非null值的计算是根据key的hashCode的高低位进行与运算求的hash值,并通过hash值和数组最大下标值进行与运算,求得key对应的位置。在扩容后,原来的key对应的下标也不需要重新计算,新的下标其实就为原来的数组大小+原来所处数组的下标就可以确定新的数组下标为多少。

    Map中做的一些列的扩容、构造函数时计算初始容量等操作其实就是为了减少hash碰撞、减少频繁扩容、浪费资源的情况,最终的目的都是为了提高map的查询效率。

    最后说一句,map中所有的方法都不是同步的,因此他是线程不安全的,如果在并发环境下要对同一个map对象进行操作,要考虑到不安全情况,或者换一个数据结构可以使用hashTable或者cocrrentHashMap。

HashMap 是一种哈希表数据结构,它实现了 Map 接口,可以存储键值对。下面是 JDK 8 中 HashMap源码详解。 1. 基本概念 哈希表是一种基于散列原理的数据结构,它通过将关键字映射到表中一个位置来访问记录,以加快查找的速度。在哈希表中,关键字被映射到一个特定的位置,这个位置就称为哈希地址或散列地址。哈希表的基本操作包括插入、删除和查找。 2. 类结构 HashMap 类结构如下: ``` public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { ... } ``` HashMap 继承了 AbstractMap 类,并实现了 Map 接口,同时还实现了 Cloneable 和 Serializable 接口,表示该类可以被克隆和序列化。 3. 数据结构 JDK 8 中的 HashMap 采用数组 + 链表(或红黑树)的结构来实现哈希表。具体来说,它使用了一个 Entry 数组来存储键值对,每个 Entry 对象包含一个 key 和一个 value,以及一个指向下一个 Entry 对象的指针。当多个 Entry 对象的哈希地址相同时,它们会被放入同一个链表中,这样就可以通过链表来解决哈希冲突的问题。在 JDK 8 中,当链表长度超过阈值(默认为 8)时,链表会被转化为红黑树,以提高查找的效率。 4. 哈希函数 HashMap 的哈希函数是通过对 key 的 hashCode() 方法返回值进行计算得到的。具体来说,它使用了一个称为扰动函数的算法来增加哈希值的随机性,以充分利用数组的空间。在 JDK 8 中,HashMap 使用了以下扰动函数: ``` static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } ``` 其中,^ 表示按位异或,>>> 表示无符号右移。这个函数的作用是将 key 的哈希值进行扰动,以减少哈希冲突的概率。 5. 插入操作 HashMap 的插入操作是通过 put() 方法实现的。具体来说,它会先计算出 key 的哈希值,然后根据哈希值计算出在数组中的位置。如果该位置是空的,就直接将 Entry 对象插入到该位置;否则,就在该位置对应的链表(或红黑树)中查找是否已经存在具有相同 key 的 Entry 对象,如果存在,则更新其 value 值,否则将新的 Entry 对象插入到链表(或红黑树)的末尾。 6. 查找操作 HashMap 的查找操作是通过 get() 方法实现的。具体来说,它会先计算出 key 的哈希值,然后根据哈希值计算出在数组中的位置。如果该位置为空,就直接返回 null;否则,就在该位置对应的链表(或红黑树)中查找是否存在具有相同 key 的 Entry 对象,如果存在,则返回其 value 值,否则返回 null。 7. 删除操作 HashMap 的删除操作是通过 remove() 方法实现的。具体来说,它会先计算出 key 的哈希值,然后根据哈希值计算出在数组中的位置。然后,在该位置对应的链表(或红黑树)中查找是否存在具有相同 key 的 Entry 对象,如果存在,则将其删除,否则什么也不做。 8. 总结 以上就是 JDK 8 中 HashMap源码详解。需要注意的是,哈希表虽然可以加快查找的速度,但是在处理哈希冲突、扩容等问题上也存在一定的复杂性,因此在使用 HashMap 时需要注意其内部实现细节,以便更好地理解其性能和使用方法。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值