jdk1.7之hashMap

源码分析

对于学习hashmap最好的方式就是读懂源码,并且独立进行设计一个hashmap的算法即手写hashMap。

数据结构

jdk1.7中hashmap的数据结构是数组+链表,在其中链表的节点存储的是Entry对象,Entry为一个链表中的一个节点,每个Entry对象都存在这属性:

static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;//key值
        V value;//存储的value
        Entry<K,V> next;//当前节点下链表下一个Node的指向
        int hash;

概念图如图:
在这里插入图片描述

HashMap一些参数的了解

	//hashMap的初始容量
	static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
	//hashMap的最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30;
	//hashMap的负载因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

部分问题

1.hashMap初始容量为16的原因需要在效率和内存使用上做一个权衡。这个值不能太大,也不能太小。太小了就可能会频繁的发生扩容,影响效率;太大了又浪费空间,不划算。
2.hashMap的容量为什么是2的n次幂:充分利用数组空间,扩容后,数组下标重新计算。

hashMap之构造方法

构造方法较为简单,里面只存在了部分校验,但是实际并未初始化hashmap。虽然hashMap默认的初始化容量是16,但是在其构造方法中是可以自定义容量和负载因子的

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方法并未进行初始数组,但是在hashMap的子类LinkHashMap中倒是实现此方法
        init();
    }
//hashMap中的实现
void init() {
    }    

hashMap之put方法

public V put(K key, V value) {
		//判断Entry数组对象是否为空,如果是空的话进行初始化数组,使用的是懒加载
        if (table == EMPTY_TABLE) {
        	//进行数据的初始化
            inflateTable(threshold);
        }
        //如果key值为null的话,走另外分支
        if (key == null)
            return putForNullKey(value);
        //对传入的key值进行hash,并不是单纯的hashCode方法,里面还存在再多个右移和异或运算,这样			   的目的是为了让hash值更加散列。
        int hash = hash(key);
        //计算数组下标位置,利用上述的hash值和数组的长度进行与运算
        int i = indexFor(hash, table.length);
        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;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }
//初始化数组
 private void inflateTable(int toSize) {
        //传入初始化容量,进行计算出大于等于此数据的2的幂次方数据
        int capacity = roundUpToPowerOf2(toSize);
		//计算新的阈值
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        //生成新的数组对象
        table = new Entry[capacity];
        
        initHashSeedAsNeeded(capacity);
    }
//放入key值为null的方法
private V putForNullKey(V value) {
		//重点,如果key值是null,那么放入的就是这个数据的第一个位置。
        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;
    }

重点代码剖析:

hashMap之扩容

void addEntry(int hash, K key, V value, int bucketIndex) {
        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);
    }
//hashMap扩容方法
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
        table = newTable;
        //重新计算阈值
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }
//数组数据转移
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);
                //此处为断链操作,是将原有链表的next指针指向null。
                e.next = newTable[i];
                //将e赋值给新的数组
                newTable[i] = e;
                //进行循环
                e = next;
            }
        }
    }

数据转移图解:
数组扩容
断链,赋值,循环
在这里插入图片描述
此处省略3,4node的移动,原理相同,最终e变为null,循环停止。
移动完成

此段代码是造成jdk1.7中出现线程不安全的主要因素。此段代码如果发生多线程即高并发操作的话,会将会形成环状,形成循环链表。

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);
                //此处为断链操作,是将原有链表的next指针指向null。
                e.next = newTable[i];
                //将e赋值给新的数组
                newTable[i] = e;
                //进行循环
                e = next;
            }
        }
    }

形成环状链表的原因图解如下:
在这里插入图片描述

数组A数据转移完成,此时数组B的元素还没有进行操作,指针跟随数组A进行移动;
数组B转移e元素
转移第二个元素
数组B转移第三个元素
最后一次转移其实转移的还是1:1元素,此时已经行程循环链表,并且丢失4:4,3:3元素,循环结束。
注:
此处链表此次链表的形成是指两个多线程同时执行到tranfers方法的循环中,线程A正常执行,而线程B执行到Entry<K,V> next = e.next;失去cpu时间,挂起,直至线程A完成循环,线程B执行。

hashMap之get方法

final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }
		//如果key是null值得话直接去数组头以为即table[0]
        int hash = (key == null) ? 0 : hash(key);
        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 != null && key.equals(k))))
                return e;
        }
        return null;
    }

面试题

链表的插入方式

jdk1.7使用的是头插法,这也是发生环形链表的原因所在。使用头插法,扩容后链表的位置与原位置相反。

扩容机制

初始容量(16)*负载因子(0.75L)并且发生了hash冲突(即插入的数组的位置有元素存在),在极端的情况下,jdk1.7的数组在不扩容的情况下是可以填满16个数组位置的。扩容的方式是现有数组容量*2(2倍扩容)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值