HashMap中的数据结构与get,put源码解析

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值寻找putindex时进行的是一个位运算(n-1)&hash,位运算是基于二进制的,所以是2的次幂;

通过位运算可以很快的找到putindex位置,所以hashMap的插入效率很高。

比如初始容量为16,一个待插入元素hash值为6,那么我们一般要插入的位置就是index=6,也就是6%16我们进行的是取余操作。那么试试位运算(16-1&6

&位与运算:有00

0111   15

0110   6

_______

0110   6

通过取余和位与运算我们都得到了想要的结果,那么位运算的高效率肯定会被采纳。

当容量为2n次幂时,减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),是链表节点的话遍历链表寻找。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值