HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
而Hashtable跟HashMap很像,唯一的区别是Hashtalbe中的方法是线程安全的,也就是同步的。
图片转载自:http://ifeve.com/hashmap-concurrenthashmap
HashMap由数组和链表组成:
数组存储区间是连续的,占用内存严重,故空间复杂的很大。
但数组的二分查找时间复杂度小,为O(1)。
数组的特点是:寻址容易,插入和删除困难;
链表存储区间是离散的,占用内存比较宽松,故空间复杂度很小。
但时间复杂度很大,达O(N)。
链表的特点是:寻址困难,插入和删除容易。
结合了数组和链表,结合了二者的特点:寻址容易,插入和删除。
数组的每个元素都是一个单链表的头节点,链表是用来解决冲突的,如果不同的key映射到了数组的同一位置处,就将其放入单链表中。
众所周知 HashMap 底层是基于 数组 + 链表 组成的,但是在 jdk1.7 和 1.8 中具体实现稍有不同。
HashMap的底层主要是基于数组和链表来实现的,它之所以有相当快的查询速度主要是因为它是通过计算散列码来决定存储的位置。HashMap中主要是通过key的HashCode来计算hash值的,只要HashCode相同,计算出来的hash值就一样。
即Key与value。
如果存储的对象对多了,就有可能不同的对象所算出来的hash值是相同的,这就出现了所谓的hash冲突。而HashMap底层是通过链表来解决hash冲突的。
java8里HashMap负载因子定义:
/**
* The load factor used when none specified in constructor.
* 在构造函数中未指定时使用的加载因子。
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
由于给定的 HashMap 的容量大小是固定的,比如默认初始化:
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
源码:
/* ---------------- Public operations -------------- */
/**
* Constructs an empty HashMap with the specified initial
* capacity and load factor.
*
* @param initialCapacity the initial capacity
* @param loadFactor the load factor
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
//initialCapacity 参数
//
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);
}
给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。
因此通常建议能提前预估 HashMap 的大小最好,尽量的减少扩容带来的性能损耗。
根据代码可以看到其实真正存放数据的是
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
/**
* 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 next;
final int hash;
……
}
Map.Entry 其实就是一个key-value对,它持有一个指向下一个元素的引用,这就构成了链表。
对于 HashMap 及其子类而言,它们采用 Hash 算法来决定集合中元素的存储位置。当系统开始初始化 HashMap 时,系统会创建一个长度为 capacity 的 Entry 数组,这个数组里可以存储元素的位置被称为“桶(bucket)”,每个 bucket 都有其指定索引,系统可以根据其索引快速访问该 bucket 里存储的元素。
无论何时,HashMap 的每个“桶”只存储一个元素(也就是一个 Entry),由于 Entry 对象可以包含一个引用变量(就是 Entry 构造器的的最后一个参数)用于指向下一个 Entry,因此可能出现的情况是:HashMap 的 bucket 中只有一个 Entry,但这个 Entry 指向另一个 Entry ——这就形成了一个 Entry 链。
public V put(K key, V value) {
//当key为null,调用putForNullKey方法,保存null与table第一个位置中,这是HashMap允许为null的原因
if (key == null)
return putForNullKey(value);
//计算key的hash值
int hash = hash(key.hashCode()); ------(1)
//计算key hash 值在 table 数组中的位置
int i = indexFor(hash, table.length); ------(2)
//从i出开始迭代 e,找到 key 保存的位置
for (Entry e = table[i]; e != null; e = e.next) {
Object k;
//判断该条链上是否存在相同的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; //返回旧值
}
}
//修改次数增加1
modCount++;
//将key、value添加至i位置处
addEntry(hash, key, value, i);
return null;
}
通过源码我们可以清晰看到HashMap保存数据的过程为:首先判断key是否为null,若为null,则直接调用putForNullKey方法,将value放置在数组第一个位置上。若不为空则根据key的hashCode重新计算hash值,然后根据hash值得到这个元素在table数组中的位置(即下标),如果table数组在该位置处已经存放有其他元素了,则通过比较是否存在相同的key,若存在则覆盖原来key的value,否则将该元素保存在链头(最先保存的元素放在链尾)。若table在该处没有元素,就直接将该元素放到此数组中的该位置上。这个过程看似比较简单,其实深有内幕。有如下几点:
1、先看迭代处。此处迭代原因就是为了防止存在相同的key值,若发现两个key值相同时,HashMap的处理方式是用新value替换旧value,这里并没有处理key,这就解释了HashMap中没有两个相同的key。另外,注意一点,对比Key是否相同,是先比HashCode是否相同,HashCode相同再判断equals是否为true,这样大大增加了HashMap的效率。
2、在看(1)、(2)处。这里是HashMap的精华所在。首先是hash方法,该方法为一个纯粹的数学计算,就是计算h的hash值。此算法加入了高位计算,防止低位不变,高位变化时,造成的hash冲突。