数组
数组存储区间是连续的,进行插入和删除操作时,平均要移动数组中近一半的元素,时间复杂度为O(N)。但数组的查找时间复杂度小,为O(1);数组的特点是:寻址容易,插入和删除困难;
链表
链表存储区间离散,插入和删除操作无需移动元素,只需修改指针,但查找时间复杂度很大,达O(N)。链表的特点是:寻址困难,插入和删除容易。
哈希表
那么我们能不能综合两者的特性,做出一种寻址容易,插入和删除也容易的数据结构呢?答案是肯定的,这就是我们要提起的哈希表。事实上,哈希表有多种不同的实现方法,我们接下来解释的是最经典的一种方法 —— 拉链法,我们可以将其理解为 链表的数组,如下图所示:
1.1 HashMap 概述
Map 是 Key-Value 对映射的抽象接口,该映射不包括重复的键,即一个键对应一个值。 简单地说,HashMap 是基于哈希表的 Map 接口的实现,以 Key-Value 的形式存在,即存储的对象是 Entry (同时包含了 Key 和 Value) 。在HashMap中,根据hash算法来计算key-value的存储位置并进行快速存取。特别地,HashMap最多只允许一条Entry的键为Null(多条会覆盖),但允许多条Entry的值为Null。此外,HashMap 是 Map 的一个非同步的实现。1.2 解决hash冲突
HashMap使用链地址法来解决hash冲突,即数组+链表的组合,JDK1.8之后才引入了红黑树进行存储优化。
每个数组元素上都是一个链表结构,当数据被hash后得到数组的下标,把数据存储到对应下标的链表上。
数组的每个元素都是指向各个链表的头节点
HashMap的数据结构
首先HashMap里面实现一个静态内部类Entry,其重要的属性有 key , value, next,从属性key,value我们就能很明显的看出来Entry就是HashMap键值对实现的一个基础bean,我们上面说到HashMap的基础就是一个线性数组,这个数组就是Entry[],Map里面的内容都保存在Entry[]里面。 /**
* The table, resized as necessary. Length MUST Always be a power of two.
*/
transient Entry[] table;
static class Entry<K,V> implements Map.Entry<K,V> {
final K key; // 键值对的键
V value; // 键值对的值
Entry<K,V> next; // 下一个节点
final int hash; // 用于定位数组索引位置
/**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry<K,V> n) { // Entry 的构造函数
value = v;
next = n;
key = k;
hash = h;
}
......
}
HashMap 的构造函数
该构造函数意在构造一个具有> 默认初始容量 (16) 和 默认负载因子(0.75) 的空 HashMap,是 Java Collection Framework 规范推荐提供的,其源码如下:
/**
* Constructs an empty HashMap with the default initial capacity
* (16) and the default load factor (0.75).
*/
public HashMap() {
//负载因子:用于衡量的是一个散列表的空间的使用程度
this.loadFactor = DEFAULT_LOAD_FACTOR;
//HashMap进行扩容的阈值,它的值等于 HashMap 的容量乘以负载因子
threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
// HashMap的底层实现仍是数组,只是数组的每一项都是一条链表
table = new Entry[DEFAULT_INITIAL_CAPACITY];
init();
}
HashMap 的存储实现
在 HashMap 中,键值对的存储是通过 put(key,vlaue) 方法来实现的,其源码如下:
/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with key, or null if there was no mapping for key.
* Note that a null return can also indicate that the map previously associated null with key.
*/
public V put(K key, V value) {
//当key为null时,调用putForNullKey方法,并将该键值对保存到table的第一个位置
if (key == null)
return putForNullKey(value);
//根据key的hashCode计算hash值
int hash = hash(key.hashCode()); // ------- (1)
//计算该键值对在数组中的存储位置bucketIndex(哪个桶)
int i = indexFor(hash, table.length); // ------- (2)
//对table的第i个桶上的链表进行遍历,寻找 key 保存的位置
for (Entry<K,V> e = table[i]; e != null; e = e.next) { // ------- (3)
Object k;
//判断该条链上是否存在hash值相同且key为同一个对象的映射;
//若存在,则直接覆盖 value,并返回旧value
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue; // 返回旧值
}
}
modCount++; //修改次数增加1,快速失败机制
//原HashMap中无该映射,将该节点添加至该链表的链头
addEntry(hash, key, value, i);
return null;
}
equals()和hashCode()的应用
上述关键代码 通过比较hash值和equals方法判断是否存在相同的key
Object类的两个方法hashCode和equals,我们先来看一下这两个方法的默认实现:
/** JNI,调用底层其它语言实现 */
public native int hashCode();
/** 默认使用==,直接比较是否同一个对象的引用 */
public boolean equals(Object obj) {
return (this == obj);
}
对象通过调用 Object.hashCode ( ) 生成晗希值,由于不可避免地会存在晗希值冲突的情况
因此hashCode 相同时 还需要再调用 equals 进行次值的比较,
但是 hashCode将直接判定 Objects 不同 跳过 equals 这加快了冲突处理效率。
Object 类定义中对 hashCode( ) 和equals( ) 要求如下
( 1 )如果两个对象的 equals 的结果是相等的,则两个对象的 hashCode 的返回值也必须是相同的。
( 2)覆写 equals方法必须同时覆写 hashCode
通过上述源码我们可以清楚了解到HashMap保存数据的过程。
1.首先,判断key是否为null,若为null,则直接调用putForNullKey方法;
2.若不为空,则先计算key的hash值,然后根据hash值计算在table数组中的索引位置;
3.1 如果table数组在该位置处有元素,则遍历链表判断是否存在相同的key,若存在则覆盖原来key的value,否则将该元素节 点添加至该链表的头节点,(最先保存的元素放在链尾)。
3.2 若table在该处没有元素,则直接保存
参考文章:
http://www.codeceo.com/article/java-hashmap-learn.html
https://www.hollischuang.com/archives/3542