您的位置:首页 > 其它

HashMap的实现原理及部分源码分析

2019-05-19 10:24 387 查看
4000

HashMap的实现原理及部分源码分析

​  Java8拥有增强的Map集合,Map是用来保存具有映射关系的数据。Map接口提供了大量的实现类,典型的实现有 HashMap和Hashtable等。Hashtable从它的类名看起来就很古老,因为它的命名都没有遵守Java的命名规范:每个单词首字母都应该大写。 HashMap(哈希表又叫散列表),在数据结构我们已经接触过,它的应用非常广泛,比如现在的缓存技术…

一、哈希表的概念

​ 哈希表利用了数组的根据下标一次定位查询某个元素的特性,所以哈希表的主干是数组。

​ 在不考虑哈希冲突时,对于在哈希表中进行添加,删除,查找等操作,性能高至仅需要一次定位即可完成,时间复杂度为O(1).

哈希函数: 存储位置 = F(key)

二、HashMap的特点

1.HashMap基于哈希表的Map而实现。

2.HashMap是线程不安全的。

3.HashMap的主干是一个初始值为空,长度为2的次幂的Entry数组。Entry 是HashMap的基本组成单元,每一个Entry包含一个key-value键值对。

4.HashMap保存数据的时候通过计算key的hash值来去决定存储的位置。

三、HashMap实现原理

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable

可见HashMap继承自父类AbstractMap,实现了接口Map。还有Cloneable,Serializable接口

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
// 序列号
private static final long serialVersionUID = 362498820763181265L;
// 默认的初始容量为16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认填充因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 当桶(bucket)上的结点数大于这个值时会转成红黑树
static final int TREEIFY_THRESHOLD = 8;
// 当桶(bucket)上的结点数小于这个值时树转链表
static final int UNTREEIFY_THRESHOLD = 6;
// 桶中结构转化为红黑树对应的table的最小大小
static final int MIN_TREEIFY_CAPACITY = 64;
// 存储元素的数组,总是2的幂次倍
transient Node<k,v>[] table;
// 存放具体元素的集
transient Set<map.entry<k,v>> entrySet;
// 存放元素的个数,注意这个不等于数组的长度。
transient int size;
// 每次扩容和更改map结构的计数器
transient int modCount;
// 临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容
int threshold;
// 填充因子
final float loadFactor;
}

Map接口定义了通用的一些操作;Cloneable接口代表可以进行拷贝; Serializable 接口代表HashMap可被序列化。

源码:

public HashMap(int initialCapacity,float loadFactor){
//初始容量不能小于0,抛错
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;
threshold = initialCapacity;
init();
}

我们知道,在常规构造器中,没有为数组table分配内存空间(有一个入参为指定Map的构造器例外),只有在执行put操作的时候才会真正构建table数组

put操作:

public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);//对key的hashcode进一步计算,确保散列均匀
int i = indexFor(hash, table.length);//得到table中的实际位置
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
//如果该对应数据已存在,执行覆盖操作。用新value替换旧value,并返回旧value
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;//保证并发访问时,若HashMap内部结构发生变化,快速响应失败
addEntry(hash, key, value, i);//新增一个entry
return null;
}

在put方法中,直接判断table是否为null,如果table数组为空数组{},进行数组填充(为table分配实际内存空间),入参为threshold,此时threshold为initialCapacity 默认是1<<4(24=16)。再判断key是否为null,如果key为null,存储位置为table[0]或table[0]的冲突链上。

HashMap的hash表中的属性解读:

<1> capacity(容量):hash 表中桶的数量。

<2> initialCapacity(初始化容量):创建hash表时桶的数量。HashMap允许在构造器中指定初始化容量。

<3> size(尺寸):当前hash表中记载的数量。

<4> load factor(负载因子):负载因子等于“size/capacity”。负载因子为0,表示hash为空;为0.5,表示hash为半空;轻负载的hash表具有冲突少,适宜插入与查询的特点。

transient Entry<key,value>[] table = (Entry<k,y>[]) EMPTY_TABLE;

Entry其实是HashMap中的一个静态内部类,里面最重要的属性有key,value,next三个属性值,而这里的key和value是我们put时的key和value。Entry是一个单链表,而next属性的值是Entry,表示的是当前节点的下一个节点是哪一个Entry。

static class Entry<k,y> implements Map.Entry<k,y>{
fianl K key;
V value;
Entry<k,y> next;
int hash;

/**
*Creates new entry.
*/

Entry(int h,K k,V v,Entry<k,y> n){
vlaue = v;
key = k;
hash = h;
}

HashMap由数组和链表组成的,主体是数组,链表主要是为了解决哈希冲突的。在当前Entry的next指向Null时,即仅需一次寻址就可以完成(时间复杂度为O(1)),此时定位到的数组位置不含链表。若定位到的数组含有链表,则 添加操作的时间复杂度为O(n);查找操作则是根据key对象的equals方法来逐一比对查找。

Java8改进了HashMap的实现,使得HashMap在存在Key冲突时,依旧能有较好的性能。

HashMap和Hashtable的两点区别

<1> Hashtable 是一个自JDK1.0出现的古典的Map实现类,它是线程安全的。而HashMap是线程不安全的,所以HashMap性能较高一些。在有多个线程访问同一个Map对象时,使用Hashtable实现类会更好。

<2> HashMap可以使用null作为key或value,而Hashtable不允许使用null作为key或value,将引起NullPointerException异常。

public class NullInHashMap
{
public static void main(Sting[] args){
HashMap hm = new HashMap();
hm.put(null,null);
hm.put(null,null);    //并不能再次放入
hm.put("a",null);     //可以放入
System.out.println(hm);
}
}

分析上述的代码:程序试图将三个Key-value对放入HashMap。因为Map中只能有一个Key-value对的key为null值,而可以有多个value为null。

输出结果:

{null = null, a = null}

HashMap和Hashtable判断两个value相等的标准:两个对象通过equals()方法比较返回true即可证明相等。

创建HashMap的方法

//参数:初始容量,负载因子
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 IllgalArgumentException("Illrcal load factor :"+loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);}
//指定初始容量
public HashMap(int initialCapacity){
this(initialCapacity,DEFAULT_LOAD_FACTOR);
}
//无参构造
public HashMap(){
this.loadFactor = DEFAULT_LOAD_FACTOR);
}

//添加指定的Map
public HashMap(Map< ? extend K,? extends V > m){
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m,false);
}
静态工具方法tableSizeFor()
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* Returns a power of two size for the given target capacity.
作用:找出大于等于initialCapacity最小的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;
}

这个方法在哪里被调用?下面:

this.threshold = tableSizeFor(initialCapacity);

在实例化HashMap实例时,如果给定了initialCapacity,由于HashMap的capacity都是2的幂,因此这个方法用于找到大于等于initialCapacity的最小的2的幂(initialCapacity如果就是2的幂,则返回的还是这个数)。

putMapEntries

//将m的所有元素存入本HashMap实例中
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
// 判断table是否已经初始化
if (table == null) { // pre-size
// 未初始化,s为m的实际元素个数
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
// 判断是否需要扩容
if (t > threshold)
threshold = tableSizeFor(t);
}
// 已初始化,并且m元素个数大于阈值,进行扩容处理
else if (s > threshold)
resize();
// 将m中的所有元素添加至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);
}
}
}

删除指定结点:

public V remove(Object key) {
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.value);
}

// 删除“键为key”的元素
final Entry<K,V> removeEntryForKey(Object key) {
// 获取哈希值。若key为null,则哈希值为0;否则调用hash()进行计算
int hash = (key == null) ? 0 : hash(key.hashCode());
int i = indexFor(hash, table.length);
Entry<K,V> prev = table[i];
Entry<K,V> e = prev;

// 删除链表中“键为key”的元素
// 本质是“删除单向链表中的节点”
while (e != null) {
Entry<K,V> next = e.next;
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
modCount++;
size--;
if (prev == e)
table[i] = next;
else
prev.next = next;
e.recordRemoval(this);
return e;
}
prev = e;
e = next;
}

return e;
}

remove()的作用就是删除“键为key”的元素。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: