1.7环境下的HashMap源码

HashMap是大家在经常使用的一个容器,那么他在1.7环境下到底是怎么实现的,我们一起来看一下

1、HashMap构造方法

public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }
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();
    }
    void init() {

我们可以看到,在你调用空参的构造函数的时候,他会自动调用2个参数的构造方法,其中DEFAULT_INITIAL_CAPACITY是默认的数组初始值,大小是1 << 4 = 16,DEFAULT_LOAD_FACTOR是默认的加载因子,与扩容有关,默认值是0.75f。在构造方法中回进行一些赋值操作,包括阈值和加载因子,至于init()方法,他在1.7中是一个空方法。在1.8中会有实现。

2.put方法

public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);//初始化
        }
        if (key == null)//key为空的时候进行操作
            return putForNullKey(value);
        int hash = hash(key);//得到hash值,进行一些异或操作(不同为1,相同为0)
        int i = indexFor(hash, table.length);//得到数组下标
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {//从table[i]的头节点开始循环,遍历这个索引上的链表,寻找是否有key相同的值
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {//判断key和hash是否相等
                V oldValue = e.value;//如果相等,得到未被覆盖时候的值
                e.value = value;//覆盖原有值
                e.recordAccess(this);//hashmap中未被实现,空方法
                return oldValue;//返回旧值
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

我们能看到,当table为空的时候,他会进行table的初始化,调用inflateTable(threshold)方法。threshold是一个阈值,初始化的时候被赋值为DEFAULT_INITIAL_CAPACITY,也就是16

private void inflateTable(int toSize) {
        // Find a power of 2 >= toSize找到一个大于toSize的2的幂次方的数
        int capacity = roundUpToPowerOf2(toSize);//处理toSize

        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        table = new Entry[capacity];//初始化数组
        initHashSeedAsNeeded(capacity);
    }

其中,他roundUpToPowerOf2(toSize)方法,如果toSize大于最大的数组值,就将他赋值为MAXIMUM_CAPACITY,否则执行(number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1,
Integer.highestOneBit((number - 1) << 1),这个方法内部是对number - 1进行一些右移操作,得到一个小于number - 1的2的幂次方数,至于为什么是number - 1呢,因为如果number刚好是一个2的幂次方数的话,减一是为了得到他本身而不是得到比他小的一个2的幂次方。最后对这个幂次方数左移,一位也就是乘以二,返回一个大于等于toSize的2的幂次方数。最后得到的capacity 是初始化数组的大小。然后重新给threshold赋值,最后初始化数组。至于initHashSeedAsNeeded(capacity)是为了看一下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;
    }

然后判断一下key是否为null,当key为空时候会有一个单独的逻辑去处理,也就是putForNullKey(value)方法。

private V putForNullKey(V value) {
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {//遍历索引为0的位置上的链表
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(0, null, value, 0);//key为null的时候放在数组的第0个位置
        return null;
    }

这个方法是逻辑比较简单的,就是遍历table[0]位置上的链表,如果找到key为null的Entry,就会覆盖原来的value,将原来的value返回。如果没有,直接执行addEntry(0, null, value, 0)方法,然后返回null。至于addEntry方法时区判断是否需要扩容。我们待会研究扩容的逻辑。
继续put方法的逻辑,他内部会使用一些异或操作得到key的hash值,再使用hash值和table的长度得到存储位置的下标,再然后就去遍历这个存储位置下的链表,如果找到key值相同的entry,就覆盖value,并且返回oldValue,没找到,执行addEntry方法。

void addEntry(int hash, K key, V value, int bucketIndex) {
    	//threshold = table.length(默认16) * loadFactor(默认0.75)
    	//只有当size >= threshold,且当前数组索引的位置有元素的时候才扩容,如果没有元素就不会扩容
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);//扩容,原来数组的2倍
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);//得到下标
        }

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

至于这个方法,主要逻辑就在于扩容方法,resize方法。至于createEntry方法是比较简单的,不在此细说

void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];//得到链表的头节点
        table[bucketIndex] = new Entry<>(hash, key, value, e);//头插法,e是下个节点
        size++;
    }

我们主要来看一下扩容方法,resize(2 * table.length);可看出是扩容为原来的两倍

void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {//如果数组最大,就不会扩容
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry[] newTable = new Entry[newCapacity];
        transfer(newTable, initHashSeedAsNeeded(newCapacity));//转移
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

首先拿为扩容之前的table,如果数组长度等于MAXIMUM_CAPACITY,就不再扩容,否则创建一个新的数组,然后transfer方法转移数据。最后改变他变了指向,重新赋值一下threshold ,下面我们来看一下transfer方法。

void transfer(Entry[] newTable, boolean rehash) {//多线程情况下可能会出线循环链表的问题
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {//循环
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }

这个方法内部逻辑也比较简单,就是循环旧table,利用key的hash值与新数组来得到在新数组下存储的下标,使用头插法转移到新数组,但是这样的方式在多线程的情况下会出现循环链表的问题,所以并不推荐在循环链表下调用此方法,所以在多线程情况下要么尽量不要去扩容,要么就去使用ConcurrentHashMap这个类去解决。

3.get方法

public V get(Object key) {
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);

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

get方法比较简单,首先判断key是否为null,如果为null,调用getForNullKey方法

private V getForNullKey() {
        if (size == 0) {
            return null;
        }
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {//key为null的时候,在table[0]位置找,因为存的时候,key为null就存在table[0]处
            if (e.key == null)
                return e.value;//找到,返回值
        }
        return null;
    }

这个方法比较简单,就是直接循环遍历table[0]的位置,因为key为null的entry在put的时候就直接put在table[0]的位置,所以就直接在这个位置上遍历链表去查找。如果不为null,调用getEntry(key)。

    final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }
        int hash = (key == null) ? 0 : hash(key);//计算hash
        for (Entry<K,V> e = table[indexFor(hash, table.length)];//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;//找到就返回entry对象
        }
        return null;
    }

这个方法就是利用key的hash和table的长度去计算key所在的tabel数组的位置,去遍历查找链表,如果找到就返回entry,否则就返回null

总结

以上就是1.7环境下HashMap的部分主要方法的源码,感谢您的观看。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值