HashMap源码笔记

前言

HashMap,应该所有java程序员都用过这个集合,是平时中很常用的一个集合。大部分人都知道怎么用它,也知道它不是线程安全的,HaspTable才是线程安全的。但很多人只是极限于此。并不知道Haspmap里面的构造是怎么样的,也不知道haspmap为什么线程不安全。所以我们今天就来看看HaspMap的源码构造吧。

HashMap的类结构

image.png

可以看出,HashMap的结构是竖直方向是数组,横向就是链表的存储方式。数组的存储方式在内存的地址是连续的,大小固定,一旦分配不能被其他引用占用。它的特点是查询快,时间复杂度是O(1),插入和删除的操作比较慢,时间复杂度是O(n),链表的存储方式是非连续的,大小不固定,特点与数组相反,插入和删除快,查询速度慢。HashMap是介于两者之间,算是一种小优化吧。

构造函数

  public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY) {
            initialCapacity = MAXIMUM_CAPACITY;
        } else if (initialCapacity < DEFAULT_INITIAL_CAPACITY) {
            initialCapacity = DEFAULT_INITIAL_CAPACITY;
        }

        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        // Android-Note: We always use the default load factor of 0.75f.

        // This might appear wrong but it's just awkward design. We always call
        // inflateTable() when table == EMPTY_TABLE. That method will take "threshold"
        // to mean "capacity" and then replace it with the real threshold (i.e, multiplied with
        // the load factor).
        threshold = initialCapacity;
        init();
    }

    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }
    
    public HashMap(Map m) {
        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                      DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
        inflateTable(threshold);

        putAllForCreate(m);
    }

可以看到hashmap的构造函数有3种。
1.HashMap(int initialCapacity, float loadFactor)可以指定初始化容量和扩容因子,里头做了容量的判断,最小是4,不能大于1>>30.扩容因子默认是0.75
2.HashMap(),创建一个空的HashMap,容量为4,扩容因子0.75
3.HashMap(Map m) 将一个map的数据赋值给当前的hashmap,我们来看看里面两个方法

/**
     * 当HashMap为空的时候初始化当前HashMap
     */
    private void inflateTable(int toSize) {
        // Find a power of 2 >= toSize
        int capacity = roundUpToPowerOf2(toSize);
        //下一次扩容的大小为当前容量*扩容因子
        float thresholdFloat = capacity * loadFactor;
        if (thresholdFloat > MAXIMUM_CAPACITY + 1) {
            thresholdFloat = MAXIMUM_CAPACITY + 1;
        }

        threshold = (int) thresholdFloat;
        table = new HashMapEntry[capacity];//构建一个容量大小的数组
    }

 private void putAllForCreate(Map m) {
        for (Map.Entry e : m.entrySet())
            putForCreate(e.getKey(), e.getValue());
    }

/**
     * This method is used instead of put by constructors and
     * pseudoconstructors (clone, readObject).  It does not resize the table,
     * check for comodification, etc.  It calls createEntry rather than
     * addEntry.
     */
    private void putForCreate(K key, V value) {
        int hash = null == key ? 0 : sun.misc.Hashing.singleWordWangJenkinsHash(key);
        int i = indexFor(hash, table.length);

        /**
         * Look for preexisting entry for key.  This will never happen for
         * clone or deserialize.  It will only happen for construction if the
         * input Map is a sorted map whose ordering is inconsistent w/ equals.
         */
        for (HashMapEntry e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k)))) {
                e.value = value;
                return;
            }
        }

        createEntry(hash, key, value, i);
    }

inflateTable(int toSize),里面通过roundUpToPowerOf2(toSize)方法找到一个比number大的2的倍数,这里面解释一下threshold这个变量,这个变量是当hashmap的里面的数据达到这个容量的时候,就会开始扩容了,一般这个变量的值是当前容量 * 扩容因子,即capacity * loadFactor。最后创建一个当前容量的数组。

putAllForCreate(m)这个方法就是讲参数中map的值赋值给hashmap。这里面我们先不说它是怎么找到hashmap位置然后赋值的。后面讲put的时候再说。

put的原理

    public V put(K key, V value) {
        if (table == EMPTY_TABLE) {//当前HashMap为空
            inflateTable(threshold);//初始化HashMap,此时会构建好table[]、容量和扩容值
        }
        if (key == null)//HashMap支持null作为key
            return putForNullKey(value);
   
        int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);
   
        int i = indexFor(hash, table.length);
 
        for (HashMapEntry e = table[i]; e != null; e = e.next) {
            Object k;
           
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        //当前链表中没有指定key的节点,新建一个节点并且添加到链表当中
        addEntry(hash, key, value, i);
        return null;
    }

    /**
     * 当添加到HashMap中的键值对的key为null的时候
     * @param value key为null对应的value
     * @return 如果当前是覆盖操作,返回被覆盖的原来的value,否则为null
     */
    private V putForNullKey(V value) {
        //可以看到key为null的时候,默认hash为0
        //先遍历table[0]的链表,看是否需要覆盖
        for (HashMapEntry e = table[0]; e != null; e = e.next) {
            if (e.key == null) {//如果当前节点key为null,直接覆盖value
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(0, null, value, 0);//否则新建一个节点并且添加到链表当中
        return null;
    }

    /**
     * 新建一个节点并且添加到指定的链表的头部
     * @param hash 需要添加的key的hash值
     * @param key 需要添加的节点的key
     * @param value 需要添加的节点的value
     * @param bucketIndex 需要添加到table[]的下标
     */
    void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
         
            hash = (null != key) ? sun.misc.Hashing.singleWordWangJenkinsHash(key) : 0;
          
            bucketIndex = indexFor(hash, table.length);
        }
        //新建节点并放在放在table[bucketIndex]链表的头部,大小+1
        createEntry(hash, key, value, bucketIndex);
    }

    /**
     * 扩容操作
     * 当HashMap添加数据的时候发现已经到了扩容标准
     * 则进行扩容
     * @param newCapacity 当前希望的新的容量
     */
    void resize(int newCapacity) {
        HashMapEntry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
       
        HashMapEntry[] newTable = new HashMapEntry[newCapacity];
        transfer(newTable);
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

    /**
     * 当进行扩容的时候,需要将旧的table[]数据转移到当前新的table[]上面
     * @param newTable 扩容后新建的table[]
     */
    void transfer(HashMapEntry[] newTable) {
        int newCapacity = newTable.length;//扩容后的大小
        //遍历旧的table[],将旧的节点转移到新的table[]中
        for (HashMapEntry e : table) {
            while(null != e) {
                HashMapEntry next = e.next;     
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }

这里总结一下put的流程。
1.首先判断key是否为空,为空的时候会走putForNullKey(value)这个方法,这个方法会遍历table[0]的链表,遍历的时候,发现有节点的key为null,会覆盖当前节点的value值。遍历完后发现没有key为null,会创建一个节点并添加到当前链表中。
2.看看新建节点这个方法。addEntry(int hash, K key, V value, int bucketIndex),这个方法一开始会判断是否需要扩容,如果需要扩容,则会直接将原数组的容量扩充2倍,然后通过transfer(HashMapEntry[] newTable)将原hashmap里面的值重新赋值给新数组,transfer这个方法里面会通过indexFor方法重新计算所有值得数组下标值然后重新放置。最后重新计算threshold 这个临界值。
2.key不为null,则计算key的hash值,然后通过indexFor(hash, table.length)这个方法计算得到数组的下标位置。看看indexFor方法

static int indexFor(int h, int length) {
        return h & (length-1);
    }

这里介绍一下这个方法,这个方法就一行代码,很简单。
举个例子
1.计算book的hashcode,结果为十进制的3029737,二进制的101110001110101110 1001。

2.假定HashMap长度是默认的16,计算Length-1的结果为十进制的15,二进制的1111。

3.把以上两个结果做与运算,101110001110101110 1001 & 1111 = 1001,十进制是9,所以 index=9。

可以说,Hash算法最终得到的index结果,完全取决于Key的Hashcode值的最后几位。加入换成不是2的倍数,得出来的重复性就会很大,很可能多个值都集中在一条链表上面。这显然不符合Hash算法均匀分布的原则。
3.找到数组的下标位置后,如果发现当前key已经存在当前链表中,会替换原来key对应的value值。否则通过
addEntry(hash, key, value, i)方法将当前值插入到链表的头结点。
4.modCount值会加一,等会解释modCount这个值的用意。

get的原理

/**
     * 从HashMap中获得指定key对应的value
     * @param key 需要查找的key,支持null
     * @return value,没有的话为null
     */
    public V get(Object key) {
        if (key == null)//key为空,直接查找table[0]即可,相对于getEntry节省了indexFor的开销
            return getForNullKey();
        Map.Entry entry = getEntry(key);

        return null == entry ? null : entry.getValue();
    }

    private V getForNullKey() {
        if (size == 0) {
            return null;
        }
        for (HashMapEntry e = table[0]; e != null; e = e.next) {
            if (e.key == null)
                return e.value;
        }
        return null;
    }

 
    final Map.Entry getEntry(Object key) {
        if (size == 0) {
            return null;
        }    
        int hash = (key == null) ? 0 : sun.misc.Hashing.singleWordWangJenkinsHash(key);
     
        for (HashMapEntry e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;     
            if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }

get就比较简单了,判断key是否为null,如果为null,走getForNullKey这个方法,这个就是在table[0]这个链表中获取key为null的value值,之前我们put的时候是有放置的。
如果key不为null,走getEntry(Object key),这个方法很放置的原理差不多,根据key通过indexFor找到数组的下标位置。然后在当前下标的链表中找到key值相同的value。

总结

1.可以看到hashmap的put和get操作都是有比较多的遍历列表的操作。这是有点损耗性能。

2.haspmap在计算数组下标时会尽量避免hash冲突,使值尽可能的均匀分布在各个链表中。解决Hash的冲突有以下几种方法:
1.开放定址法(线性探测再散列,二次探测再散列,伪随机探测再散列
2. 再哈希法
3. 链地址法
4. 建立一 公共溢出区
而HashMap采用的是链地址法。
3.HaspMap不是线程安全的,在多线程高并发的情况下会有问题。ConcurrentHashMap就是为了解决这个问题而提出的。

你可能感兴趣的:(HashMap源码笔记)