HashMap源码分析

 

HashMap在面试和工作中使用很多。

 

Hash,一般翻译做"散列",也有直接音译为"哈希"的,就是把任意长度的输入(又叫做预映射, pre-image),通过散列算法,变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来唯一的确定输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。 HASH函数(计算机算法领域) 概述 HashMap是Map接口下一个线程不安全的,基于哈希表的实现类,它解决冲突的方法是链表法,所以它的数据结构是数组+链表 JDK1.8之后的数据结构则变为数组 + 链表 + 红黑树

HashMap有两个参数影响其性能:初始容量和扩容因子。默认初始容量是16,加载因子是0.75。

特点:输入任意长度,得到固定长度的输出;

            不可逆,把输入的东西转换为哈希值,但是不能把哈希值换成原来输入的东西

作用:用来快速检索,安全性高,方便保存密码

 

initialCapacity 初始容量,通过tableSizeFor计算会返回一个比给定整数大且最接近的2的幂次方整数,并赋值给threshold
默认初始容量为16,必须为2的幂
loadFactor 扩容因子 默认为DEFAULT_LOAD_FACTOR 即0.75(在0.75属于时间和空间的一个平衡)
当容量一定是2^n时,h & (length - 1) == h % length
MIN_TREEIFY_CAPACITY:
是指当桶被转化为树形结构的时候,此时桶所拥有的最小容量

MAXIMUM_CAPACITY = 1 << 30;
这是定义容量的最大值。
 MAXIMUM_CAPACITY = 1 << 30;
 这是定义容量的最大值。
transient int modCount;定义修改次数,记录元素被修改的次数,
容易造成并发修改异常,举个例子,我正在修改一个元素,还没有修改完成,另一个人过来也在修改了这个元素,
我们都会进行modCount++但是另一个比我修改的快,我这边还在进行修改,他那边已经完成,这时候系统已经完成了+1操作,
我再次要进行加1 操作时 发现这个时候modCount的值已经比原来+1了 ,这时候程序直接就会报出并发修改异常

MOD运算(取模运算)

让每一个元素都可以均匀的分布在数组里,保证不会有很多元素挤在同一个格子里

对数字取余,数字是几就取余几次,就是几种情况

比如:对5取余那么结果就可能有0,1,2,3,4这5种情况

由于不同元素的哈希值不同 ,用哈希值对数组的长度取余后,就可以得到这个元素的索引

将这个索引添加进数组就能平均分到元素

(根据不同的元素,得到的哈希值有可能是负数,负数对数组长度取余是负数,索引无法是负数,计算机是基于二进制的,所以二进制的运算最快),

所以我们要将哈希值和数组长度两个值转换为二进制进行按位与&运算

0和&操作后都是0,就会有全是0的操作,为了避免这种情况,保证每次出来都有值,所以

用:2的幂次方减一。这种会有效的避免全是0的操作

举例:

10010//数组长度
 0111//第一个值
 00010//得到的索引是10

10010//数组长度
 0101//第二个值
 0000//得不到索引

进行mod运算后
 10001//18-1
  0101//第二个值
  0001//索引

所以mod运算索引的公式就是 hash&(数组长度-1)

 

哈希冲突

 哈希是通过对数据进行再压缩,提高效率的一种解决方法。但由于通过哈希函数产生的哈希值是有限的,而数据可能比较多,导致经过哈希函数处理后仍然有不同的数据对应相同的值。这时候就产生了哈希冲突。

解决哈希冲突的四种方法:开放地址法;链式地址法;再哈希法;建立公共溢出区。

HasMap的底层是通过数组+链表实现的。此处的“链表”就是HashMap为我们提供的解决哈希冲突的方法。也就是链式地址法。

oid addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {//当前元素大于扩容的阈值并且分给这个元素的位置不为空,就扩容
        resize(2 * table.length);//将原来的数组长度乘2
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);//重新计算数组长度算出索引
    }

    createEntry(hash, key, value, bucketIndex);
}

利用此方法进行扩容

构造方法:

1.HashMap()

使用默认初始容量16与默认负载因子0.75构造一个空的HashMap。

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;//初始值
static final float DEFAULT_LOAD_FACTOR = 0.75f;//扩容因子
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; 
}

2.HashMap(int intitialCapacity)

public HashMap(int initialCapacity) {
        //此处通过把第二个参数负载因子使用默认值0.75f,然后调用有两个参数的构造方法
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
  

3.HashMap(int initialCapacity, float loadFactor)

public HashMap(int initialCapacity, float loadFactor) {
       if (initialCapacity < 0)
           throw new IllegalArgumentException("Illegal initial capacity: " +
                                              initialCapacity);
        //如果指定的容量不能小于0,否则会报错
       if (initialCapacity > MAXIMUM_CAPACITY)
           initialCapacity = MAXIMUM_CAPACITY;
        //如果指定的容量大于最大容量的话,将容量定为最大容量   
       if (loadFactor <= 0 || Float.isNaN(loadFactor))
           throw new IllegalArgumentException("Illegal load factor: " +
                                              loadFactor);
           //如果负载因子小于等于0或者不是数字,则抛出异常
       this.loadFactor = loadFactor;
       this.threshold = tableSizeFor(initialCapacity);
   }

put方法:

public V put(K key, V value) {
       if (table == EMPTY_TABLE) {     //判断是否内容为空,这里的table是一个键值对数组,默认是空的。
           inflateTable(threshold);            //如果内容为空,则首先初始化
       }
       if (key == null)                            //如果传入的key是null,则调用下面的方法
           return putForNullKey(value);
       int hash = hash(key);                //如果key是存在的,那么计算key的hash
       int i = indexFor(hash, table.length);//计算完哈希后,根据哈希和数组长度去计算对应的索引,也就是这个key应该在数组里哪里存储
       for (Entry<K,V> e = table[i]; e != null; e = e.next) {//数组中的每个元素是一个链表,已经计算除了索引,所以遍历该索引位置上的链表,目的是为了检查传进来的key是否已经存在在表里
           Object k;
           if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {//如果发现链表上的元素和传进来的key各方面都一样(hash,地址,内容)那么就说明表里已经有了,那么就保持key不变,更新value,并返回旧的value
               V oldValue = e.value;
               e.value = value;
               e.recordAccess(this);
               return oldValue;
           }
       }
  
       modCount++;  //被修改次数加一,这个值主要是为了线程安全考虑,和本方法的业务无关
       addEntry(hash, key, value, i); //如果是一个新key,则添加元素。
       return null;
   }

get方法:

int hash = (key == null) ? 0 : hash(key);//判断key是否为null
for (Entry<K,V> e = table[indexFor(hash, table.length)];
//计算完哈希后,根据哈希和数组长度去计算对应的索引,也就是这个key应该在数组里哪里存储
e != null;e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; }

 初始化:

private void inflateTable(int toSize) {
        // Find a power of 2 >= toSize
        int capacity = roundUpToPowerOf2(toSize);//初始化时都会取2的幂次方

        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);//计算扩容阈值
        table = new Entry[capacity];//初始化
        initHashSeedAsNeeded(capacity);
    }

 

小结

(1) 扩容是一个特别耗性能的操作,所以当程序员在使用HashMap的时候,估算map的大小,初始化的时候给一个大致的数值,避免map进行频繁的扩容。

(2) 负载因子是可以修改的,但是通常不会去修改

(3) HashMap是线程不安全的,不要在并发的环境中同时操作HashMap,建议使用ConcurrentHashMap。

(4) JDK1.8引入红黑树大程度优化了HashMap的性能。

 

posted @ 2021-08-08 18:27  五色石  阅读(28)  评论(0)    收藏  举报