深入学习Java之HashMap - 未完成
前言
在前面的几个小节中,我们学习了List接口以及List接口下的几个常用的实现,ArrayList
、LinkedList
、Vector
,接下来的几个小节里,我们将继续学习容器中比较常用的一些实现,包含Map接口、Set接口以及它们对应的实现,本小节主要来学习Map
接口及其实现HashMap
HashMap的继承结构
从上图中可以看到,HashMap实现了Map接口,Cloneable接口以及Serializable接口,并且继承AbastractMap抽象类,其中的Cloneable、Serializable接口只是标记接口,而且我们在前面的小节中已经学习过,所以这里我们就不展开了
同之前学习List一样,我们先从宏观上来学习Map接口以及AbstractMap,然后再深入学习HashMap,剖析HashMap的源码实现
Map接口
所谓的Map,其实就是键值对映射的集合,所谓的键值对,就是指一个由键和值组成的二元组,其中可以通过键来获取值,而且一般来说,如果键相同,则对应的值是相同的,也就是说,如果一个Map中有两个相同的键值对,则他们理论上是同一个键值对。数组可以理解为最简单的键值对集合,也就是最简单的Map,其中的索引就是键,也就是Key,数组中的元素就是值,也就是Value,比如a[1] = a, a[2] = b
其中的 1、2是键,而a、b就是它们所对应的值,也可以把Map理解为就是把key映射到value的一个数据结构
接下来我们来看下Java中的Map接口
从上图中可以看到,Map接口中提供了非常多的方法,接下来我们来简单了解各个方法的作用
- contain开头的方法主要用于查看是否map中是否包含该元素,如
containKey
、containValue
- get方法用于根据key获取对应的值
- put开头的方法用于将键值对放入map中,如
put()
、putAll()
- remove开头的方法用于将键值对从map中移除
keySet
、values
、entrySet
分为用于获取map中键的集合,值的容器以及键值对的集合
Map中还有一个非常重要的元素,Entry,用于对应存放在Map中的元素的形式,也就是上面所说的键值对,结构如下
从上图中可以看到,Entry接口中定义了操作一个Entry的方法,如获取键、获取值、根据键设置值等,这几个方法相对来说比较见名之意,所以这里我们就不做细致的展开,等到具体学习的时候再进行展开
AbstractMap抽象类
从上面的Map的结构图中可以看到,AbstractMap实现了Map接口,AbstractMap中实现了Map接口中部分通用的方法,如下面具体代码所示
查看Map中是否包含某个值
public boolean containsValue(Object value) {
// 获得EntrySet的迭代器
Iterator<Entry<K,V>> i = entrySet().iterator();
// 如果输入的值是null,则查找第一个null元素
if (value==null) {
while (i.hasNext()) {
Entry<K,V> e = i.next();
if (e.getValue()==null)
return true;
}
// 如果不是null,则查找对应的值
} else {
while (i.hasNext()) {
Entry<K,V> e = i.next();
if (value.equals(e.getValue()))
return true;
}
}
return false;
}
查看Map中是否包含某个键
public boolean containsKey(Object key) {
// 获取EntrySet的迭代器
Iterator<Map.Entry<K,V>> i = entrySet().iterator();
// 判断输入的键是否是null,如果是,则查看键为null的entry
if (key==null) {
while (i.hasNext()) {
Entry<K,V> e = i.next();
if (e.getKey()==null)
return true;
}
// 如果不是null,则查找对应的键
} else {
while (i.hasNext()) {
Entry<K,V> e = i.next();
if (key.equals(e.getKey()))
return true;
}
}
return false;
}
根据key获取值
public V get(Object key) {
Iterator<Entry<K,V>> i = entrySet().iterator();
if (key==null) {
while (i.hasNext()) {
Entry<K,V> e = i.next();
if (e.getKey()==null)
return e.getValue();
}
} else {
while (i.hasNext()) {
Entry<K,V> e = i.next();
if (key.equals(e.getKey()))
return e.getValue();
}
}
return null;
}
删除值
public V remove(Object key) {
Iterator<Entry<K,V>> i = entrySet().iterator();
Entry<K,V> correctEntry = null;
if (key==null) {
while (correctEntry==null && i.hasNext()) {
Entry<K,V> e = i.next();
if (e.getKey()==null)
correctEntry = e;
}
} else {
while (correctEntry==null && i.hasNext()) {
Entry<K,V> e = i.next();
if (key.equals(e.getKey()))
correctEntry = e;
}
}
V oldValue = null;
if (correctEntry !=null) {
oldValue = correctEntry.getValue();
i.remove();
}
return oldValue;
}
AbstractMap中还有其他一些实现方法,不过由于这些方法在Map的不同实现中会有不同,所以这里我们就不做过多的展开了
深入学习HashMap
在前面的内容中,我们从宏观的角度学习Map的一些常用方法,以及AbstractMap中实现的Map的几个方法,接下来我们将摄入地来学习Map实现类之一的HashMap,并且对HashMap的源码进行剖析
Hash结构的简单介绍
哈希结构,也就是Hash,是一种常用的数据结构,主要就是通过将键进行哈希计算,将大范围的数据映射到一个小的范围中,从而减少对其所占用的空间,比如说,有数据范围在1-100w的数据,而这些数据可能只有1000个,如果采用数组来存放,则需要的空间时非常大的,而且,造成的浪费也是非常明显的,这个时候,如果采用一个hash函数,将1-100w的数据范围映射到一个比较小的空间,比如最简单的MOD 1w(也就是哈希映射,MOD 1W,也就是所采用的哈希函数)则将数据的空间有效地减少了,不过由于将大范围的数据映射到小范围,则必然会造成一些数据映射到同一个空间,这就是哈希冲突,而解决哈希冲突除了需要一个良好的哈希函数外,还需要有处理哈希冲突的方法
常用的哈希函数
- 直接寻址法,取key或者key的某个线性函数
- 数字分析法,根据key自身的特性,选取某几位
- 平方取中法,key平方后取中间几位
- 折叠法,将key切割成位数一样的几个部分,然后进行折叠
- 随机数法,采用随机函数
- 除留余数法,key MOD一个数,上面举例所采用的方法
解决哈希冲突的方法
- 开放地址法
- 再哈希法
- 链地址法(比较常用),将key相同的元素组成一个链
如果对于上面的概念不是很熟悉的话,则需要额外查看资料进行补充,可以参考常见hash算法的原理、哈希冲突的处理方法
HashMap源码剖析
HashMap,是一个非常常用的数据结构,其本质就是对key做了hash的Map的集合,由于采用了Hash方法,HashMap的取元素的效率非常高,接近于O(1),而存放,删除元素的效率也是比较高的,接下来我们来剖析JDK中HashMap的实现,从前辈们的代码中学习具体的HashMap的具体实现
需要注意的小细节
- HashMap是允许null值以及null键的,也就是说,在HashMap中,key=null是允许的,value=null也是允许的
- HashMap是非线程安全的
- HashMap中的元素的顺序是无法保证的
- HashMap中有两个比较重要的属性,装填因子(loadfactor,默认为0.75)和容量(bcapacity)
HashMap的成员
// 默认容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4
// 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30
// 默认装填因子
static final float DEFAULT_LOAD_FACTOR = 0.75f
// HashMap的核心,本质就是键值对节点数组,数组中的每个元素都是一个链表
// 从这里也可以看出,HashMap中采用的哈希冲突解决方法为链地址法
transient Node<K,V>[] table;
// HashMap中所有的键值对集合
transient Set<Map.Entry<K,V>> entrySet;
// 键值对数量
transient int size;
// 装填因子
final float loadFactor;
// 阈值,当容量达到该值时,进行扩容
int threshold;
// 节点,也就是前面所提到的键值对
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; }
// 计算hashcode
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
// 设置值并且返回旧值
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
// 判断两个Node是否相等,只有键以及值都相等才算相等
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;
}
}
构造方法
// 提供初始容量和装填因子来构造
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;
this.threshold = tableSizeFor(initialCapacity);
}
// 通过给定的数值计算大小
// 这里计算的目的的使得n的左边的第一个1的右边全部为1
// 最大值为2^32,然后执行n+1,也就是产生进位,使得
// 所有n成为原本的值的2倍中最近接2的幂的数
// 好厉害啊,原来还可以这么做,学习了:)
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
// 仅提供初始容量,采用默认的装填因子,也就是0.75f
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
// 使用另一个Map来构造,此时采用默认的装填因子
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
// 将一个Map中的元素放入HashMap
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
// 如果此时的HashMap中没有元素,也就是采用该Map的元素来初始化HashMap
if (table == null) {
// 使用该map的大小/装填因子,计算出此时所需要的table的大小
// 装填因子 = 实际使用容量/table大小
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
if (t > threshold)
threshold = tableSizeFor(t);
}
// 如果此时的hashMap不为空,则判断所需要的大小是否已经超过需要进行扩容的阈值,如果超过,则进行扩容
else if (s > threshold)
resize();
// 遍历该map,并且将所有的键值对放入HashMap中
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
// 调整大小,也就是扩容
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
//如果原来的hashMap中已经有元素了
if (oldCap > 0) {
// 如果就容量已经超过最大值,则将阈值调整至Integer.MAX_VALUE
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 将新容量调整为原来的两倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // 将新阈值调整为原来的两倍
}
else if (oldThr > 0) // 如果阈值大于0,则将新容量设置为阈值
newCap = oldThr;
else { // 将容量设置为默认容量,也就是16,并且计算初始时的阈值
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
// 如果是树节点,则按树的操作方式,这里的底层是红黑树,不过
// 目前还没有学习到,无法对其进行解析
// HashMap果然复杂,还要好好加油才是 :(
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
// 将原本的数据取出
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
// 将指定的键值对放入HashMap中
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 这里写得非常精简,首先tab指向table然后判断table是否为空
// 不为空则n=tab的长度,如果n=0,则进行扩容并且获取扩容后的长度
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 根据hash值计算元素所要放入的位置,如果此时该位置没有元素,则直接放入即可
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 如果有元素,则说明产生了hash冲突
else {
Node<K,V> e; K k;
// 判断所要插入的元素是否是第一个元素,判断的标准为hash相等,key相等
// 如果是,则直接替换
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 判断是否是树节点,这里还没有弄明白:(
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 这里目前还只是知道将其插入而其原理还不了解
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
本来想继续研究下去的,不过,感觉有一些内容还不了解,所以目前只研究到这里,等过两天研究懂了再进行补充