Java集合类:HashMap (基于JDK1.8)

前言

今天咱们来学习Java中较为经常使用的集合类 HashMap
另外说明一下,本文的 HashMap 源码是基于Jdk1.8版本的,若是没有特别说明的话,以后的集合类源码解析都是1.8的版本。html

HashMap的数据结构

打开HashMap源码文件,能够看到它是继承自 AbstractMap,并实现了Java集合的根接口Map,以及Cloneable和Serializable接口,因此HashMap能够被序列化。
在这里插入图片描述
HashMap的底层结构是哈希表的具体实现,经过相应的哈希运算就能够很快查询到目标元素在表中的位置,拥有很快的查询速度,所以,HashMap被普遍应用于平常的开发中。理想的状况就是一个元素对应一个Hash值,这样的查询效果是最优的。web

但实际这是不可能的,由于哈希表存在“hash (哈希) 冲突“ 的问题。当发生hash冲突时,HashMap采用 “拉链法“ 进行解决,也就是数组加链表的结构。在HashMap的代码注释中,数组中的元素用 “bucket” (中文读做 桶) 来称呼,而哈希函数的做用就是将key寻址到buckets中的一个位置,若是一个 bucket 有多个元素,那么就以链表的形式存储(jdk1.8以前单纯是这样)。面试

这是HashMap的存储结构图:
在这里插入图片描述segmentfault

关于 “拉链法” 和 “hash冲突” 的知识点有疑问的读者能够看下我以前的文章 数据结构:哈希表以及哈希冲突的解决方案
为了方便,下文的 “bucket“ 都用 “桶“ 替代。数组

深刻源码

两个参数

在具体学习源码以前,咱们须要先了解两个HashMap中的两个重要参数,“初识容量” 和 “加载因子”,安全

初识容量是指数组的数量。加载因子则决定了 HashMap 中的元素在达到多少比例后能够扩容 (rehash),当HashMap的元素数量超过了加载因子与当前容量的乘积后,就须要对哈希表作扩容操做。数据结构

在HashMap中,加载因子默认是0.75,这是结合时间、空间成本均衡考虑后的折中方案,由于 加载因子太大的话发生冲突的可能性会变大,查找的效率反而低;过小的话频繁rehash,下降性能。在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减小 rehash 操做次数。若是初始容量大于最大条目数除以加载因子,则不会发生 rehash 操做。app

成员变量

好了,前面说了那么多,如今开始深刻源码学习吧,先了解一下HashMap的主要的成员变量:svg

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75F;
static final int TREEIFY_THRESHOLD = 8;
static final int MIN_TREEIFY_CAPACITY = 64;
transient HashMap.Node<K, V>[] table;
transient Set<Entry<K, V>> entrySet;
transient int size;
int threshold;
final float loadFactor;

能够看出,HashMap主要的成员变量比较多,有些变量还初始化了值,下面一个个来作解释。函数

DEFAULT_INITIAL_CAPACITY:默认初识容量 1 << 4 ,也就是16,必须是2的整数次方。

DEFAULT_LOAD_FACTOR:默认加载因子,大小为0.75。

MAXIMUM_CAPACITY:最大容量, 2^ 30 次方。

TREEIFY_THRESHOLD :树形阈值,大于这个数就要树形化,也就是转成红黑树。

MIN_TREEIFY_CAPACITY:树形最小容量。

table:哈希表的连接数组,对应桶的下标。

entrySet:键值对集合。

size:键值对的数量,也就是HashMap的大小。

threshold:阈值,下次须要扩容时的值,等于 容量*加载因子。

loadFactor:加载因子。

介绍玩变量,下面介绍HashMap的构造方法。

四个构造方法

HashMap共有四个构造方法,代码以下:

//加载默认大小的加载因子
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; 
}
//加载默认大小的加载因子,并建立一个内容为参数 m 的内容的哈希表
public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        //添加整个集合
        putMapEntries(m, false);
}
//指定容量和加载因子
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);
}
//指定容量,加载因子默认大小
public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

不难发现,上面第三个构造函数能够自定义加载因子和容量,首先判断传入的加载因子是否符合要求,而后根据制定的容量执行 tableSizeFor() 方法,它会根据容量来指定阈值,为什么要多这一步呢?

由于buckets数组的大小约束对于整个HashMap都相当重要,为了防止传入一个不是2次幂的整数,必需要有所防范。tableSizeFor()函数会尝试修正一个整数,并转换为离该整数最近的2次幂。

static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

好比传入一个整数244,通过位移,或运算后会返回最近的2次幂 256
在这里插入图片描述

插入数据的方法:put()

在集合中最经常使用的操做是存储数据,也就是插入元素的过程,在HashMap中,插入数据用的是 put() 方法。

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

put方法没有作多余的操做,只是传入 keyvalue 还有 hash 值 进入到 putVal方法中并返回对应的值,点击进入方法,一步步跟进源码:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //哈希表若是为空,就作扩容操做 resize()
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    //要插入位置没有元素,直接新建一个包含key的节点
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    //若是要插入的桶已经有元素,替换
    else {
        Node<K,V> e; K k;
        //key要插入的位置发生碰撞,让e指向p
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        //没碰撞,可是p是属于红黑树的节点,执行putTreeVal()方法
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        //p是链表节点,遍历链表,查找并替换
        else {
        	//遍历数组,若是链表长度达到8,转换成红黑树
            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;
                }
                // 找到目标节点,退出循环,e指向p
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        // 节点已存在,替换value,并返回旧value
        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;
}

代码看上去有点复杂,参数有点乱,但理清逻辑后容易理解多了,源码大概的逻辑以下:

  • 先调用 hash() 方法计算哈希值
  • 而后调用 putVal() 方法中根据哈希值进行相关操做
  • 若是当前 哈希表内容为空,作扩容
  • 若是要插入的桶中没有元素,新建个节点并放进去
  • 不然从要插入的桶中第一个元素开始查找(这里为何是第一个元素,下面会讲到)
    1. 若是没有碰撞,赋值给e,结束查找
    2. 有碰撞,并且当前采用的仍是 红黑树的节点,调用 putTreeVal() 进行插入
    3. 链表节点的话从传统的链表数组中查找、找到赋值给e,结束
    4. 若是链表长度达到8,转换成红黑树
  • 最后检查是否须要扩容

put方法的代码中有几个关键的方法,分别是:

  1. hash():哈希函数,计算key对应的位置
  2. resize():扩容
  3. putTreeVal():插入红黑树的节点
  4. treeifyBin():树形化容器

前面两个是HashMap的桶链表操做的核心方法,后面的方法是Jdk1.8以后有关红黑树的操做,后面会讲到,先来看前两个方法。

哈希函数:hash()

hash() 方法是HashMap 中的核心函数,在存储数据时,将key传入中进行运算,得出key的哈希值,经过这个哈希值运算才能获取key应该放置在 “桶” 的哪一个位置,下面是方法的源码:

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

从源码中能够看出,传入key以后,hash() 会获取key的hashCode进行无符号右移 16 位,而后进行按位异或,并把运算后的值返回,这个值就是key的哈希值。这样运算是为了减小碰撞冲突,由于大部分元素的hashCode在低位是相同的,不作处理的话很容易形成冲突。

以后还须要把 hash() 的返回值与table.length - 1作与运算,获得的结果便是数组的下标(为何这么算,下面会说),在上面的 putVal() 方法中就能够看到有这样的代码操做,举个例子图:
在这里插入图片描述
table.length - 1就像是一个低位掩码(这个设计也优化了扩容操做的性能),它和hash()作与操做时必然会将高位屏蔽(由于一个HashMap不可能有特别大的buckets数组,至少在不断自动扩容以前是不可能的,因此table.length - 1的大部分高位都为0),只保留低位,这样一来就老是只有最低的几位是有效的,就算你的hashCode()实现得再好也难以免发生碰撞。这时,hash()函数的价值就体现出来了,它对hash code的低位添加了随机性而且混合了高位的部分特征,显著减小了碰撞冲突的发生。
另外,在putVal方法的源码中,咱们能够看到有这样一段代码

if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);

上面的注释也说明了,这是检测要插入位置是否有元素,没有的话直接新建一个包含key的节点,那么这里为何要用 i = (n - 1) & hash 做为索引运算呢?

下面这段解释摘自http://www.importnew.com/29724.html

这实际上是一种优化手段,因为数组的大小永远是一个2次幂,在扩容以后,一个元素的新索引要么是在原位置,要么就是在原位置加上扩容前的容量。这个方法的巧妙之处全在于&运算,以前提到过&运算只会关注n
– 1(n =
数组长度)的有效位,当扩容以后,n的有效位相比以前会多增长一位(n会变成以前的二倍,因此确保数组长度永远是2次幂很重要),而后只须要判断hash在新增的有效位的位置是0仍是1就能够算出新的索引位置,若是是0,那么索引没有发生变化,若是是1,索引就为原索引加上扩容前的容量。

效果图以下:

在这里插入图片描述
这样在每次扩容时都不用从新计算hash,省去了很多时间,并且新增有效位是0仍是1是带有随机性的,以前两个碰撞的Entry又有可能在扩容时再次均匀地散布开,真可谓是很是精妙的设计。

动态扩容:resize()

在HashMap中,初始化数组或者添加元素个数超过阈值时都会触发 resize() 方法,它的做用是动态扩容,下面是方法的源码:

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) {
    	//超过最大值不会扩容,把阈值设置为int的最大数
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        //向左移动1位扩大为原来2倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    //旧数组大小为0,旧阈值>0,说明以前建立了哈希表但没有添加元素,初始化容量等于阈值
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
    	//旧容量、旧阈值都是0,说明还没建立哈希表,容量为默认容量,阈值为 容量*加载因子
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    //新阈值尚未值,从新根据新的容量newCap计算大小
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    //不为空,表明是扩容操做
    if (oldTab != null) {
    	//遍历旧数组的每个‘桶’,移动到新数组newTab
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                //节点是树形节点,须要对红黑树进行拆分
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                //普通链表节点,遍历链表,并将链表节点按原顺序进行分组
                    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;
}

上面的源码有点长,但整体逻辑就三步:

  1. 计算新桶数组的容量大小 newCap 和新阈值 newThr
  2. 根据计算出的 newCap 建立新的桶数组,并初始化桶的数组table
  3. 将键值对节点从新映射到新的桶数组里。若是节点是 TreeNode 类型,则须要拆分成黑树 (调用**split()**方法 )。若是是普通节点,则节点按原顺序进行分组。

前面两步的逻辑比较简单,这里很少叙述。重点是第三点,涉及到了红黑树的拆分,这是由于扩容后,桶数组变多了,原有的数组上元素较多的红黑树就须要从新拆分,映射成链表,防止单个桶的元素过多。

红黑树的拆分是调用TreeNode.split() 来实现的,这里不单独讲。放到后面的红黑树一块儿分析。

节点树化、红黑树的拆分

红黑树的引进是HashMap 在 Jdk1.8以后最大的变化,在1.8之前,HashMap的数据结构就是数组加链表,某个桶的链表有可能由于数据过多而致使链表过长,遍历的效率低下,1.8以后,HashMap对链表的长度作了处理,当链表长度超过8时,自动转换为红黑树,有效的提高了HashMap的性能。

但红黑树的引进也使得代码的复杂度提升了很多,添加了有关红黑树的操做方法。本文只针对这些方法来作解析,不针对红黑树自己作展开,想了解红黑树的读者能够看我以前的文章

数据结构:红黑树的结构以及方法剖析 (上) 以及 数据结构:红黑树的结构以及方法剖析 (下)

节点树化

HashMap中的树节点的代码用 TreeNode 表示:

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
    TreeNode<K,V> parent;  // red-black tree links
    TreeNode<K,V> left;
    TreeNode<K,V> right;
    TreeNode<K,V> prev;    // needed to unlink next upon deletion
    boolean red;
    TreeNode(int hash, K key, V val, Node<K,V> next) {
        super(hash, key, val, next);
    }

能够看到就是个红黑树节点,有父亲、左右孩子、前一个元素的节点,还有个颜色值。知道节点的结构后,咱们来看有关红黑树的一些操做方法。

先来分析下树化的代码:

//将普通的链表转化为树形节点链表
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    // 桶数组容量小于 MIN_TREEIFY_CAPACITY,优先进行扩容而不是树化
    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
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        if ((tab[index] = hd) != null)
        	// 树形节点不为空,转换为红黑树
            hd.treeify(tab);
    }
}
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
        return new TreeNode<>(p.hash, p.key, p.value, next);
    }

上面的代码并不太复杂,大体逻辑是根据hash表的元素个数判断是须要扩容仍是树形化,而后依次调用不一样的代码执行。

值得注意的是,在判断容器是否须要树形化的标准是链表长度须要大于或等于 MIN_TREEIFY_CAPACITY,前面也说了,它是HashMap的成员变量,初始值是64,那么为何要知足这个条件才会树化呢?

下面这段摘自http://www.javashuo.com/article/p-hbiddcap-mp.html

当桶数组容量比较小时,键值对节点 hash
的碰撞率可能会比较高,进而致使链表长度较长。这个时候应该优先扩容,而不是立马树化。毕竟高碰撞率是由于桶数组容量较小引发的,这个是主因。容量小时,优先扩容能够避免一些列的没必要要的树化过程。同时,桶容量较小时,扩容会比较频繁,扩容时须要拆分成黑树并从新映射。因此在桶容量比较小的状况下,将长链表转成红黑树是一件吃力不讨好的事。

因此,HashMap的树化过程也是尽可能的考虑了容器性能,再看回上面的代码,链表树化以前是先把节点转为树形节点,而后再调用 treeify() 转换为红黑树,而且树形节点TreeNode 继承自 Node 类,因此 TreeNode 仍然包含 next 引用,原链表的节点顺序最终经过 next 引用被保存下来。

下面看下转换红黑树的过程:

final void treeify(Node<K,V>[] tab) {
    TreeNode<K,V> root = null;
    for (TreeNode<K,V> x = this, next; x != null; x = next) {
        next = (TreeNode<K,V>)x.next;
        x.left = x.right = null;
        if (root == null) {		//第一次进入循环,肯定头节点,而且是黑色
            x.parent = null;
            x.red = false;
            root = x;
        }	
        else {		//后面进入循环走的逻辑,x 指向树中的某个节点
            K k = x.key;
            int h = x.hash;
            Class<?> kc = null;
            //从根节点开始,遍历全部节点跟当前节点 x 比较,调整位置,
            for (TreeNode<K,V> p = root;;) {
                int dir, ph;
                K pk = p.key;
                if ((ph = p.hash) > h)	//当比较节点的哈希值比 x 大时, dir 为 -1
                    dir = -1;
                else if (ph < h)		//哈希值比 x 小时 dir 为 1
                    dir = 1;
                else if ((kc == null &&
                          (kc = comparableClassFor(k)) == null) ||
                         (dir = compareComparables(kc, k, pk)) == 0)
                    // 比较节点和x的key
                    dir = tieBreakOrder(k, pk);

                TreeNode<K,V> xp = p;
                //把 当前节点变成 x 的父亲
                //若是当前比较节点的哈希值比 x 大,x 就是左孩子,不然 x 是右孩子
                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;
                }
            }
        }
    }
    moveRootToFront(tab, root);
}

能够看到,代码的整体逻辑就是拿树中的节点与当前节点作比较,进而肯定节点在树中的位置,具体实现的细节仍是比较复杂的,这里不一一展开了。

红黑树拆分

介绍了节点的树化后,咱们来学习下红黑树的拆分过程,HashMap扩容后,普通的节点须要从新映射,红黑树节点也不例外。

在将普通链表转成红黑树时,HashMap 经过两个额外的引用 next 和 prev 保留了原链表的节点顺序。这样再对红黑树进行从新映射时,彻底能够按照映射链表的方式进行。这样就避免了将红黑树转成链表后再进行映射,无形中提升了效率。

下面看一下拆分的方法源码:

//map 容器自己
//tab 表示保存桶头结点的哈希表
//index 表示从哪一个位置开始修剪
//bit 要修剪的位数(哈希值)
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
    TreeNode<K,V> b = this;
    // Relink into lo and hi lists, preserving order
    // 修剪后的两个链表,下面用lo树和hi树来替代
    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;
        e.next = null;
        //若是当前节点哈希值的最后一位等于要修剪的 bit 值,用于区分位于哪一个桶
        if ((e.hash & bit) == 0) {
        	//把节点放到lo树的结尾
            if ((e.prev = loTail) == null)
                loHead = e;
            else
                loTail.next = e;
            loTail = e;
            ++lc;
        }
        //把当前节点放到hi树
        else {
            if ((e.prev = hiTail) == null)
                hiHead = e;
            else
                hiTail.next = e;
            hiTail = e;
            ++hc;
        }
    }
	
    if (loHead != null) {
    	// 若是 loHead 不为空,且链表长度小于等于 6,则将红黑树转成链表
        if (lc <= UNTREEIFY_THRESHOLD)
            tab[index] = loHead.untreeify(map);
        else {
            tab[index] = loHead;
            /* 
             * hiHead != null 时,代表扩容后,
             * 有些节点不在原位置上了,须要从新树化
             */
            if (hiHead != null) // (else is already treeified)
                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);
        }
    }
}

源码的逻辑大概是这样:拆分后,将红黑树拆分红两条由 TreeNode 组成的链表(hi树和lo树)。若是链表长度小于 UNTREEIFY_THRESHOLD,则将链表转换成普通链表。不然根据条件从新将 TreeNode 链表树化。这里用两张图来展现一下拆分先后的变化

红黑树拆分前:
在这里插入图片描述

拆分后:
在这里插入图片描述

至此,有关红黑树的一些转换操做就介绍完毕了,除此以外,hashMap还提供了不少操做红黑树的方法,原理都差很少,读者们能够本身去研究。

总结

HashMap的源码解析就告一段落了,最后,总结一下HashMap的一些特性:

一、HashMap 容许 key, value 为 null;

二、HashMap源码里没有作同步操做,多个线程操做可能会出现线程安全的问题,建议用Collections.synchronizedMap 来包装,变成线程安全的Map,例如:

Map map = Collections.synchronizedMap(new HashMap<String,String>());

三、Jdk1.7之前,当HashMap中某个桶的结构为链表时,遍历的时间复杂度为O(n),1.8以后,桶中过多元素的话会转换成了红黑树,这时候的遍历时间复杂度就是O(logn)。

心得

最后,说下心得,老实说,在写这篇文章以前,我对HashMap只是的了解仅仅停留在用过的层面,没有对源码作深刻的了解,直到心血来潮想学习下Java的集合类才去看HashMap的源码,看完源码后,我被深深的震撼了,说实话,我没想过平时最多见的工具类的源码是这么复杂,一个HashMap就涉及到了如此众多的技术知识,好比红黑树,链表转换,hash运算等,经过简单的代码就整合了这些知识点,并且还保证了HashMap的高效性能。说实话,我对设计者是很是佩服的,估计今生我都写不出如此牛逼的代码。我也终于能理解为何那么多公司面试时很喜欢问集合类的底层实现了,由于集合中涉及的技术知识是很是高深的,若能吃透集合类的源码,那人能不NB吗?

最后,感谢这几位大神的技术文章

https://blog.csdn.net/u011240877/article/details/53351188

http://www.javashuo.com/article/p-hbiddcap-mp.html

http://www.importnew.com/29724.html

本文分享 CSDN - 鄙人薛某。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索