1.HashMap之概述
HashMap是基于哈希表的Map接口的非同步实现。此实现提供有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
首先抛一个问题:HashMap和HashTable有什么区别,最普通的是:
1.HashMap是非线程安全的,HashTable是线程安全的
2.HashMap的键和值都运行有null值存在,而HashTable则不行
3.因为线程安全的问题,HashMap效率比HashTable的要高
总结一下:
关注点 | 结论 |
---|---|
HashMap是否允许空 | Key和Vaule都运行为空 |
HashMap是否允许重复数据 | Key重复会覆盖,Value允许重复 |
HashMap是否有序 | 无序,特别说明这个无序指的是遍历HashMap的时候基本不可能是put的顺序 |
HashMap是否线程安全 | 非线程安全 |
2.HashMap的数据结构
HashMap的底层主要是基于数组和链表来实现的,它之所以有相对快的查询速度主要是因为它是通过计算散列码来决定存储位置。HashMap中主要是通过key的hashCode来计算hash值得,只要hashCode相同,计算出来的hash值久一样。如果存储的对象多了,就有可能不同的对象所算出来的hash值是相同的,这就出现了所谓的hash冲突,HashMap底层是通过链表来解决hash冲突的。
3.HashMap的构造函数
HashMap提供了三个构造函数:
HashMap():构造一个具有默认初始容量(16)和默认加载因子(0.75)的空HashMap
HashMap(int initalCapacity):构造一个带指定初始容量和默认加载因子(0.75)的空HashMap
HashMap(int initialCapacity,float loadFactor):构造一个带指定初始容量和加载因子的空HashMap
初始容量,加载因子 这两个参数是影响HashMap性能的重要参数,其中容量表示哈希表中桶的数量,初始容量是创建哈希表时的容量,加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度,衡量的是一个散列表的空间使用程度,负载因子越大表示散列表的装填程度越高,反之越小
若:加载因子越大,填满的元素越多,好处是,空间利用率高,但:冲突的机会加大了,链表长度会越来越长,查找效率低。
反之,加载因子越小,填满的元素越少,好处是:冲突的机会减小了,但:空间浪费多了,表中的数据过于稀疏
当哈希表中条目超出了当前容量*加载因子(其实就是HashMap的实际容量)时,则对该哈希表进行rehash操作,将哈希表扩充两倍的桶数。
HashMap构造函数的源码:
public HashMap(itn initalCapacity,float loadFactor){
//初始容量不能小于0
if(initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity:"+ initialCapacity);
//初始容量不能>最大容量值,HashMap的最大容量值为2^30
if(initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY
//负载因子不能<0
if(loadFactor <=0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illafal load factor:"+ loadFactor)
//计算出大于initialCapacity的最小的2的n次方值。
int capacity = 1;
while(capacity < initialCapacity)
capacity << =1;
this.loadFactor = loadFactor;
//设置HasMap的容量极限,当HashMap的容量达到该极限时就会进行扩容操作
threshold = (int) (capacity * loadFactor);
//初始化table数组
table = new Entry[capacity];
init();
}
每次建一个HashMap,都会初始化一个table数组。table数组的元素为Entry节点,Entry为HashMap的内部类,包含了键Key、值value、下一个节点next,以及hash值,正是由于Entry才构成table数组的项为链表
存储实现:
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());
//计算key hash 值在table数组中的位置
int i = indexFor(hash,table.length);
//从i出开始迭代e,找到key保存的位置
for(Entry<K,V> e = table[i];e != null;e = e.next){
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;//返回旧值
}
}
//修改次数增加1
modCount ++;
//将key、value添加至i位置处
addEntry(hash,key,value,i)
return null;
}
}
通过源码可以清晰看到HashMap保存数据的过程为:首先判断key是否为null,若为null,则直接调用putForNullKey方法。若不为空则先计算key的hash值,然后根据hash值搜索在table数组中的索引位置,如果table数组在该位置处有元素,则通过比较是否存在相同的key,若存在则覆盖原来key的value,否则将该元素保存在链头(最先保存的元素放在链尾)
读取实现:
public V get(Object key) {
// 若为null,调用getForNullKey方法返回相对应的value
if (key == null)
return getForNullKey();
// 根据该 key 的 hashCode 值计算它的 hash 码
int hash = hash(key.hashCode());
// 取出 table 数组中指定索引处的值
for (Entry<K, V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
Object k;
//若搜索的key与查找的key相同,则返回相对应的value
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;
}
// 若为null,调用getForNullKey方法返回相对应的value
if (key == null)
return getForNullKey();
// 根据该 key 的 hashCode 值计算它的 hash 码
int hash = hash(key.hashCode());
// 取出 table 数组中指定索引处的值
for (Entry<K, V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
Object k;
//若搜索的key与查找的key相同,则返回相对应的value
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;
}
根据key快速的取到value除了和HashMap的数据结构密不可分外,还和Entry有莫大的关系,在前面就提到过,HashMap在存储过程中并没有将key,value分开来存储,而是当做一个整体key-value来处理的,这个整体就是Entry对象