java HashMap源码简析(1.8)

这篇博客里整理了HashMap源码中比较重要,需要掌握和探究的点,也是一些在面试中常常遇到的问题~

包括以下问题——

  1. HashMap的key、value;
  2. HashMap是线程不安全的;
  3. HashMap数据结构;
  4. 初始化容量为什么为16?
  5. 加载因子;
  6. 为什么加载因子的默认值为0.75?
  7. HashMap 中关于红黑树的三个关键参数;
  8. 为什么哈希表的容量一定要是2的整数次幂?
  9. HashMap的扩容(resize)机制;
  10. HashMap存取时,如何计算当前key应该对应Entry[]数组哪个元素,即计算数组下标的代码;
  11. HashMap和HashTable中对于hash的实现的总结;
  12. 扰动运算;
  13. HashMap共有4个构造函数;
  14. put()方法;
  15. get()方法;
  16. fail-fast机制;

 

1、HashMap的key、value都可以为null,映射不是有序的,并且不保证该序列恒久不变。

 

2、HashMap是线程不安全的——

HashMap不是同步的,通过Collections类的静态方法synchronizedMap——

public static Map synchronizedMap(Map m)

返回由指定映射支持的同步(线程安全的)映射。

 

3、HashMap数据结构

HashMap底层是基于数组和链表来实现的。

查询速度快的原因:通过计算散列码来决定存储位置。

计算hash值:通过key的HashCode来计算。(只要HashCode相同,hash值就相同)

hash冲突:不同对象所算出的hash值可能是相同的。

HashMap底层如何解决hash冲突:链表。

图解:

java HashMap源码简析(1.8)_第1张图片

 

4、初始化容量为什么为16?

首先,假设HashMap的长度是10

hashcode : 101110001110101110 1001

length - 1 : 1001

index : 1001

 

再换一个hashcode 101110001110101110 1111 试试:

hashcode : 101110001110101110 1111

length - 1 : 1001

index : 1001

 

从结果可以看出,虽然hashcode变化了,但是运算的结果都是一样,为1001,也就是说,当HashMap长度为10的时候,有些index结果的出现几率会更大,而有些index结果永远不会出现(比如0111),这样就不符合hash均匀分布的原则;

 

反观长度16或者其他2的幂,length - 1的值是所有二进制位全为1,这种情况下,index的结果等同于hashcode后几位的值,只要输入的hashcode本身分布均匀,hash算法的结果就是均匀的

 

所以,HashMap的默认长度为16,是为了降低hash碰撞的几率

 

 

5、加载因子:

static final float DEFAULT_LOAD_FACTOR = 0.75f; 

为什么是0.75?

若:加载因子越大,填满的元素越多,好处是空间利用率高了,但冲突的机会加大了.链表长度会越来越长,查找效率降低。

反之,加载因子越小,填满的元素越少,好处是冲突的机会减小了,但空间浪费多了.表中的数据将过于稀疏(很多空间还没用,就开始扩容了)

 

冲突的机会越大,则查找的成本越高

因此——必须在"冲突的机会"与]空间利用率"之间寻找一种平衡。

如果机器内存足够,并且想要提高查询速度的话可以将加载因子设置小一点;相反如果机器内存紧张,并且对查询速度没有什么要求的话可以将加载因子设置大一点。

一般我们都不用去设置它,用默认值0.75即可。

 

 

为什么加载因子的默认值为0.75?

泊松分布——在理想情况下,使用随机哈希码,节点出现的频率在hash桶中遵循泊松分布。

用0.75作为加载因子的值的时候,桶内元素的个数和概率对照表:

  • 0: 0.60653066
  • 1: 0.30326533
  • 2: 0.07581633
  • 3: 0.01263606
  • 4: 0.00157952
  • 5: 0.00015795
  • 6: 0.00001316
  • 7: 0.00000094
  • 8: 0.00000006

 

上面的对照表显示:当加载因子为0.75时,桶中的元素个数达到8个的概率已经很小了;

即表明——0.75作为hashmap的加载因子的时候,每个碰撞位置的链表长度几乎都在8个

以下了。

 

 

6、HashMap 中关于红黑树的三个关键参数

HashMap 中有三个关于红黑树的重要参数:

1、static final int TREEIFY_THRESHOLD = 8;

一个桶的树化阈值:当桶中元素个数超过8的时候,需要使用红黑树节点替换链表节点

 

2、static final int UNTREEIFY_THRESHOLD = 6;一个树的链表还原阈值:当扩容时,桶中元素个数小于等于6的时候,就会把树形的桶元素 还原(切分)为链表结构3、static final int MIN_TREEIFY_CAPACITY = 64;哈希表的最小树形化容量:当哈希表中的容量大于这个值时,表中的桶才能进行树形化

 

注:桶内元素太多,且容量小于64时,会扩容,而不是树形化

 

 

7、为什么哈希表的容量一定要是2的整数次幂?

首先,h&(length-1)就相当于对length取模,这样便保证了散列的均匀,同时也提升了效率;

其次,length为2的整数次幂的话,为偶数,这样length-1为奇数,奇数的最后一位是1,这样便保证了h&(length-1)的最后一位可能为0,也可能为1(这取决于h的值),即与后的结果可能为偶数,也可能为奇数,这样便可以保证散列的均匀性。

(如果length为奇数的话,很明显length-1为偶数,它的最后一位是0,这样h&(length-1)的最后一位肯定为0,即只能为偶数,这样任何hash值都只会被散列到数组的偶数下标位置上,这便浪费了近一半的空间)

 

所以,length取2的整数次幂,是为了使不同hash值发生碰撞的概率较小,这样就能使元素在哈希表中均匀地散列。

 

 

8、HashMap的扩容(resize)机制:

为什么要扩容(重新计算容量):在向HashMap对象里不停的添加元素,而HashMap对象内部的数组无法装载更多的元素时———对象就需要扩大数组的长度,以便能装入更多的元素。当然Java里的数组是无法自动扩容的,方法是使用一个新的数组代替已有的容量小的数组。

代码解析:

//传入新的容量

void resize(int newCapacity) {

//引用扩容前的Entry数组 Entry[] oldTable = table; int oldCapacity = oldTable.length;

//扩容前的数组大小如果已经达到最大(2^30)了 if (oldCapacity == MAXIMUM_CAPACITY) {

//修改阈值为int的最大值(2^31-1),这样以后就不会扩容了 threshold = Integer.MAX_VALUE; return; }

//初始化一个新的Entry数组 Entry[] newTable = new Entry[newCapacity];

//将数据转移到新的Entry数组里 transfer(newTable);

//HashMap的table属性引用新的Entry数组 table = newTable;

//修改阈值 threshold = (int)(newCapacity * loadFactor);//修改阈值 }

 

transfer()方法:将原有Entry数组的元素拷贝到新的Entry数组里。

注意:要重新计算每个元素在数组中的位置(hash值)

遍历顺序:先遍历到哈希表的第一个元素,然后依次遍历以这个元

素为头结点的单链表中的每个元素。接着遍历哈希表中第二个元素……

重复该过程。

 

9、HashMap存取时,要计算当前key应该对应Entry[]数组哪个元素,即计算

数组下标,代码如下:

    static int indexFor(int h, int length) {

        return h & (length-1);

    }

 

我们一般对哈希表的散列很自然地会想到用hash值对length取模(即除法散列法)Hashtable中也是这样实现的这种方法基本能保证元素在哈希表中散列的比较均匀,但取模会用到除法运算,效率很低HashMap中则通过h&(length-1)的方法来代替取模,同样实现了均匀的散列,但效率要高很多,这也是HashMap对Hashtable的一个改进。

 

效率更高的原因位运算直接对内存数据进行操作,不需要转化为十进制,处理速度更快。

 

 

10、HashMap和HashTable中对于hash的实现的总结——

HashMap默认的初始化大小为16,之后每次扩充为原来的2倍

HashTable默认的初始大小为11,之后每次扩充为原来的2n+1倍

 

原因:

HashTable选择取模运算:当哈希表的大小为素数时,简单的取模哈希的结果会更加

均匀,hash结果越分散效果越好。

HashMap选择取模运算:在取模计算时,如果模数是2的幂,那么我们可以直接使用

位运算来得到结果,运算效率要大大高于做取模。

但是,HashMap为了提高效率使用位运算代替哈希——引入了哈希分布不均匀的问题,所以HashMap为解决这问题,又对hash算法做了一些改进,进行了扰动计算。

 

 

11、扰动运算

h ^= k.hashCode();

h ^= (h >>> 20) ^ (h >>> 12);return h ^ (h >>> 7) ^ (h >>> 4);

对key的hashCode进行扰动计算,防止不同hashCode的高位不同但低位相同导致的hash冲突。

简单来说,就是为了把高位的特征和低位的特征组合起来,降低哈希冲突的概率,也就是说,尽量做到任何一位的变化都能对最终得到的结果产生影响。

 

 

12、HashMap共有4个构造函数:

①HashMap()

构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空 HashMap。

②HashMap(int initialCapacity)

构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap。

 

③HashMap(int initialCapacity, float loadFactor)

构造一个带指定初始容量和加载因子的空HashMap。

 

④HashMap(Map m)

构造一个映射关系与指定 Map 相同的新 HashMap。

 

 

9、put方法 (将“key-value”添加到HashMap中)

    public V put(K key, V value) {
// 若“key为null”,则将该键值对添加到哈希表的下标0的位置中。
        if (key == null)
            return putForNullKey(value);
//若“key不为null”,计算该key的哈希值,然后将其添加到该哈希值对应的链表中。
        int hash = hash(key.hashCode());
        int i = indexFor(hash, table.length);
        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;
            }
        }
// 若“该key”对应的键值对不存在,则将“key-value”添加到哈希表中
        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

 

10、get方法 (获取key对应的value)

    public V get(Object key) {
        if (key == null)
            return getForNullKey();
// 获取key的hash值
        int hash = hash(key.hashCode());
// 在“该hash值对应的链表”上查找“键值等于key”的元素
        for (Entry e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
                return e.value;
        }
        return null;
    }

 

11、fail-fast

fail-fast:java.util.HashMap不是线程安全的,因此如果在使用迭代器的过程中有其他线程修改了map,那么将抛出ConcurrentModificationException,这就是所谓fail-fast策略。

 

实现及原理:这一策略在源码中的实现是通过modCount域,modCount顾名思义就是修改次数,对HashMap内容的修改都将增加这个值,那么在迭代器初始化过程中会将这个值赋给迭代器的expectedModCount。

 

 由所有HashMap类的“collection 视图方法”所返回的迭代器都是快速失败的:在迭代器创建之后,如果从结构上对映射进行修改,除非通过迭代器本身的 remove 方法,其他任何时间任何方式的修改,迭代器都将抛出 ConcurrentModificationException。

 

 

 

 

 

 

 

 

你可能感兴趣的:(java,面经)