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

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”的元素。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值