Java源码学习-HashMap(JDK5与JDK7)

目录

首先,来看一下jdk5的经典实现吧。

然后,开始瞅一瞅JDK7的HashMap实现吧

最后,对HashMap的两个思考

为什么容量大小一定是2的幂次方?

第二个问题就是重写equals方法必须重写hashcode方法在HashMap这得到印证。



首先,来看一下jdk5的经典实现吧。

1、HashMap继承了AbstractMap,实现了三个接口:Map,Cloneable,Serializable

数据结构:数组+链表

public class HashMap<K,V> extends AbstractMap<K,V> 
                          implements Map<K,V>, Cloneable, Serializable
{}

static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        // 存储hash(key)后key的hash值,避免了重复计算
        final int hash;
        // 指向下一个Entry的引用,单链表
        Entry<K,V> next;
}

2、HashMap一些默认的参数取值

// 以下3个默认大小的字段都声明为final,说明在HashMap中不可变或者在构造函数中赋值
// 默认的初始化容量大小为16,容量大小必须为2的n次方
static final int DEFAULT_INITIAL_CAPACITY = 16;

// 最大的容量大小为2的30次方,
static final int MAXIMUM_CAPACITY = 1 << 30;

// 默认的负载因子是0.75f,float类型的,主要用作计算阀值大小
static final float DEFAULT_LOAD_FACTOR = 0.75f;

// HashMap的结构就一个Entry类型的数组,不可被序列化
transient Entry[] table;

// size是HashMap的大小,也就是存了多少个key-value对,同样不可序列化
transient int size;

// 阀值大小,没有final修饰,在整个HashMap的操作过程中可变。threshold = capacity * load factor
// 当size++ >= threshold时,就要开始给table扩容2倍
int threshold;

// 负载因子
final float loadFactor;

// 修改次数,不仅不可序列化,而且在多线程情况下,其值对其他线程立即可见
// 这个值主要被用来集合做iterators迭代时,保证集合的快失败机制。也就是iterator迭代集合时,
// 集合被外部修改后,会报ConcurrentModificationException,具体见后面的方法分析
transient volatile int modCount;

3、HashMap的4个构造函数

    // 两个入参:初始化容量大小, 负载因子
    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;
        // 根据初始化容量大小,找到第一个比initialCapacity大的2的n次方的值
        // 假设initialCapacity=15,则capacity=16;假设initialCapacity=17,则capacity=32;
        while (capacity < initialCapacity) 
            capacity <<= 1;
    
        this.loadFactor = loadFactor;
        // 计算阀值,(capacity * loadFactor)==>(int*float)计算结果是浮点型的,需要强转成int型
        threshold = (int)(capacity * loadFactor);
        // 新建一个数组并初始化
        table = new Entry[capacity];
        init();
    }

    // 这个构造函数传入初始容量值,然后把默认的负载因子0.75f传入到上面的构造函数并调用
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    // 无参构造函数,各个参数都用默认值,构造出一个16个容量大小的数组
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
        table = new Entry[DEFAULT_INITIAL_CAPACITY];
        init();
    }

    // 这个构造函数主要是用一个已有的Map集合,构造出一个新的HashMap
    // 一开始没有判断m的空指针异常感觉有点不应该,虽然在代码注释部分有写NullPointerException
    /**
     * @param   m the map whose mappings are to be placed in this map.
     * @throws  NullPointerException if the specified map is null.
    */
    public HashMap(Map<? extends K, ? extends V> m) {
        // 计算一个初始容量因子和默认的0.75f的负载因子去调用第一个构造函数
        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                      DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
        // 把m的值导入到HashMap中,源码见下方
        putAllForCreate(m);
    }

    void putAllForCreate(Map<? extends K, ? extends V> m) {
        // 便利m的entrySet,把每个Entry都插入到HashMap
        for (Iterator<? extends Map.Entry<? extends K, ? extends V>> i =                         
             m.entrySet().iterator(); i.hasNext(); ) {
            Map.Entry<? extends K, ? extends V> e = i.next();
            putForCreate(e.getKey(), e.getValue());
        }
    }

    // 这个方法只会在构造函数被调用时执行,不是put方法,它不会扩容,也不会检查修改次数等
    private void putForCreate(K key, V value) {
        K k = maskNull(key);
        // 计算hash值和计算table的索引下文会提到
        int hash = hash(k.hashCode());
        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 (Entry<K,V> e = table[i]; e != null; e = e.next) {
            if (e.hash == hash && eq(k, e.key)) {
                e.value = value;
                return;
            }
        }

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

    static final Object NULL_KEY = new Object();

    /**
     * Returns internal representation for key. Use NULL_KEY if key is null.
     */
    static <T> T maskNull(T key) {
        return key == null ? (T)NULL_KEY : key;
    }

    // 创建Entry
    void createEntry(int hash, K key, V value, int bucketIndex) {
	    Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
        size++;
    }

 4、HashMap最重要的方法put()出场了

    // 往HashMap里面插入一对键值对,如果key已经存在就返回老的value,如果不存在就返回null
    public V put(K key, V value) {
        // 如果key是null,那就把Null_Key计算索引后放进HashMap(jdk8是放table[0])
	    if (key == null)
	        return putForNullKey(value);
        // 计算hash值
        int hash = hash(key.hashCode());
        // 计算table的索引位置
        int i = indexFor(hash, table.length);
        // table[i]所处的链表上如果存在相同的key,则把value值替换,并返回旧的value值
        for (Entry<K,V> 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;
            }
        }
        // 增加修改次数,增加一个Entry在此索引处的链表头节点并返回null
        // 保证并发访问时,HashMap内部修改时,能够快速失败
        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

    // 放入key=null的键值对
    private V putForNullKey(V value) {
        // 同样的计算哈希值和数组索引
        int hash = hash(NULL_KEY.hashCode());
        int i = indexFor(hash, table.length);
        // 遍历当前table的链表,如果此链表上已存在key=NULL_KEY的,则直接替换value
        // 并return oldvlaue
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            if (e.key == NULL_KEY) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        // 当前数组节点的链表不存在key=NULL_KEY的,就在table[i]处新增一个Entry,并return null
        modCount++;
        // 新增一个Entry,当前的table[i]变成新增Entry的next节点
        addEntry(hash, (K) NULL_KEY, value, i);
        return null;
    }

    void addEntry(int hash, K key, V value, int bucketIndex) {
	    Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
        // 如果当前size达到阀值了,则需要扩容2倍
        if (size++ >= threshold)
            resize(2 * table.length);
    }

    // 扩容操作,newCapacity=2*oldCapacity
    void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        // 如果旧容量已经达到最大容量了,则把阀值设置为Integer.MAX_VALUE并直接返回
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
        // 建立一个新的数组
        Entry[] newTable = new Entry[newCapacity];
        // 旧table转到新table
        transfer(newTable);
        table = newTable;
        // 重新计算阀值
        threshold = (int)(newCapacity * loadFactor);
    }

    // 扩容时新旧数组转换
    void transfer(Entry[] newTable) {
        Entry[] src = table;
        int newCapacity = newTable.length;
        // 遍历旧的table
        for (int j = 0; j < src.length; j++) {
            Entry<K,V> e = src[j];
            if (e != null) {
                // 旧table当前Entry置null
                src[j] = null;
                // 遍历链表
                do {
                    Entry<K,V> next = e.next;
                    // 重新哈希散列到新table
                    // 重新hash后,下一次get的时候怎么找得到呢?
                    // 因为e的hash值在扩容后没变,newCapacity在下一次扩容前也不变,
                    //所以get()的时候indexfor计算出来的值就和现在indexfor计算的结果是一样的
                    int i = indexFor(e.hash, newCapacity);  
                    // 第一次执行时,newTable[i]=null
                    // 其实就是把e插入到这条链表的头节点处,之前的节点链接到此插入节点后面,
                    // 最后一个节点的next为null(第一次hash到此索引位置时)
                    e.next = newTable[i];
                    newTable[i] = e;
                    // 下一个链表节点
                    e = next;
                } while (e != null);
            }
        }
    }

    // 计算数组索引
    static int indexFor(int h, int length) {
        return h & (length-1);
    }

    /**
     * 两个hash算法,默认是oldHash
     * Set to true only by hotspot when invoked via
     * -XX:+UseNewHashFunction or -XX:+AggressiveOpts
     */
    private static final boolean useNewHash;
    static { useNewHash = false; }

    static int hash(int h) {
	    return useNewHash ? newHash(h) : oldHash(h);
    }

    static int hash(Object key) {
	    return hash(key.hashCode());
    }

6、HashMap里面的get()方法

    // put方法看明白后,get方法就简单了,都是采用一样的算法
    public V get(Object key) {
	    if (key == null)
	        return getForNullKey();
        int hash = hash(key.hashCode());
        // 先计算数组索引位置,然后遍历找到key,最后返回value
        for (Entry<K,V> 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;
    }
    
    // 得到key=NULL值的value
    private V getForNullKey() {
        int hash = hash(NULL_KEY.hashCode());
        int i = indexFor(hash, table.length);
        Entry<K,V> e = table[i];
        while (true) {
            if (e == null)
                return null;
            if (e.key == NULL_KEY)
                return e.value;
            e = e.next;
        }
    }

7、HashMap的其他方法

    // 这个方法类似get()方法
    public boolean containsKey(Object key) {
        Object k = maskNull(key);
        int hash = hash(k.hashCode());
        int i = indexFor(hash, table.length);
        Entry e = table[i]; 
        while (e != null) {
            if (e.hash == hash && eq(k, e.key)) 
                return true;
            e = e.next;
        }
        return false;
    }

    // 输入键key,删除HashMap集合元素,返回value
    public V remove(Object key) {
        Entry<K,V> e = removeEntryForKey(key);
        return (e == null ? null : e.value);
    }

    // 删除Entry
    Entry<K,V> removeEntryForKey(Object key) {
        Object k = maskNull(key);
        int hash = hash(k.hashCode());
        int i = indexFor(hash, table.length);
        Entry<K,V> prev = table[i];
        Entry<K,V> e = prev;

        while (e != null) {
            Entry<K,V> next = e.next;
            if (e.hash == hash && eq(k, e.key)) {
                modCount++;
                size--;
                // 如果要删除的e刚好是table[i]处的元素,则直接把e的next放到table[i],就删了
                if (prev == e) 
                    table[i] = next;
                // 如果e是链表中的元素,则把e的前一个元素指向e的下一个元素,就可以删除
                else
                    prev.next = next;
                e.recordRemoval(this);
                return e;
            }
            prev = e;
            e = next;
        }
   
        return e;
    }

    // 清空table所有元素
    public void clear() {
        modCount++;
        Entry[] tab = table;
        for (int i = 0; i < tab.length; i++) 
            tab[i] = null;
        size = 0;
    }

然后,开始瞅一瞅JDK7的HashMap实现吧

看完源码后发现JDK7和JDK5的实现并没有什么本质的区别,一些细节方面的实现列举如下:

1、初始化HashMap的时候,不在构造函数里面构建数组了,而是在第一次put的时候,判断table是不是空,如果是空就构建一个table出来

2、放入key=null的位置不一样了,JDK5是new Object(),然后计算hash值,按照正常的key处理。JDK7是把key=null的放在table[0]位置,并且hash值也置0

3、addEntry()的时候,JDK5是先新建Entry,然后判断是否需要扩容。JDK7是先判断是否扩容

(size>=threshold && null != table[i]),扩容后重新计算hash和index,然后新建Entry

最后,对HashMap的两个思考

为什么容量大小一定是2的幂次方?

先想想HashMap中最耗时的操作会发生在什么时候,当然是扩容的时候。

indexfor()函数:

h & (length-1)

其中的length就是table.length();也就是容量大小capacity。

假设capacity=16,h=10,length-1就是15,15的二进制是地位全是1,h和它做与运算的结果就是:

    00000000 00000000 00000000 00001010

&

    00000000 00000000 00000000 00001111

=  00000000 00000000 00000000 00001010

假设现在扩容了,capacity=32,h=10,length-1就是31,h和它做与运算的结果就是:

    00000000 00000000 00000000 00001010

&

    00000000 00000000 00000000 00011111

=  00000000 00000000 00000000 00001010

看出来了吗,15和31之间的差别就是低位多了一个1,那么在扩容时,重新indexfor的时候,很多元素位置是不需要动的,这样就能保证新数组和老数组的一致性,减少数据调动。顺便提一句,在老数组copy到新数组时,copy的是引用,因为数组中存的是Entry的引用。

另外一点,上面低位全是1,说明index取决于hash值,而hash值的计算是通过一系列神奇的位运算使得hash值算出来比较均匀,从而使得index比较均匀,也就是数据分布比较均匀。

第二个问题就是重写equals方法必须重写hashcode方法在HashMap这得到印证。

举个例子:

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

public class Main {

    public static void main(String[] args) {
        Map<Person, String> map = new HashMap<Person, String>();
        Person person = new Person(18, "xxx");
        map.put(person, "code");
        System.out.println(map.get(new Person(18, "xxx")));
    }
}

class Person{
    int id;
    String name;
    public Person(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null || getClass() != obj.getClass()){
            return false;
        }
        Person person = (Person) obj;
        //两个对象是否等值,通过id来确定
        return this.id == person.id;
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(getId()) ^ Objects.hashCode(getName());
    }
}

如果不重写hashCode()方法,则输出:null (HashMap在put和get时的索引值不一致导致,索引值由key的hash值决定)

如果重写hashCode()方法,则输出:code

 

问题:线程不安全

比如多线程情况下,resize()可能会发生死循环(循环链表)

 

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值