核心属性
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代替) 保存数据的结构如上图所示。
- 先解释一下数组吧
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中表现明显。
- 第二个问题线性链表
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算法存储数据的优势在于计算性能优于循环遍历
- hash的优势
数组(arrayList): 的优势在于查询,可以根据下标直接获取到某个位置的值,修改元素的性能也不错,但是插入,删除元素复杂度高,需要移动原有的数组元素,并且根据元素值查询元素的性能低,需要遍历。
链表: 增加,删除,修改 性能都不错,但是查询需要通过遍历。数据量大了,遍历对比消耗比较大
hash: 正是通过计算的方式,确定元素的存储位置。从而避免了通过遍历查找元素的缺点,增删改查性能都不错, - 缺点就是 无序。
以及扩容时,需要对原有的数据进行重新计算存储位置,从新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;
}