使用key-value键值对映射的Map是非常经典的数据结构,作为Map的子类,HashMap是最常用的Map,根据键的hashcode值存储数据,根据键可以直接获取它的值,具有很快的访问速度。由于HashMap源码在JDK1.7和JDK1.8版本的实现不同,这里讲的是1.8版本的HashMap。
PS:1.7和1.8版本的不同主要在于JDK1.7中使用一个Entry数组来存储数据,所有hashCode取模后相同结果的key都会被定位到Entry数组的同一个格子,key以链表的形式存储,当多个hashCode相同时,遍历需要经过一串很长的链表,需要遍历的操作(get/put等)遍历效率都会变低,时间复杂度在最差情况下会变为O(n)。而在1.8版本中,使用Node数组来存放数据,这个Node数组在一开始是链式结构,当达到某个条件时(key>8)会调用treeifyBin(),将链表转换为红黑树,由于使用了红黑树遍历查找元素,时间复杂度最差只有O(log n),这就是1.7和1.8版本的主要不同。
首先,看一下HashMap的类继承图
继承抽象类AbstractMap的同时实现了Map, Cloneable, Serializable接口,实现了右侧所有的方法。
下图是HashMap核心的成员变量
/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* The maximum capacity, used if a higher value is implicitly specified
* by either of the constructors with arguments.
* MUST be a power of two <= 1<<30.
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* The bin count threshold for using a tree rather than list for a
* bin. Bins are converted to trees when adding an element to a
* bin with at least this many nodes. The value must be greater
* than 2 and should be at least 8 to mesh with assumptions in
* tree removal about conversion back to plain bins upon
* shrinkage.
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* The bin count threshold for untreeifying a (split) bin during a
* resize operation. Should be less than TREEIFY_THRESHOLD, and at
* most 6 to mesh with shrinkage detection under removal.
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* The smallest table capacity for which bins may be treeified.
* (Otherwise the table is resized if too many nodes in a bin.)
* Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
* between resizing and treeification thresholds.
*/
static final int MIN_TREEIFY_CAPACITY = 64;
DEFAULT_INITIAL_CAPACITY 是指HashMap容器的初始大小,MUST be a power of two意思是必须是2的幂次方,二进制格式中,1向左移动4位,也就是0001 --> 1 0000,即2^4=16。
为什么初始容量是16而不是4,8或其他呢?因为16的索引是0~15,在JDK中,都是用2进制的10进制数%16,结果在0~15之间。HashMap通过key的hashcode值,来进行位运算,公式:,在2的幂次方的情况下,length-1的所有二进制位全是1,hashCode码&(length - 1)的结果取决于hashCode本身,如果hashCode是均匀的,就可以减少hash碰撞情况(一个hash对应多个字符串),同时汇编处理的进制都是16进制,Java的底层是C,C的底层是汇编,越接近底层开发效率越快,所以初始容量是16位
MAXIMUM_CAPACITY 是指容量的极限值,默认设置为2^31
DEFAULT_LOAD_FACTOR 是指负载因子,默认为0.75f。负载因子的存在是非常重要的,由于初始化HashMap的容量大小是固定的,当存储的数据超过最大值*负载因子,即16*0.75=12时,将在当前容量的基础上进行扩容。JDK1.7版本是使用了rehash方法,创建新的链表,如果在新链表的数组索引位置相同,则链表元素会倒置。而JDK1.8版本中,链表元素相对位置没有变化, 实际是对对象的内存地址进行操作
TREEIFY_THRESHOLD 是指节点数,当一个元素被添加到至少有8(默认值)个节点的桶中时,桶中链表结构将转化为树形结构(红黑树)
UNTREEIFY_THRESHOLD 也是指节点数,不过是树形结构转换为链表结构
MIN_TREEIFY_CAPACITY 是指当桶被转化为树形结构的时候,此时桶所拥有的最小容量
以上是HashMap中重要的几个成员变量,接下来是核心的成员属性
/* ---------------- Fields -------------- */
/**
* The table, initialized on first use, and resized as
* necessary. When allocated, length is always a power of two.
* (We also tolerate length zero in some operations to allow
* bootstrapping mechanics that are currently not needed.)
*/
transient Node<K,V>[] table;
/**
* Holds cached entrySet(). Note that AbstractMap fields are used
* for keySet() and values().
*/
transient Set<Map.Entry<K,V>> entrySet;
/**
* The number of key-value mappings contained in this map.
*/
transient int size;
/**
* The number of times this HashMap has been structurally modified
* Structural modifications are those that change the number of mappings in
* the HashMap or otherwise modify its internal structure (e.g.,
* rehash). This field is used to make iterators on Collection-views of
* the HashMap fail-fast. (See ConcurrentModificationException).
*/
transient int modCount;
/**
* The next size value at which to resize (capacity * load factor).
*
* @serial
*/
// (The javadoc description is true upon serialization.
// Additionally, if the table array has not been allocated, this
// field holds the initial array capacity, or zero signifying
// DEFAULT_INITIAL_CAPACITY.)
int threshold;
/**
* The load factor for the hash table.
*
* @serial
*/
final float loadFactor;
table 这是一个Node数组,在第一次使用时需要初始化,并按需调整大小。当分配时,长度总是2的幂次方。(特殊情况下也允许为0)
entrySet 这是 键-值 对的集合,Set里面的类型是Map.Entry,这个接口里面设置了常用的取(设)键值方法,相当于一个JavaBean,对键值对进行了一个封装便于后面的操作
size 即map中所包含键值对的数量
modCount 用于记录集合的修改次数,包括对HashMap的映射修改以及结构修改,如果某个实现不希望提供快速失败迭代器,则可以忽略此字段
threshold 这个成员变量是阈值,是下一个容器的大小,决定了是否要将散列表再散列,它的值是capacity * load factor
loadFactor 负载因子
重点介绍table 属性,这是一个Node数组,数据存储的地方,它的结构是
数据传输进来的时候,位置的决定就是由公式 决定的,同时,next这个属性由null指向当前数据(Entry)的内存地址,例如0XFF110。当多个hashCode相同时,next指向也相同,这块内存地址就会出现一条单向链表,也就是一个接一个地往下挂,它的容量就是TREEIFY_THRESHOLD ,在JDK1.7的时候,可以一直往下挂到16个,而在JDK1.8里,只能挂到第七个(index0 --> index6),挂到第八个时就会进行红黑树的转换,存到桶的最大值64。同时,当节点减少到不足8个时,即UNTREEIFY_THRESHOLD <=6,红黑树就会转化为链表,这点在代码里的体现如下
if (loHead != null) {
if (lc <= UNTREEIFY_THRESHOLD)
tab[index] = loHead.untreeify(map);
else {
tab[index] = loHead;
if (hiHead != null) // (else is already treeified)
loHead.treeify(tab);
}
}
if (hiHead != null) {
if (hc <= UNTREEIFY_THRESHOLD)
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead;
if (loHead != null)
hiHead.treeify(tab);
}
}
以上就是核心的成员属性,下一章介绍HashMap核心的成员方法