Map之hashmap源码分析

本文深入分析了Java中HashMap的工作原理,包括其内部数据结构、在JDK 1.8中的改进,以及关键操作如插入、查找和删除的实现细节。


HashMap作为Java编程中最为常用的工具类之一,本文将从JDK1.8源码的层面对HashMap进行分析。

全限定类名:java.util.HashMap

一:HashMap概述

HashMap是实现了KEY-VALUE的非线程安全的数据结构。允许使用 null 值和 null 键。

二:HahsMap的内部是以何种形式存储数据的.

根据每个segment包含元素的具体数量以及MIN_TREEIFY_CAPACITY参数的限定共同决定使用链表还是红黑树。(在1.7以及之前只存在链表结构。关于红黑树的数据结构这边不予以详细介绍)

三:JDK1.8的HashMap与1.7中有何区别

JDK7与JDK8中HashMap实现的最大区别就是对于冲突的处理方法。由JDK7中的链表变为了JDK8中的链表、红黑树并存。

四:哪些优秀的内容本文没有提及的

红黑树的相关数据结构,实现原理,查询插入删除操作本文没有提及。
JDK8引入的函数式编程,包括default的拓展方法,map等函数的使用本文没有提及。
关于序列化和反序列化的具体内容,包括writeObject和readObject方法。

五:源码分析及实现

1.常量说明

1.1 DEFAULT_INITIAL_CAPACITY  默认初始化容量。容量必须为2的次方。原因在下面hash的时候会解释。默认的hashmap大小为16.

 /**
     * The default initial capacity - MUST be a power of two.
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

1.2 MAXIMUM_CAPACITY 最大的容量大小2^30。

 /**
     * The maximum capacity, used if a higher value is implicitly specified
     * by either of the constructors with arguments.
     * MUST be a power of two <= 1<<30.
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

1.3 DEFAULT_LOAD_FACTOR默认resize的因子。0.75,即实际数量超过总数*DEFAULT_LOAD_FACTOR的数量即会发生resize动作。

    /**
     * The load factor used when none specified in constructor.
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

1.4 TREEIFY_THRESHOLD 树化阈值。当单个segment的容量超过阈值时,将链表转化为红黑树。

    /**
     * The bin count threshold for using a tree rather than list for a
     * bin.  Bins are converted to trees when adding an element to a
     * bin with at least this many nodes. The value must be greater
     * than 2 and should be at least 8 to mesh with assumptions in
     * tree removal about conversion back to plain bins upon
     * shrinkage.
     */
    static final int TREEIFY_THRESHOLD = 8;

1.5 UNTREEIFY_THRESHOLD 链表化阈值。当删除操作后单个segment的容量低于阈值时,将红黑树转化为链表。

    /**
     * The bin count threshold for untreeifying a (split) bin during a
     * resize operation. Should be less than TREEIFY_THRESHOLD, and at
     * most 6 to mesh with shrinkage detection under removal.
     */
    static final int UNTREEIFY_THRESHOLD = 6;

1.6 MIN_TREEIFY_CAPACITY 最小树化容量。当桶中的bin被树化时最小的hash表容量,低于该容量时不会树化。

 /**
     * The smallest table capacity for which bins may be treeified.
     * (Otherwise the table is resized if too many nodes in a bin.)
     * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
     * between resizing and treeification thresholds.
     */
    static final int MIN_TREEIFY_CAPACITY = 64;

2.关键操作分析

2.1 如何分配到各个segment当中。

一致性算法一般包含mod或者是hash。HashMap是以hash操作作为散列依据。但是又与传统的hash存在着少许的优化。hash值是key的hashcode与其hashcode右移16位的异或结果。在put方法中,将取出的hash值与当前的hashmap容量-1进行与运算。得到的就是位桶的下标。那么为何需要使用key.hashCode() ^ h>>>16的方式来计算hash值呢。其实从微观的角度来看,这种方法与直接去key的哈希值返回在功能实现上没有差别。但是由于最终获取下表是对二进制数组最后几位的与操作。所以直接取hash值会丢失高位的数据,从而增大冲突引起的可能。由于hash值是32位的二进制数。将高位的16位于低位的16位进行异或操作,即可将高位的信息存储到低位。因此该函数也叫做扰乱函数。目的就是减少冲突出现的可能性。而官方给出的测试报告也验证了这一点。直接使用key的hash算法与扰乱函数的hash算法冲突概率相差10%左右。

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
 public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
   final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
	...
	n = table.length
	i = (n - 1) & hash
	...
	}

2.2 如何获取下一个需要扩容的大小。即如何获取距离一个数字的最近的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;
    }

由于map的容量需要维持在2的幂次方,所以需要计算出下一个扩容的大小。具体算法如图所示,int n = cap-1;的目的是为了防止当前的大小正好为2的幂次。

二进制中一个大于0的数字必定有一位为1。假设一个数字为00001******。在进行第一次n|= n>>>1;后结果变为000011*****;即让最高位以及次一位的数字变为1.同理

下一步变为00001111****;即让后两位再变为一。以此类推直到数据变为000011111111.最后执行+1操作即得到了距离给定数字最近的二次幂。

2.3 如何根据key获取value。

源码:
public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
 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;
    }

核心思想即通过传入的key,调用其hash值对segment的位置进行判断后。根据定位的segment对相应的链表/红黑树进行搜索。判断两个key的.equals()方法为真时,即取得
对应的value。

2.5 如何根据提交新的值。

源码:
public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
 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)     //(1)
            n = (tab = resize()).length;			//(2)
        if ((p = tab[i = (n - 1) & hash]) == null)		//(3)
            tab[i] = newNode(hash, key, value, null);		//(4)
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))		//(5)
                e = p;								//(6)
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); //(7)
            else {
                for (int binCount = 0; ; ++binCount) {				//(7)
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st	//(8)
                            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);			//无用操作,为linkedhashmap服务
                return oldValue;
            }
        }
        ++modCount;					//(9)
        if (++size > threshold)				//(10)
            resize();
        afterNodeInsertion(evict);			//无用操作,为linkedhashmap服务
        return null;
    }
先判断当前的table是否为空(1),如果是则对table进行resize操作(2)。否则根据传入的key的hash算法进入到相应的segment(3)。如果segment对应的Node节点为空(4),
则创建新节点并赋值。如果不为空则判断当前节点是否为传入节点(5),若是则替换value值(6),否则循环遍历list或者红黑树查询是否存在相同的key(7),若是则替换,否则插入。
同时判断树化阈值以及最小树化阈值来判断是否resize或者是树化(8)。将操作计数+1(9)。判断是否需要扩容(10)。本段没有详细描述红黑树如何插入,查找节点。红黑树内容请
参照其他文章进行学习。

2.6 如何删除一个值

源码
    public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }

 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;
        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;
                ++modCount;
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }
删除操作可以参照上面的插入操作。进行反向思路即可。



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值