Java数据结构:HashMap源码分析

HashMap简介

一、内部结构

transient Node<K,V>[] table;

static class Node<K,V> implements Map.Entry<K,V> {
	final int hash;
	final K key;
	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;
	}
}

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);
        }
}

由上面源码可以看出 HashMap实际上是一个 数组+链表+红黑树的结构,也就是散列表的结构

如图:
请添加图片描述

二、构造函数

    /**
     * Constructs an empty {@code HashMap} 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);
    }

构造函数主要就是赋值 系数loadFactor和下次扩容大小threshold

那么为什么loadFactor默认值是0.75?

因为:

  • 数据一开始是保存在数组里面的,当发生了Hash碰撞的时候,就是在这个数据节点上,生出一个链表,当链表长度达到一定长度的时候,就会把链表转化为红黑树。
  • 当负载因子为1.0的时候,就会产生很多hash碰撞,红黑树就会变得异常复杂,查询数据的效率就会大大降低
  • 当负载因子为0.5的时候,虽然可以减少hash碰撞,链表和红黑树也会变得简单,但是这样会增加数据的占用空间,原本1M的数据现在需要2M
  • 复杂因子0.75也是为了权衡查询效率和空间利用率得出来的默认值

下次扩容大小tableSizeFor如下

    /**
     * Returns a power of two size for the given target capacity.
     */
    static final int tableSizeFor(int cap) {
        //Integer.numberOfLeadingZeros :得到 32 - 二进制最高位数 ;eg :参数5 转换为二进制 101;有两位,那么结果就等于32-3=29 
        //n = -1>>>29 = 7
        //整个连起来 得到 初始大小-1 数据的 二进制最高位的最大数据
        int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
        //最终结果在 n的基础上 +1,或者最大值
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

三、get数据

    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(key)) == null ? null : e.value;
    }
    
    final Node<K,V> getNode(Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n, hash; K k;
        //使用 (tab.length-1) & (hash(key)) 得到数组index
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & (hash = hash(key))]) != null) {
            //判断first节点是否为查询的node
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                //区分节点是否为TreeNode
                if (first instanceof TreeNode)
                    //使用TreeNode自身的get函数
                    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函数过程:

  1. 采用 (tab.length-1) & (hash(key)) 得到数组index 进行判断此index在数组tab中是否有数据

  2. 判断首节点是否为查询的Node

  3. 不是首节点,则判断是否是TreeNode,是TreeNode则使用getTreeNode查询数据,链表则使用链表遍历方法进行查询后续节点数据

时间复杂度:

  • 由key获取hash值然后定位到首节点的时间复杂度都是O(1)

  • 如果没有后续节点那么时间复杂度就是O(1)

  • 如果后续节点为链表,则需要遍历链表,时间复杂度为O(n)

  • 如果后续节点为红黑树,则查询数据时间复杂度为O(logn)

三、put数据

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    
    /**
     * 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)
            //查询到数组index位置没有数据,则newNode
            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))))
                //首节点是否为需要修改的node
                e = p;
            else if (p instanceof TreeNode)
                //使用红黑树的方式put
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                //使用链表方式put
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //重要点:当链表长度 >= TREEIFY_THRESHOLD(8)的时候会变为红黑树treeNode
                        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;
    }

整个put函数过程:

  1. 当前没有数据,则需要初始化容量

  2. 采用 (tab.length-1) & (hash(key)) 得到数组index 进行判断此index在数组tab中是否有数据,没有数据则new一个node数据

  3. 判断首节点是否为需要修改的node,如果是则修改

  4. 判断后续节点是否是红黑树,如果是则使用红黑树put函数

  5. 不为红黑树,则使用链表遍历查找是否是需要修改的node,如果是则修改,如果遍历玩置灰仍然没有则new一个node;

    重要点:当链表长度 >= TREEIFY_THRESHOLD(8)的时候会变为红黑树treeNode

四、扩容机制

为什么hashmap 采用的是2倍扩容机制?

HashMap的容量为什么是2的n次幂,和这个(n - 1) & hash的计算方法有着千丝万缕的关系,符号&是按位与的计算,这是位运算,计算机能直接运算,特别高效,按位与&的计算方法是,只有当对应位置的数据都为1时,运算结果也为1,当HashMap的容量是2的n次幂时,(n-1)的2进制也就是1111111***111这样形式的,这样与添加元素的hash值进行位运算时,能够充分的散列,使得添加的元素均匀分布在HashMap的每个位置上,减少hash碰撞

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值