HashMap 源码详解(1.7)

一、HashMap初始化

	//创建一个无参的构造器,默认容量为16,加载因子0.75
    public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }
    //创建一个指定容量的HashMap,默认加载因子为0.75
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    //创建一个指定容量,指定加载因子的HashMap
    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;
        threshold = initialCapacity;
        init();
    }
    //创建一个指定参数为Map对象的HashMap
    public HashMap(Map<? extends K, ? extends V> m) {
        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                      DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
        inflateTable(threshold);

        putAllForCreate(m);
    }
    

HashMap提供了四个构造方法,分别是无参数的构造器,包含一个参数(初始容量)的构造器,包含两个参数的构造器(初始容量,加载因子),和一个包含一个Map对象的构造器。HashMap初始容量为16,加载因子 0.75

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

static final float DEFAULT_LOAD_FACTOR = 0.75f;

二、Entry

我们知道HashMap 是以键值对的方式存储的,HashMap 内部实现了Entry,它是实现键值对存储的关键,Entry类包含一个keyvalue,一个外部引用的hash,以及Entry对象的引用,有点类似链表的结构。

    static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;//键
        V value;//值
        Entry<K,V> next;//下一节点
        int hash;//hash值

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

三、table数组

    static final Entry<?,?>[] EMPTY_TABLE = {};//空的Entry对象

    transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE; //空数组

每个键值对是通过Entry实现的,那么多个键值对元素是怎么存放的呢? HashMapEMPTY_TABLE表示一个空的数组,每个Entry都是放在一个table 数组中。由此可以看出,HashMap底层是通过链表+数组的方式元素。

如何创建数组

    /**
     * Inflates the table.
     */
    private void inflateTable(int toSize) {
        //这个方法之后返回 大于或者等于 2的几次幂
        int capacity = roundUpToPowerOf2(toSize);
		
		//默认容量16 *0.75=12 ,从12和最大容量之间取最小值,
		//因此这里threshold = 12,就是第一个需要扩容时的大小
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        table = new Entry[capacity]; //指定容量的Entry对象放到数组中
        initHashSeedAsNeeded(capacity);
    }
    private static int roundUpToPowerOf2(int number) {
        // assert number >= 0 : "number must be non-negative";
        return number >= MAXIMUM_CAPACITY
                ? MAXIMUM_CAPACITY
                : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
    }

put方法中,当 数组table 为空时,通过inflateTable 初始化一个数组,数组默认容量为16,其实是通过roundUpToPowerOf2 返回大于等于最接近number的2的冪数,比如传入的为16,返回16,传入13,返回16,传入7,返回8。

四、增 put

在这里插入图片描述

    public V put(K key, V value) {
    	//如果数组为空,创建一个数组
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        //单独对key== null时的处理,其实就是去判断一下是否有空key,有的话,替换掉之前的value,
        //没有的话把key为null放到链表第一位				   
        if (key == null)
            return putForNullKey(value);
        //获取key的hash值
        int hash = hash(key);
        //通过hash值和数组长度,得到key所在的位置
        int i = indexFor(hash, table.length);
        //循环遍历链表
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            //都相等的话,新传入的value值替换老的value,可以看到key是不能重复的,重复就会被替换
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
		//记录修改次数,fail-fast机制,HashMap不是线程同步的
        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

	//key == null,放到链表第一位,存在就替换掉value,不存在直接添加到首位
    private V putForNullKey(V value) {
        for (Entry<K,V> 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);
        return null;
    }
    void addEntry(int hash, K key, V value, int bucketIndex) {
    	//当前大小 > 需要扩容的容量时候, 默认 threshold = 12 ,扩容扩大2倍
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }
		//重新再创建链表 并不能保证之前的顺序
        createEntry(hash, key, value, bucketIndex);
    }
    void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }
  1. 判断数组是否为空,为空的话就初始化一个默认容量为16的数组
  2. 判断传入的key是否为空,为空的话,就调用专门的插入null key的方法
  3. 拿到keyhash值,通过indexFor找到这个hash值所对应的数组的下标位置
  4. 循环遍历所在数组所在位置的链表,如果key的hash和传入key的hash相同且(key内存地址相等 或 equals方法相等),则意味着会更新在链表中的value值,并返回旧的value值。
  5. 如果上边的条件都没满足,执行到下边,就修改状态+1 ,然后通过createEntry 创建新的Entry对象

在hash的方法中,上边有一段这样的注释Note: Null keys always map to hash 0, thus index 0.

注意:空键始终映射到散列0,因此索引为0。我们在看putForNullKey这个方法,其实它是为了NULL值专门设置的,NULL值的hash始终为0,所以key为NULL的Entry对象肯定在数组的第0个位置。同样,如果找到则更新,没有找到则添加。

调用addEntry方法意味着要往这个数组链表中添加一个Entry,所以会在最开始判断已经存在的Entry数量是否超过了实际可容纳量(默认是16 *0.75 = 12)。如果超过了,则会调用resize方法将数组扩大两倍,注意在扩大之后会对已经存入的Entry进行重排,原因是当初存入时IndexFor方法与数组长度有关系。接着会调用createEntry方法。

createEntry方法很简单,就是将原本在数组中存放的链表头置入到新的Entry之后,将新的Entry放入数组中。从这里我们可以看出HashMap不保证顺序问题
在这里插入图片描述

五、查 get方法,contains方法

    public V get(Object key) {
    	//HashMap 对key 为null的时候,都有一个专门的处理方法
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);
		//entry不为空的时候,返回value
        return null == entry ? null : entry.getValue();
    }

    private V getForNullKey() {
        if (size == 0) {
            return null;
        }
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null)
                return e.value;
        }
        return null;
    }
    
    final Entry<K,V> getEntry(Object key) {
        if (size == 0) {//长度为0返回null
            return null;
        }

        int hash = (key == null) ? 0 : hash(key);//获得key的hash值,为null时hash==0
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {//hash和数组长度获取对应的索引位置,再得到指定位置的entry
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }
  1. 首先判断传入的key 是否为null,如果是null直接调用getForNullKey;
  2. getForNullKey 其实类似put放入null key的方法,这个方法也是专门处理key 为 null的情况,当map不为空的时候,直接取数组中的第一个元素 table[0],然后它的value
  3. 当key不是null的时候,就用去Entry中取值,通过getEntry方法,返回一个Entry对象
  4. getEntry方法依旧是,当map不是空的时候,通过传入key的hash值,然后indexFor将hash值转换为key所在的位置,然后拿到指定位置的entry对象,再去比较key的hash和传入key的hash相同且(key内存地址相等 或 equals方法相等)
  5. 最后,通过一个三元运算符,如果entry不为null,返回entry指定key对应的value值,否则返回null。

contains方法 ,直接调用的getEntry方法。

六、删除 remove

    public V remove(Object key) {
        Entry<K,V> e = removeEntryForKey(key);
        return (e == null ? null : e.value);
    }

    final Entry<K,V> removeEntryForKey(Object key) {
        if (size == 0) {
            return null;
        }
         // 依旧是根据key算hash值再根据hash值和数组长度算出索引值,如果key是null,则索引就是0
        int hash = (key == null) ? 0 : hash(key);
        int i = indexFor(hash, table.length);
        // prey是该索引位置的链表的上一个节点,当前节点e,下一个节点next
        Entry<K,V> prev = table[i];
        Entry<K,V> e = prev;
		// 开始遍历链表
        while (e != null) {
            Entry<K,V> next = e.next;
            Object k;
            //这里类似get,put方法  如果各种都相等,那么就认为找到了给定的key对应的entry
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k)))) {
                // 修改次数modCount加1,key-value数量size减1
                modCount++;
                size--;
                if (prev == e)
                    table[i] = next;
                  // 将上一个节点prev的next指向当前节点的next,即跳过了当前节点
                // 此时当前节点没有任何引用指向,它在程序结束之后就会被gc回收
                else
                    prev.next = next;
                e.recordRemoval(this);
                return e;
            }
            prev = e;
            e = next;
        }

        return e;
    }
  1. HashMap是一个数组与链表的复合结构,主要部分是Entry,通过对keyhash确定数组索引,通过hash值和equals方法比较key进行put
  2. 计算数组索引时,是通过hash&(length-1)的方法进行计算的,效率大大高于取模。此时,要保证数组的length为2的整数次幂,这样能减少hash冲突,使元素散列均匀。
  3. HashMap的默认初始容量为16,加载因子为0.75,当达到容量*加载因子时会调用resize()进行扩容,扩容大小为一倍。扩容操作需要将数组复制,重新计算每个元素位置,十分消耗性能,要尽量避免扩容操作。
  4. HashMap是无序的,允许空值空键,但不允许重复的key,所以只能有一个null key
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值