1. ArrayList

ArrayList的扩容规则

  1. 无参的ArrayList的容量为0,底层是一个空数组 ```java List list1 = new ArrayList<>();

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

public ArrayList() { // 初始化一个空数组 this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }

  1. 2. 有参的ArrayList会使用指定容量的数组
  2. ```java
  3. List<Integer> list1 = new ArrayList<>(10);
  4. public ArrayList(int initialCapacity) {
  5. if (initialCapacity > 0) {
  6. // 初始化数组的长度
  7. this.elementData = new Object[initialCapacity];
  8. } else if (initialCapacity == 0) {
  9. this.elementData = EMPTY_ELEMENTDATA;
  10. } else {
  11. throw new IllegalArgumentException("Illegal Capacity: "+
  12. initialCapacity);
  13. }
  14. }
  1. add(Object o)首次扩容为10,再次扩容为上次的1.5倍

ArrayList的add源码阅读

  1. @Test
  2. public void test01() {
  3. ArrayList<Integer> list1 = new ArrayList<>(10);
  4. // 第一次扩容,容量为10, volume = 10;
  5. list1.add(0);
  6. list1.add(1);
  7. list1.add(2);
  8. list1.add(3);
  9. list1.add(4);
  10. list1.add(5);
  11. list1.add(6);
  12. list1.add(7);
  13. list1.add(8);
  14. list1.add(9);
  15. // 第二次扩容,容量为上次的1.5倍,volume += volume >> 1; volume = 15;
  16. list1.add(10);
  17. list1.add(11);
  18. list1.add(12);
  19. list1.add(13);
  20. list1.add(14);
  21. // 第三次扩容,容量为上次的1.5倍 volume += volume >> 1; volume = 22;
  22. list1.add(15);
  23. }
  1. public boolean add(E e) {
  2. // 扩容,初始size = 0
  3. ensureCapacityInternal(size + 1); // Increments modCount!!
  4. elementData[size++] = e;
  5. return true;
  6. }
  7. // 第一次扩容minCapacity = 1
  8. private void ensureCapacityInternal(int minCapacity) {
  9. // 先计算列表容量
  10. ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
  11. }
  12. private static final int DEFAULT_CAPACITY = 10;
  13. // 计算列表容量:这个函数只是当第一次添加元素时,将数组长度初始化为1-
  14. private static int calculateCapacity(Object[] elementData, int minCapacity) {
  15. if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
  16. // 第一次添加,为空列表,返回默认容量DEFAULT_CAPACITY = 10
  17. return Math.max(DEFAULT_CAPACITY, minCapacity);
  18. }
  19. return minCapacity;
  20. }
  21. // 验证扩容的容量是否满足添加元素后的容量
  22. private void ensureExplicitCapacity(int minCapacity) {
  23. modCount++; // 记录列表被修改的次数
  24. // 如果容量不够,扩容
  25. if (minCapacity - elementData.length > 0)
  26. grow(minCapacity);
  27. }
  1. private void grow(int minCapacity) {
  2. int oldCapacity = elementData.length; // 原始数组容量
  3. int newCapacity = oldCapacity + (oldCapacity >> 1); // 扩容为原始数组容量的1.5倍
  4. if (newCapacity - minCapacity < 0)
  5. newCapacity = minCapacity;
  6. if (newCapacity - MAX_ARRAY_SIZE > 0)
  7. newCapacity = hugeCapacity(minCapacity);
  8. elementData = Arrays.copyOf(elementData, newCapacity); // 将原来的数组拷贝到一个新的数组中,容量为扩容后的容量
  9. }

可视化过程
List<Integer> list1 = new ArrayList<>();
无参时,第一次添加扩容为10
image.png
List<Integer> list1 = new ArrayList<>(0);
有参时,不会初始化扩容为10,容量就是0,每次扩容为之前的1.5倍
image.png

  1. addAll(Collection c)的扩容规则
  • 无元素时:扩容为Math.max(10, 添加后元素的个数);
  • 有元素时: 扩容为Math.max(原容量1.5倍,添加后元素的个数);

ArrayList与LinkedList的区别

  1. ArrayList基于数组,需要连续的内存;LinkedList基于双向链表,不需要连续的内存。
  2. ArrayList的随机访问速度快,只需要根据下表就可以实现O(1)事件的访问速度;LinkedList则需要沿着链表遍历,所以随机访问速度慢,空间复杂度为O(n)。
  3. ArrayList的尾部插入删除性能高,但头部插入及其他部分插入由于需要大量的移动数据,性能较低。LinkedList的头尾插入删除都具有较高的性能,但是中间部分的插入删除,由于需要遍历链表定位插入删除元素的位置,导致性能也很低。
  4. 同时ArrayList由于底层使用数组实现,可以利用CPU缓存的局部性原理,提高数据数据的读取性能,而LinkedList由于存储不连续,则不能实现类似的高性能数据读入。
  5. LinkedList由于内部结点存储了链表的指针等信息,导致其占用内存比ArrayList大
    什么是CPU的局部性原理
    CPU局部性原理就是CPU在访问内存时,所访问的存储单元总是趋近于一块较小的内存区域,因此在将内存中的数据读入缓存中时,通常会将要访问存储单元附近连续的一小段内存数据存入缓存中,这样就可以提高数据读取的效率
    image.png

    Fail-Fast和Fail-Safe是两种容错机制

  • Fail-Fast: 快速故障;遇到故障,立即停止;例如在ArrayList中遍历的同时不能修改,一旦修改尽快失败
  • Fail-Safe: 故障安全;遇到故障,可以忽略;如CopyOnWriteArrayList 通过读写分离实现了在遍历的同时修改列表

    2. HashMap

    HashMap的数据结构

    在jdk1.8在之前HashMap使用了数组+链表,HashMap通过key的hashcode获取hash值,然后对数组的长度取余,获取元素存放的桶下表(也就是存放的位置)。如果当前位置不存在元素则直接放入;如果存在元素的话,遍历链表,比较链表中的元素与当前元素的hash值是否相等,如果不相等,就插入到链表后面,如果相等,再比较两者的key是否相同,相同直接覆盖,不相同就将原素插入到链表后面。
    Java集合 - 图4
    拉链法:将数组与链表结合,数组中的每一格存储的都是链表,当遇到哈希冲突时,直接将冲突的值插入链表后面即可
    在jdk1.8中HashMap使用了数组+红黑树,jdk1.8以后HashMap在解决哈希冲突上做了较大的改变,当链表的长度大于8,并且数组的程度大于64时,链表将转化为红黑树
    Java集合 - 图5
    红黑树

HashMap中链表过长的解决方案

  1. 数组扩容

HashMap中第一次插入数据时,数组的默认长度为16,触发扩容有两种情况:

  • 当插入的数据的长度大于数组原始长度的0.75倍,将会扩容一倍

image.png
image.png

  • 当链表的长度大于8,并且数组长度小于64时将会触发扩容

image.png
image.png

  1. // 数组的默认长度
  2. static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 1 << 4 = 16
  3. // 负载因子
  4. static final float DEFAULT_LOAD_FACTOR = 0.75f;
  5. // 扩容的阈值,负载因子*原始容量
  6. newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
  1. 转为红黑树

HashMap链表转为红黑树的条件:
链表的长度大于8,并且数组的长度大于64
image.png
image.png

树化的意义?

避免链表超长,导致查询性能下降

为什么不直接使用红黑树?

在链表长度较短时,直接使用链表的性能要更高,因为红黑树的查询、插入更复杂;而且红黑树中结点占用的内存也比链表中的结点占用的内存高;

链表的长度为什么要选择8?

首先,链表过长过短都不好。链表过长的话,会使得查询性能下降;链表过短的话,又会频繁的树化也会导致性能下降,所以应尽可能降低树化的概率;如果哈希值速购随机,链表长度超过8的概率将会非常小,这样使得正常情况下不会频繁的树化。

红黑树退化为链表的条件?

  1. 在扩容拆分树时,树的元素个数 <= 6会退化为链表

image.png
image.png

  1. remove节点时,如果root,root.left,root.right,root.left.left有一个为null,也会退化为链表

image.png
image.png

HashMap的索引如何计算?

首先计算key的hashcode,然后经过hash()方法进行二次hash,最后将计算出的哈希值与数组的长度取模计算HashMap的索引(取模使用的位操作(n - 1) & hash)

  1. // 索引计算
  2. p = tab[i = (n - 1) & hash]
  3. static final int hash(Object key) {
  4. int h;
  5. return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  6. }

为什么要二次hash?

二次哈希是为了使得哈希分布更加均匀,避免超长链表的出现

数组容量为什么是2的n次幂?

  1. 在计算索引,使用哈希码对数组长度取模时,HashMap使用了(n - 1) & hash的位运算提高计算效率,因此使用2的n次幂,可以使得这样的位运算与取模运算等价。
  2. 在扩容时,采用了2的n次幂,也可以使得元素的移动效率效率更高。如果hash & oldCap == 0 的话,表明hash值小于原来的容量值,扩容后,索引值不变直接保留在原来的位置即可,如果hash & oldCap != 0的话,hash值大于原来的容量,元素就需要移动到新的位置。新位置 = 旧位置 + odlCap

    HashMap的负载因子为什么要设成0.75呢?

  • 负载因子设为0.75是为了在内存占用和查询时间上取得较好的权衡
  • 如果负载因子比较大,哈希冲突较多,链表会比较长,影响查询性能
  • 如果负载因子比较小,哈希冲突变少了,但是扩容更加频繁,内存占用更多,空间利用率吧也比较小

    HashMap在jdk1.7和1.8中的区别?

  • jdk1.7的底层数据结构使用的是数组+链表,jdk1.8中使用的是数组+链表/红黑树

  • jdk1.7中链表插入结点是头插法,jdk1.8中是尾插法
  • jdk1.7是在大于等于阈值并且没有空位时才扩容,jdk1.8是大于阈值就扩容
  • jdk1.8扩容计算索引使用了位运算优化计算过程

    jdk1.8扩容源码分析

    ```java @Test public void test03() {

    HashMap map = new HashMap<>();

    // 首先加入11个元素,索引为0-11 for(int i = 1; i <= 11; i++) {

    1. map.put(i,i);

    } // 插入第12个元素,索引为7 map.put(151, 151);

    // 插入第13个元素,开始扩容 map.put(13, 13);

}

  1. **元素添加过程:**<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/25887408/1654565337287-4939f02e-dffd-423d-997b-5c529ef640f6.png#clientId=u2b6e044a-e2ea-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=74&id=u01ed7dad&margin=%5Bobject%20Object%5D&name=image.png&originHeight=93&originWidth=1084&originalType=binary&ratio=1&rotation=0&showTitle=false&size=24578&status=done&style=none&taskId=u958a0a58-5cdb-4502-adff-c265ffba936&title=&width=867.2)<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/25887408/1654565389063-dc4590cb-b48f-4f24-b5ab-9eca457cda03.png#clientId=u2b6e044a-e2ea-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=106&id=uc0640412&margin=%5Bobject%20Object%5D&name=image.png&originHeight=132&originWidth=1094&originalType=binary&ratio=1&rotation=0&showTitle=false&size=30965&status=done&style=none&taskId=u6495ea01-e6bb-4341-9d8b-bc1451cf4a9&title=&width=875.2)<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/25887408/1654565458998-a890ca17-d576-428f-b3c5-c4b86118ba37.png#clientId=u2b6e044a-e2ea-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=81&id=u28db031b&margin=%5Bobject%20Object%5D&name=image.png&originHeight=101&originWidth=1143&originalType=binary&ratio=1&rotation=0&showTitle=false&size=32596&status=done&style=none&taskId=ufb967f86-bf97-480e-9085-a76b7dd6d11&title=&width=914.4)<br />**扩容具体过程:**<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/25887408/1654565389063-dc4590cb-b48f-4f24-b5ab-9eca457cda03.png#clientId=u2b6e044a-e2ea-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=106&id=HA2wS&margin=%5Bobject%20Object%5D&name=image.png&originHeight=132&originWidth=1094&originalType=binary&ratio=1&rotation=0&showTitle=false&size=30965&status=done&style=none&taskId=u6495ea01-e6bb-4341-9d8b-bc1451cf4a9&title=&width=875.2)<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/25887408/1654565725500-759b3b14-32c6-4108-a945-377ef5596329.png#clientId=u2b6e044a-e2ea-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=118&id=uaf8bc014&margin=%5Bobject%20Object%5D&name=image.png&originHeight=147&originWidth=1135&originalType=binary&ratio=1&rotation=0&showTitle=false&size=27721&status=done&style=none&taskId=u7458917f-2b18-4b0e-bce7-70f0c6a22fd&title=&width=908)<br />![无标题-2021-10-17-2118.png](https://cdn.nlark.com/yuque/0/2022/png/25887408/1654566014612-ec39f678-5990-43d6-9214-04859876d646.png#clientId=u2b6e044a-e2ea-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=uff87bcaf&margin=%5Bobject%20Object%5D&name=%E6%97%A0%E6%A0%87%E9%A2%98-2021-10-17-2118.png&originHeight=287&originWidth=1306&originalType=binary&ratio=1&rotation=0&showTitle=false&size=68600&status=done&style=none&taskId=ue39003ae-9504-4012-94a4-9f4c5bbe0d7&title=)<br />![无标题-2021-10-17-2118.png](https://cdn.nlark.com/yuque/0/2022/png/25887408/1654567121273-a3c2979f-9783-4488-a0f6-b67601847c5c.png#clientId=u2b6e044a-e2ea-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=u89e82606&margin=%5Bobject%20Object%5D&name=%E6%97%A0%E6%A0%87%E9%A2%98-2021-10-17-2118.png&originHeight=371&originWidth=1370&originalType=binary&ratio=1&rotation=0&showTitle=false&size=106011&status=done&style=none&taskId=u8961aba4-ede3-46d6-bb63-bcc5ebf571e&title=)<br />![无标题-2021-10-17-21182.png](https://cdn.nlark.com/yuque/0/2022/png/25887408/1654567206591-d580ed3d-f8c4-493e-9c13-89656170467b.png#clientId=u2b6e044a-e2ea-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=u16cd0c19&margin=%5Bobject%20Object%5D&name=%E6%97%A0%E6%A0%87%E9%A2%98-2021-10-17-21182.png&originHeight=135&originWidth=1283&originalType=binary&ratio=1&rotation=0&showTitle=false&size=32478&status=done&style=none&taskId=u07a68951-0322-4364-b068-8fafb41d4f9&title=)
  2. ```java
  3. // 创建一个新的数组,容量为扩容后的新容量
  4. Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
  5. table = newTab;
  6. // 旧数组不为空,需要将旧数组中的值拷贝到新数组中
  7. if (oldTab != null) {
  8. for (int j = 0; j < oldCap; ++j) {
  9. // 指向当前结点的指针
  10. Node<K,V> e;
  11. if ((e = oldTab[j]) != null) {
  12. oldTab[j] = null; // 清理原数组中的垃圾
  13. if (e.next == null) // e.next == null,表明链表上只有一个结点
  14. // 将原数组中的元素存储在新数组中,这里都直接存储在旧的索引上,后面还要计算新的索引
  15. newTab[e.hash & (newCap - 1)] = e;
  16. else if (e instanceof TreeNode)
  17. ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
  18. else {
  19. // 处理链表结点数不为1的哈希冲突情况
  20. // 新建链表,用来保存不用调整索引的元素和要调整索引的元素
  21. Node<K,V> loHead = null, loTail = null; // 存储低位元素的索引
  22. Node<K,V> hiHead = null, hiTail = null; // 存储高位元素的索引
  23. Node<K,V> next;
  24. do {
  25. next = e.next;
  26. // e.hash & oldCap) == 0,无须调整索引,保留在低位
  27. if ((e.hash & oldCap) == 0) {
  28. if (loTail == null)
  29. loHead = e;
  30. else
  31. loTail.next = e;
  32. loTail = e;
  33. }
  34. else {// e.hash & oldCap) != 0 调整索引,保留到高位
  35. if (hiTail == null)
  36. hiHead = e;
  37. else
  38. hiTail.next = e;
  39. hiTail = e;
  40. }
  41. } while ((e = next) != null);
  42. if (loTail != null) {
  43. loTail.next = null;
  44. // 存储在低位的链表:原来的索引
  45. newTab[j] = loHead;
  46. }
  47. if (hiTail != null) {
  48. hiTail.next = null;
  49. // 存储在高位的链表:原来的索引 + 加上扩容的容量
  50. newTab[j + oldCap] = hiHead;
  51. }
  52. }
  53. }
  54. }
  55. }

HashMap并发丢失数据

HashMap在并发操作时,可能会出现丢失数据的线程安全问题

  1. public static void main(String[] args) throws InterruptedException {
  2. HashMap<String, Object> map = new HashMap<>();
  3. Thread t1 = new Thread(() -> {
  4. map.put("a", new Object()); // 97 => 1
  5. }, "t1");
  6. Thread t2 = new Thread(() -> {
  7. map.put("1", new Object()); // 49 => 1
  8. }, "t2");
  9. t1.start();
  10. t2.start();
  11. t1.join();
  12. t2.join();
  13. System.out.println(map);
  14. }
  1. final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
  2. boolean evict) {
  3. Node<K,V>[] tab; Node<K,V> p; int n, i;
  4. if ((tab = table) == null || (n = tab.length) == 0)
  5. n = (tab = resize()).length;
  6. if ((p = tab[i = (n - 1) & hash]) == null)
  7. /**
  8. * 在这里并发操作可能会出现线程安全问题
  9. * t1线程已经判断tab[1] == null,进入此处,但是t2线程抢到事件片开始执行
  10. * t2线判断tab[1] == null, 执行tab[2] = newNode('1');
  11. * t1再次执行是tab[1] = newNode('a'),直接将newNode('1')覆盖掉导致数据丢失
  12. **/
  13. tab[i] = newNode(hash, key, value, null);
  14. else {
  15. ...
  16. }
  17. return null;
  18. }

执行结果:{a=java.lang.Object@133314b},其中'1'丢失!

jdk1.7中HashMap的扩容死链问题

死链产生原因:HashMap线程不安全,并发情况下触发了扩容,由于jdk 1.7中添加元素为头插法,导致了扩容死链问题。

  1. void transfer(Entry[] newTable, boolean rehash) {
  2. int newCapacity = newTable.length;
  3. for (Entry<K,V> e : table) {
  4. // e指向当前要迁移的结点
  5. while(null != e) {
  6. // next指向下一个要迁移的结点
  7. Entry<K,V> next = e.next;
  8. if (rehash) {
  9. e.hash = null == e.key ? 0 : hash(e.key);
  10. }
  11. int i = indexFor(e.hash, newCapacity);
  12. e.next = newTable[i];
  13. newTable[i] = e;
  14. e = next;
  15. }
  16. }
  17. }

用语言描述一下扩容死链问题:在并发情况下,有两个线程同时操作HashMap,并且触发了扩容过程。在jdk1.7的HashMap中,就有两个线程要将原始数组中的元素迁移至扩容后的数组中,由于jdk 1.7添加元素使用的是头插法,加入原数组中的一个链表的顺序为
a -> b->null,假设线程2已经迁移了过去,那么链表的顺序将会编程b->a->null,此时线程1的引用,e指向a,next指向b,将a->b迁移过去后,形成了b->a,b的next又指向a,就会再一次将a插入b的前面,由此形成了a->b->a即死链循环。

HashMap扩容死链.drawio.png

HashMap的put源码分析

  1. HashMap是懒惰创建数组的,首次调用才会创建数组,初始化数组长度为16
  2. 往HashMap中添加元素,首先会计算key的hashCode,然后经过二次hash获取哈希值,将哈希值对数组长度取余集合计算出元素在HashMap中的桶下表,即索引
  3. 判断当前索引有没有元素,如果没有元素,直接插入,创建Node,结束
  4. 如果有元素,则要判断当前元素的结点是链表结点还是红黑树结点,如果是红黑树结点,就走红黑树的插入逻辑;如果是链表结点,就将结点插到链表最后面,同时判断链表长度是否超过树化阈值,超过的话,要转化为红黑树
  5. 最后检查HashMap容量是否超过扩容阈值,一旦超过进行扩容

hashmap.drawio.png

  1. final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
  2. boolean evict) {
  3. Node<K,V>[] tab; Node<K,V> p; int n, i;
  4. if ((tab = table) == null || (n = tab.length) == 0)
  5. // 首次调用HashMap,将容量初始化为16
  6. n = (tab = resize()).length;
  7. if ((p = tab[i = (n - 1) & hash]) == null)
  8. // 当先位置还没有元素,直接添加
  9. tab[i] = newNode(hash, key, value, null);
  10. else { // 当前位置存在元素
  11. Node<K,V> e; K k; // e:结点;k:键
  12. // // 检查tab[i]上的hash与插入元素的hash是否相等,然后检查,两者key是否相等
  13. if (p.hash == hash &&
  14. ((k = p.key) == key || (key != null && key.equals(k))))
  15. e = p; // 相等,直接覆盖tab[i]上的元素
  16. // 检查tab[i]是否为红黑树结点
  17. else if (p instanceof TreeNode)
  18. // 将结点插入至红黑树中
  19. e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
  20. // 插入链表结点,并检查是否需要树化
  21. else {
  22. // 开始遍历tab[i]上的链表
  23. for (int binCount = 0; ; ++binCount) {
  24. // 到了链表的尾结点,直接将新结点插入链表后面
  25. if ((e = p.next) == null) {
  26. // 新建结点,新结点插入链表后面
  27. p.next = newNode(hash, key, value, null);
  28. // 检查是否需要树化
  29. if (binCount >= TREEIFY_THRESHOLD - 1)
  30. treeifyBin(tab, hash);
  31. break; // 插入结点完毕,直接退出循环
  32. }
  33. if (e.hash == hash &&
  34. ((k = e.key) == key || (key != null && key.equals(k))))
  35. break; // 插入的元素已经在链表中出现,直接跳出循环 goto <标记1>
  36. // p指针移动至下一个结点, 遍历链表
  37. p = e;
  38. }
  39. }
  40. // <标记1> e不为null,说明插入的元素已经在链表中出现
  41. if (e != null) {
  42. V oldValue = e.value; // 记录旧元素的值
  43. if (!onlyIfAbsent || oldValue == null)
  44. e.value = value; // 使用新元素的值直接覆盖旧元素的值
  45. afterNodeAccess(e);
  46. return oldValue;
  47. }
  48. }
  49. ++modCount;
  50. if (++size > threshold) // 大于扩容阈值
  51. resize(); // 开始扩容
  52. afterNodeInsertion(evict);
  53. return null;
  54. }

String的hashCode()设计

hashCode()方法是为了使得每个字符串的哈希表更为独特,分布在HashMap的桶中分布的更加均匀,散列计算公式如下:
Java集合 - 图18
为什么要选择乘以31呢?

  • 选用31会有更好的散列特性,使得哈希码分散的更均匀,这样在hashMap中就不会出现一个桶中存在非常多的hash值相同的元素
  • 31*h 可以被优化为h << 5 - h, 位运算计算效率更高
    1. public int hashCode() {
    2. int h = hash;
    3. if (h == 0 && value.length > 0) {
    4. char val[] = value;
    5. // hash = S_0 * 31^{n-1} + S_1 * 31^{n-2} + ... + S_{n-1} * 31^{0}
    6. for (int i = 0; i < value.length; i++) {
    7. h = 31 * h + val[i];
    8. }
    9. hash = h;
    10. }
    11. return h;
    12. }
    关于选用hashCode中选用不同乘数,对hashCode散列特性的影响
    image.png
    image.png
    image.png

    3 ConcurrentHashMap