前言:HashMap是最常用到的一个数据集合。相信对每个学Java的人来说,他的重要性都是不言而喻的。所以开了一个系列,记录自己相关的一些知识点。
HashMap底层数据结构介绍
HashMap 主要用来存放键值对,它基于哈希表的Map接口实现,是常用的Java集合之一。
JDK1.8 之前 HashMap 由 数组+链表 组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的.JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值)时,将链表转化为红黑树(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树),以减少搜索时间。
解释
数组:采用一段连续的存储单元来存储数据。对于指定下标的查找,时间复杂度为O(1);HashMap是基于哈希表的,哈希表的事件复杂度O(1)的实现也依赖于数组结构(键值散列在固定长度的内存区域内)。所以HashMap的主题也是数组。用bucket(桶)代表数组中位置。
线性链表:查找操作需要遍历链表逐一进行比对,复杂度为O(n)。对于可能的哈希冲突,就需要用到线性链表。用bin代表链表中的数据:所有映射到同一个bucket的数据。
处理哈希冲突的一个方法是链地址法(又叫拉链法),这种方法的基本思想是将所有哈希地址相同的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的主体数组中,因而查找、插入和删除主要在同义词链中进行。
红黑树:红黑树就是自平衡的二叉搜索树,所以能够在发生哈希冲突时,依旧提供较好的查找性能。
HashMap类中的重要变量
HashMap的主干是一个Node数组。Node是HashMap的基本组成单元,每一个Node包含一个key-value键值对。
/**
* The table, initialized on first use, and resized as
* necessary. When allocated, length is always a power of two.
* :该表在首次使用时初始化,并根据需要调整大小。 分配时,长度始终是2的幂
* (We also tolerate length zero in some operations to allow
* bootstrapping mechanics that are currently not needed.)
*/
transient Node<K,V>[] table;
- 存放key-value 的集合
transient Set<Map.Entry<K,V>> entrySet;
- Node节点
Node是HashMap中的一个静态内部类。代码如下
Node 中的hash存储的就是key的hash值。Node还重写了hashcode方法和equal方法。后面会讲到为什么要重写这两个方法的。
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
- 类元素
// 序列号
private static final long serialVersionUID = 362498820763181265L;
// 默认的初始容量是16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认的填充因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 当桶(bucket)上的结点数大于这个值时会转成红黑树
static final int TREEIFY_THRESHOLD = 8;
// 当桶(bucket)上的结点数小于这个值时树转链表
static final int UNTREEIFY_THRESHOLD = 6;
// 桶中结构转化为红黑树对应的table的最小大小
static final int MIN_TREEIFY_CAPACITY = 64;
// 存储元素的数组,总是2的幂次倍
transient Node<k,v>[] table;
// 存放具体元素的集
transient Set<map.entry<k,v>> entrySet;
// 存放元素的个数,注意这个不等于数组的长度。
transient int size;
// 每次扩容和更改map结构的计数器
transient int modCount;
// 临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容
int threshold;
// 加载因子
final float loadFactor;
- 负载因子 loadFactor
是控制数组存放数据的疏密程度,也是一个扩容阈值,每当loadFactor达到指定值时,就会对数组进行扩容。
/**
* The load factor for the hash table.
*
* @serial
*/
final float loadFactor;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
为什么默认负载因子为0.75?
加载因子如果定的太大,比如1,这就意味着数组的每个空位都需要填满,即达到理想状态,不产生链表。
但实际是不可能达到这种理想状态,如果一直等数组填满才扩容,虽然达到了最大的数组空间利用率,但会产生大量的哈希碰撞,同时产生更多的链表。
但如果设置的过小,比如0.5,这样一来保证了数组空间很充足,减少了哈希碰撞,这种情况下查询效率很高,但消耗了大量空间。
因此,我们就需要在时间和空间上做一个折中,默认情况取loadFactor=0.75
到这里其实差不多了解了HashMap是什么,长啥样,存了啥东西。后面就可以去详细了解HashMap的get和put方法还有扩容等等等内容了、