Java HashMap初探

最近代码中经常出现一些Cache缓存,以减少大量用户请求导致数据服务器load过高的情况,而这些Cache缓存的底层实现数据结构支持都是Map,于是决定翻看以下各种map的源码。于是从HashMap开始。

1. HashMap中使用的Map Entry

HashMap中的Entry类源码如下:
总体上来看,Entry类就是对Map.Entry接口的一种实现方式。

    static class Entry implements Map.Entry {
        final K key;     //键
        V value;         //值
        Entry next; //链表下一结点指针
        final int hash;  //hash值,根据具体的Hash算法以key为输入进行计算获得

        /**
         * Creates new entry.
         */
        Entry(int h, K k, V v, Entry n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }

        public final K getKey() {
            return key;
        }

        public final V getValue() {
            return value;
        }

        public final V setValue(V newValue) {
	    V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry e = (Map.Entry)o;
            Object k1 = getKey();
            Object k2 = e.getKey();
            if (k1 == k2 || (k1 != null && k1.equals(k2))) {
                Object v1 = getValue();
                Object v2 = e.getValue();
                if (v1 == v2 || (v1 != null && v1.equals(v2)))
                    return true;
            }
            return false;
        }

        public final int hashCode() {
            return (key==null   ? 0 : key.hashCode()) ^
                   (value==null ? 0 : value.hashCode());
        }

        public final String toString() {
            return getKey() + "=" + getValue();
        }

        /**
         * This method is invoked whenever the value in an entry is
         * overwritten by an invocation of put(k,v) for a key k that's already
         * in the HashMap.
         */
        void recordAccess(HashMap m) {
        }

        /**
         * This method is invoked whenever the entry is
         * removed from the table.
         */
        void recordRemoval(HashMap m) {
        }
    }
上面的Entry类共包含了四个数据域,我在上面加上了中文注释,你会发现,Entry类是一个典型单链表节点类定义(将next视为下一节点指针,剩下的就是数据域),此时你应该已经能够想到Java中HashMap的避碰策略了吧,对就是采用外接链表的避碰策略。

2.HashMap中的一些基本函数

2.1 构造函数

我们平时使用的最多的无参构造函数最终都是调用了以下的构造函数
/**
     * Constructs an empty 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);

        // Find a power of 2 >= initialCapacity
        int capacity = 1;     //保证HashMap初始的存储能力都是2的平方数,可以看到这一点在indexFor函数中很有用
        while (capacity < initialCapacity)
            capacity <<= 1;

        this.loadFactor = loadFactor;
        threshold = (int)(capacity * loadFactor);   //HashMap执行Resize操作的临界值
        table = new Entry[capacity];                //初始化Entry table,也就是初始化所有的桶,每个桶都是一个链表
        init();
    }

2.2 从put函数说起

/**
     * Associates the specified value with the specified key in this map.
     * If the map previously contained a mapping for the key, the old
     * value is replaced.
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with key, or
     *         null if there was no mapping for key.
     *         (A null return can also indicate that the map
     *         previously associated null with key.)
     */
    public V put(K key, V value) {
        if (key == null)        //HashMap允许键为空(key=null)的情况出现
            return putForNullKey(value);
        int hash = hash(key.hashCode());
        int i = indexFor(hash, table.length);   //根据确定待插入/更新key-value对的bucket位置
        for (Entry e = table[i]; e != null; e = e.next) {  //遍历对应bucket中存储的链表
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  //已存在相应的key值,更新
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;  //更新完毕返回key之前对应的value
            }
        }

        modCount++;
        addEntry(hash, key, value, i);  //之前i位置对应的bucket处不存在key,则执行插入操作
        return null;
    }

2.3 putForNullKey添加以null为key的键值对

可以看出,以null为key的键值对始终存储在index为0的桶内
    /**
     * Offloaded version of put for null keys
     */
    private V putForNullKey(V value) {
        for (Entry e = table[0]; e != null; e = e.next) {
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(0, null, value, 0); //如果之前不存在null为key的键值对,则最终还是调用插入操作
        return null;
    }

2.4 indexFor函数,计算key对应hash值对应的table中bucket位置编号

    /**
     * Returns index for hash code h.
     */
    static int indexFor(int h, int length) {
        return h & (length-1);  //位运算散列
    }
函数里使用位运算的操作进行散列映射,之所以可以这样做就是因为上文说的HashMap的存储能力Capacity始终都是2的幂。

2.5 addEntry新增key-value对

    /**
     * Adds a new entry with the specified key, value and hash code to
     * the specified bucket.  It is the responsibility of this
     * method to resize the table if appropriate.
     *
     * Subclass overrides this to alter the behavior of put method.
     */
    void addEntry(int hash, K key, V value, int bucketIndex) {
	Entry e = table[bucketIndex];      
        table[bucketIndex] = new Entry(hash, key, value, e); //仔细看Entry的构造函数,你会发现插入操作时在链表头进行的
        if (size++ >= threshold)
            resize(2 * table.length);  //重建HashMap操作,每次重建capacity的值为原来的2倍,保证了始终为2的整数次幂
    }
英文注释中说的很明白,就是这个函数要负责在链表的size也就是负载达到threshold临界值时重建Map,这要花费很多的时间,另外一点需要注意的时,HashMap的负载计算并不是按照有值存在的bucket位置的比例来计算的,其计算方式就是按照我们所存储的key-value对的个数比上capacity。

2.6 resize重建HashMap

    /**
     * Rehashes the contents of this map into a new array with a
     * larger capacity.  This method is called automatically when the
     * number of keys in this map reaches its threshold.
     *
     * If current capacity is MAXIMUM_CAPACITY, this method does not
     * resize the map, but sets threshold to Integer.MAX_VALUE.
     * This has the effect of preventing future calls.
     *
     * @param newCapacity the new capacity, MUST be a power of two;
     *        must be greater than current capacity unless current
     *        capacity is MAXIMUM_CAPACITY (in which case value
     *        is irrelevant).
     */
    void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;   //讲临界值设为最大,由此就不会再出现新的resize操作请求了
            return;
        }

        Entry[] newTable = new Entry[newCapacity];
        transfer(newTable);                  //真正重建HashMap的执行函数
        table = newTable;
        threshold = (int)(newCapacity * loadFactor);
    }

2.7 transfer重建HashMap

    /**
     * Transfers all entries from current table to newTable.
     */
    void transfer(Entry[] newTable) {
        Entry[] src = table;
        int newCapacity = newTable.length;
        for (int j = 0; j < src.length; j++) {
            Entry e = src[j];
            if (e != null) {
                src[j] = null;
                do {
                    Entry next = e.next; 
                    int i = indexFor(e.hash, newCapacity);  //重新散列计算
                    e.next = newTable[i]; //在链表头插入
                    newTable[i] = e;
                    e = next;
                } while (e != null);
            }
        }
    }
可以看到HashMap的重建,不但要申请新的内存空间而且对已有的key-value对都要进行重新hash散列计算。

2.2~2.7就已经时put操作的所有操作过程了。

2.8 get操作

    public V get(Object key) {
        if (key == null)
            return getForNullKey();
        int hash = hash(key.hashCode());
        for (Entry e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
                return e.value;
        }
        return null;
    }
相比与put来说,get操作就是对这个邻接表结构进行遍历查找操作了。还有相关的containsKey,getEntry等函数,大同小异,就不多说了。

2.9 remove 删除

 /**
     * Removes the mapping for the specified key from this map if present.
     *
     * @param  key key whose mapping is to be removed from the map
     * @return the previous value associated with key, or
     *         null if there was no mapping for key.
     *         (A null return can also indicate that the map
     *         previously associated null with key.)
     */
    public V remove(Object key) {
        Entry e = removeEntryForKey(key);
        return (e == null ? null : e.value);
    }

    /**
     * Removes and returns the entry associated with the specified key
     * in the HashMap.  Returns null if the HashMap contains no mapping
     * for this key.
     */
    final Entry removeEntryForKey(Object key) {
        int hash = (key == null) ? 0 : hash(key.hashCode());
        int i = indexFor(hash, table.length);
        Entry prev = table[i];
        Entry e = prev;

        while (e != null) {
            Entry next = e.next;
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k)))) {
                modCount++;
                size--;
                if (prev == e)
                    table[i] = next;
                else
                    prev.next = next;
                e.recordRemoval(this);
                return e;
            }
            prev = e;
            e = next;
        }

        return e;
    }
删除操作就是对单链表的删除~~

3.并发支持及相关总结

(ˇˍˇ) 想~继续往下写的时候发现这篇文章写的太好了,于是给出下面这个链接:
http://coolshell.cn/articles/9606.html
真心给这篇文章的作者点个赞。
HashMap时部支持并发访问的,多线程中使用HashMap会出现死循环状况。
这时就有了 ConcurrentHashmap~~

你可能感兴趣的:(Java随笔)