HashMap源码

核心属性

HashMap的设计思想:

HashMap也叫散列表。由数组和线性链表组成。存储数据的核心是一个 Entry[] 的table数组。
  • 线性链表
    先看HashMap的一个内部类Entry
	Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;
   }

**HashMap保存数据的容器也是数组,Entry[],
原子集Entry被设计成一个线性链表,保存当前元素以及下一个Entry的索引。

**
HashMap中的容器 Entry[] 数组 (即table,下文统一用table代替) 保存数据的结构如上图所示。

  1. 先解释一下数组吧
    table是一个数组,但是table保存的数据是非连续的,也就是说table中可能低位为空,高位反而存储了数据。而不像ArrayList那种,一个元素挨着一个元素。
    那他是如何解决索引问题的呢?
    我们都知道他是根据hash算法去存储数据的。具体操作如何呢?请看源码。
//HashMap 添加一个元素的 源码
public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);//对于无参构造器初始化,没有初始化“临界值”,所以要在此初始化临界值。
        }
        if (key == null)
            return putForNullKey(value);//key为null的处理方式
        int hash = hash(key);//1. 获取key的hash值
        int i = indexFor(hash, table.length);// 2. 根据key的hash值,求key在当前数组长度下的索引。
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {//3. 获得当前索引下的 table保存的元素 entry。
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {3.1 当key相同时,替换原来的value值。
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);//entry中添加key value
        return null;
    }
    void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {//4. size大于临界值 ,并且bucketIndex这个桶已经存储过元素的时候,需要扩容,扩容为原来的两倍
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);//重新计算当前元素的hash在 新的容器大小下的 索引。
        }

        createEntry(hash, key, value, bucketIndex);// 5. 添加元素
    }
    void createEntry(int hash, K key, V value, int bucketIndex) {// 6. 添加元素,注意新增元素是添加在线性链表的头部,而不是末尾,这种设计可以减少寻找链表末尾时的遍历,提升性能。
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }

由此我们可以发现,HashMap是根据 key的hash值,然后与容器进行逻辑与操作。得到当前元素在table中保存的索引。这样 插入和删除元素 就不存在ArrayList那样的需要copy插入或删除点之后的所有元素的问题。另外,根据元素本身查询元素也可以根据计算来求取,而不需要通过遍历,这一优势在HashSet中表现明显。

  1. 第二个问题线性链表
    table的每一个索引下挂着的并不是一个entry。而是一条entry,一个链表。
  • 问题一:
    HashMap<String, String> map = new HashMap<String, String>(4);
    map.put(“aa”, “2”);
    map.put(“cc”, “1”);
    map.put(“bb”, “1”);
    aa和cc在数组为4的bucketIndex(索引)相同。采用的便是aa挂在cc这个Entry的next下面,
    为什么aa挂在cc下面?
    添加元素的时候只需要将当前的table[bucketIndex]赋值给新建的 entry的next就可以了。如果要cc挂在aa下面,则需要遍历table[buketIndex],找到末尾节点,这样无形中增加了不必要的性能损耗。

  • 问题二:
    扩容过程前面已经提过了,并不是size大于threshold就扩容,可以这样理解。hashmap的扩容并非那么死板。可能threshold=16,size=17就需要扩容,也可能size=20都依然没有扩容。比如size<16的时候,之前15条数据计算出来的bucketIndex=0,那么下一个bucketIndex= 1/2/3/。。。都不会扩容,只有当 bucketIndex再一次重复时才扩容。

  • 问题三:扩容时之前的数据怎么copy。

    采用双层遍历,一层遍历数组,一层遍历Entry,由于entry中保存了key,所以在新的数组中可以重新计算新的bucketIndex值。也就意味着,每次扩容都需要对原有的数据进行重新分配一次。对于数据量比较大的集合,可以预先设定初始化大小。

  • 问题三:key相同时怎么处理。
    根据bucketIndex获取到entry,然后遍历entry,当key相同时,则替换。

  • 问题四:取值逻辑。
    根据传参key 计算hashcode,根据当前数组长度,计算bucketIndex。根据bucketIndex获取entry。
    然后遍历entry,当entry的 key值等于 key时,返回 entry,返回value;

  • 问题五:为什么加载因子越大,查询效率越低。
    因为装载因子越大,添加元素的时候,发生碰撞的几率越高。一旦发生碰撞,存储的数据某个桶就呈链表结构,查询的时候根据hashcode获取到bucketIndex,再获取到entry,碰撞越多,entry越长。查询遍历消耗的时间就越多。根据hash算法存储数据的优势在于计算性能优于循环遍历

  1. hash的优势
    数组(arrayList): 的优势在于查询,可以根据下标直接获取到某个位置的值,修改元素的性能也不错,但是插入,删除元素复杂度高,需要移动原有的数组元素,并且根据元素值查询元素的性能低,需要遍历。
    链表: 增加,删除,修改 性能都不错,但是查询需要通过遍历。数据量大了,遍历对比消耗比较大
    hash: 正是通过计算的方式,确定元素的存储位置。从而避免了通过遍历查找元素的缺点,增删改查性能都不错,
  2. 缺点就是 无序。
    以及扩容时,需要对原有的数据进行重新计算存储位置,从新copy一遍。复杂度为 大致为 二分之n平方减去n,所以对于数据量比较大时,可以考虑初始化一个较大一点的 initialCapacity

源码解析

属性

    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 	// aka 16 默认初始化大小。
    static final int MAXIMUM_CAPACITY = 1 << 30;				//最大容积
    static final float DEFAULT_LOAD_FACTOR = 0.75f;			//默认加载因子,越大查询性能越慢,越小越浪费空间
    static final Entry<?,?>[] EMPTY_TABLE = {};						//空的table
    transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;	//元素容器
    transient int size;				//map中装载元素的个数,并不是占用桶的数量,因为每个桶可以再挂桶,也就是同一个hash,可以存多个值。
	int threshold;		//capacity*factor的结果,临界容积。如果size小于threshold,bucketIndex(为key在此capacity下的hash值)若已有值,则添加在此值的index上面。若size大于threshold,且bucketIndex有值 才扩容。
 
    final float loadFactor;//加载因子
    transient int modCount;//修改次数

构造方法,初始化集合的大小和加载因子,以及集合装载元素的临界值

public HashMap() {
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
public HashMap(int initialCapacity) {
    this(initialCapacity, 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();//啥事也没做

}

添加对象

添加数据的逻辑

	public V put(K key, V value) {//
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);//初始化临界值
        }
        if (key == null)
            return putForNullKey(value);//键为空的时候的处理
        int hash = hash(key);//计算key的hash值
        int i = indexFor(hash, table.length);//hash与table数组的长度 进行与 运算,计算出当前数据存储在数组中的位置。
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {//遍历数组中的index位,是否已经包含了当前key的
//元素。如果有,则,进行替换。
            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;
}
// bucketIndex 当前数据存放的位置。
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);
    }
	//扩容
   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);
}
//对原数据进行重新分配。
	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);//求key的hash在新的capacity中的 下标。
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }
// 获取对象
public V get(Object key) {
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);
        return null == entry ? null : entry.getValue();
    }
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)]; e != null; e = e.next) {//计算hash在capacity中的index,并获取index处的entry
            Object k;
            if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))//遍历entry,如果entry的key等于入参,则返回entry
                return e;
        }
        return null;
    }
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值