HashMap底层实现原理

爱被打了一巴掌 2024-03-23 15:29 162阅读 0赞

Jdk8 HashMap底层实现原理

HashMap的简单的概述

hashmap是一个k,v键值对型的集合,是java中常用的集合类对象,其基于哈希表的Map的接口进行实现。hashmap的实现不是同步的,这意味着它不是线程安全的。如果需要使用线程安全的k-v集合我们可以使用ConcurrentHashMap或者HashTable。hashmap的key以及value是可以为null的,但ConcurrentHashMap中是不允许key和value为null的。HashMap的映射不是有序的。jdk1.8之前HashMap有数组+链表组成,Hashmap的组成主要是由数组的形式为主,链表是为了解决hash冲突的。jdk8之后HashMap引入了红黑树,当数组中存储的数据大于数组长度*0.75时触发数组的扩容,当数组长度大于64且链表长度大于8时再次触发其扩容机制链表将变成红黑树。红黑树结点小于6时将转换为链表。

hash冲突以及解决方式

一、 什么是hash?

Hash叫做“散列表”,就是把任意长度的输入,通过散列算法,变成固定长度输出,该输出结果是散列值。其实这种转换是一种压缩映射,散列表的空间通常小于输入的空间,不同的输入可能会散列成相同的输出,所以不能从散列表来唯一的确定输入值。这就出现了Hash冲突。

二、 什么是hash冲突?

根据key(键)即经过一个函数f(key)得到的结果的作为地址去存放当前的key value键值对(这个是hashmap的存值方式),但是却发现算出来的地址上已经被占用了。这就是所谓的hash冲突。

三、 解决hash冲突的方法

  1. 开放定址法
    该方法也叫做再散列法,其基本原理是:当关键字key的哈希地址p=H(key)出现冲突时,以p为基础,产生另一个哈希地址p1,如果p1仍然冲突,再以p为基础,产生另一个哈希地址p2,…,直到找出一个不冲突的哈希地址
  2. 再Hash法
    这种方法就是同时构造多个不同的哈希函数: Hi=RH1(key) i=1,2,…,k。当哈希地址Hi=RH1(key)发生冲突时,再计算Hi=RH2(key)……,直到冲突不再产生。这种方法不易产生聚集,但增加了计算时间。
  3. 链地址法(hashmap使用方式)
    其基本思想: 将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。
  4. 平方寻址法

    即是发生冲突时,用发生冲突的单元H(key), 加上 1²、 2²等,即H(key) + 1²,H(key) + 2²,H(key) + 3²…直到找到空闲单元。f(i)也可以构造为:±i^2,i=1,2,3,…,k。

hashmap 在内存中的结构

在这里插入图片描述

hashMap 体系结构

在这里插入图片描述

源码阶段

在开启追源码之路之前先来介绍几个属性:

  1. static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
  2. static final float DEFAULT_LOAD_FACTOR = 0.75f; //加载因子
  3. static final int TREEIFY_THRESHOLD = 8; //转为树的阈值
  4. static final int UNTREEIFY_THRESHOLD = 6; //树退化为链表的阈值
  5. static final int MIN_TREEIFY_CAPACITY = 64;//数组长度转为红黑树的阈值
  6. static final int MAXIMUM_CAPACITY = 1 << 30; //最大容量
  7. int threshold; //阈值

构造方法

  1. public HashMap(int initialCapacity, float loadFactor) {
  2. if (initialCapacity < 0)
  3. throw new IllegalArgumentException("Illegal initial capacity: " +
  4. initialCapacity);
  5. if (initialCapacity > MAXIMUM_CAPACITY)
  6. initialCapacity = MAXIMUM_CAPACITY;//给定的初始容量大于最大容量时将默认容量改为最大容量
  7. if (loadFactor <= 0 || Float.isNaN(loadFactor))
  8. throw new IllegalArgumentException("Illegal load factor: " +
  9. loadFactor);
  10. this.loadFactor = loadFactor;//根据给定的加载因子赋给成员
  11. this.threshold = tableSizeFor(initialCapacity); //将容量转化成2的幂的形式
  12. }
  13. public HashMap(Map<? extends K, ? extends V> m) {
  14. this.loadFactor = DEFAULT_LOAD_FACTOR;
  15. putMapEntries(m, false);//将传进来的map赋值给新的map
  16. }
  17. public HashMap(int initialCapacity) {
  18. //指定初始容量,使用默认加载因子
  19. this(initialCapacity, DEFAULT_LOAD_FACTOR);
  20. }
  21. public HashMap() {
  22. this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
  23. }

数组容量的加工

  1. //具体算法思想:对任意十进制数转换为2的整数幂,结果是这个数本身的最高有效位的前一位变成1,最高有效位以及其后的位都变为0,保证了任意十进制数转换为2的整数次幂
  2. static final int tableSizeFor(int cap) {
  3. int n = cap - 1;
  4. n |= n >>> 1;
  5. n |= n >>> 2;
  6. n |= n >>> 4;
  7. n |= n >>> 8;
  8. n |= n >>> 16;
  9. return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
  10. }

hash码的算法

  1. //key为空时直接返回0,如果不为空获取key的hashcode与其左移16位的值的进行异或运算,保证了高16位与低16位都参与到hash的运算当中,以减小hash冲突的可能
  2. static final int hash(Object key) {
  3. int h;
  4. return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  5. }
  6. //具体实例
  7. //如果得到两key的hashcode为以下两种,如果不进行移位,如果此时数组的长度为16,则两个key得到的位置均为10,形成hash冲突
  8. 0000 1010 0010 1010 1010 1000 1010 1010
  9. 0000 1010 0011 1011 1010 1000 0010 1010
  10. //如果按照如此上述代码方式,我们将会得到一个数组下标为0,一个数组下标为1,从一定程度上减小了hash冲突
  11. 0000 1010 0010 1010 1010 1000 1010 1010
  12. 0000 0000 0000 0000 0000 1010 0010 1010
  13. ----------------------------------------
  14. 0000 1010 0010 1010 1010 0010 1000 0000
  15. 0000 1010 0011 1011 1010 1000 0010 1010
  16. 0000 0000 0000 0000 0000 1010 0011 1011
  17. ------------------------------------------
  18. 0000 1010 0011 1011 1010 0010 0001 0001

putVal(put方法添加)

  1. //jdk 1.8
  2. final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
  3. boolean evict) {
  4. Node<K,V>[] tab; Node<K,V> p; int n, i;
  5. //判断当前数组是否初始化
  6. if ((tab = table) == null || (n = tab.length) == 0)
  7. //没有初始化就去初始化
  8. n = (tab = resize()).length;
  9. //通过hash值与数组长度相与得到元素存放的数组下标,如果该位置为空直接添加结点,如果不为空则出现哈希冲突,利用拉链法将结点利用尾插的方式插入
  10. if ((p = tab[i = (n - 1) & hash]) == null)
  11. tab[i] = newNode(hash, key, value, null);
  12. else {
  13. Node<K,V> e; K k;
  14. if (p.hash == hash && //如果两个节点的key相同直接替换
  15. ((k = p.key) == key || (key != null && key.equals(k))))
  16. e = p;
  17. else if (p instanceof TreeNode)//如果当前节点已经为红黑树则采用树的添加方式进行添加
  18. e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
  19. else {
  20. //如果为链表,则采用尾插的方式,如果链表长度大于8则会调用treeifyBin方法进行扩容
  21. for (int binCount = 0; ; ++binCount) {
  22. if ((e = p.next) == null) {
  23. p.next = newNode(hash, key, value, null);//尾插
  24. if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st //如果链表长度大于转为红黑树的阈值将进行对数组扩容或者转为红黑树
  25. treeifyBin(tab, hash);
  26. break;
  27. }
  28. if (e.hash == hash && 判断链表中是否有重复值
  29. ((k = e.key) == key || (key != null && key.equals(k))))
  30. break;
  31. p = e;
  32. }
  33. }
  34. //判断是否有重复key,替换旧值
  35. if (e != null) {
  36. // existing mapping for key
  37. V oldValue = e.value;
  38. if (!onlyIfAbsent || oldValue == null)
  39. e.value = value;
  40. afterNodeAccess(e);
  41. return oldValue;
  42. }
  43. }
  44. //迭代器的failfast机制
  45. //在HashMap使用迭代器时,在remove、put元素的时候可能会导致迭代器中的内容数据破坏,
  46. ++modCount;
  47. //判断一下当前的HashMap容量是否已经达到扩容的阈值,达到阈值进行扩容
  48. if (++size > threshold)
  49. resize();
  50. afterNodeInsertion(evict);
  51. return null;
  52. }

treeifyBin

主要工作:

  1. 对数组进行扩容
  2. 满足转换红黑树的条件时进行转换

    final void treeifyBin(Node[] tab, int hash) {

    1. int n, index; Node<K,V> e;
    2. if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)//如果数组长度小于64则对数组进行扩容
    3. resize();
    4. else if ((e = tab[index = (n - 1) & hash]) != null) {
    5. //否者将转为红黑树
    6. TreeNode<K,V> hd = null, tl = null;
    7. do {
    8. TreeNode<K,V> p = replacementTreeNode(e, null);
    9. if (tl == null)
    10. hd = p;
    11. else {
    12. p.prev = tl;
    13. tl.next = p;
    14. }
    15. tl = p;
    16. } while ((e = e.next) != null);
    17. if ((tab[index] = hd) != null)
    18. hd.treeify(tab);
    19. }

    }

resize方法(扩容)

主要分为两大步:

  1. 根据不同的情况进行对容量和阈值进行扩大
  2. 将原来数组的中的元素转移到新的数组当中

    //第一步:根据不同的情况对其的容量阈值进行扩大
    final Node[] resize() {

    1. //将table中的值和数据保存下来
    2. Node<K,V>[] oldTab = table;
    3. //检查其数组的长度,是已经初始化过的还是没有被初始化的
    4. int oldCap = (oldTab == null) ? 0 : oldTab.length;
    5. //旧值的阈值
    6. int oldThr = threshold;
    7. int newCap = 0;//新的容量
    8. newThr = 0;//新的阈值
    9. if (oldCap > 0) {
    10. if (oldCap >= MAXIMUM_CAPACITY) {
    11. //如果当前容量已经大于MAXIMUM_CAPACITY则修改阈值为Integer.MAX_VALUE直接返回即可
    12. threshold = Integer.MAX_VALUE;
    13. return oldTab;
    14. }//否则将原来的容量扩大两倍并且大于默认容量时将阈值扩大原来的两倍
    15. else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
    16. oldCap >= DEFAULT_INITIAL_CAPACITY)
    17. newThr = oldThr << 1; // double threshold
    18. }
    19. //若没有经历过初始化,并且在使用时通过构造函数指定了initialCapacity, 则table大小为threshold, 即大于指定initialCapacity的最小的2的整数次幂
    20. else if (oldThr > 0) // initial capacity was placed in threshold
    21. newCap = oldThr;
    22. //若没有被初始化过则采用默认的容量16 以及阈值12
    23. else {
    24. // zero initial threshold signifies using defaults
    25. newCap = DEFAULT_INITIAL_CAPACITY;
    26. newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    27. }
    28. if (newThr == 0) {
    29. //如果阈值等于零时通过加载因子和新的容量乘积进行计算,
    30. float ft = (float)newCap * loadFactor;
    31. //计算阈值小于最大容量并且最后确定的容量小于最大容量,则计算出的阈值可以使用,若不满足上述两个条件任何一条则阈值为最大值
    32. newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
    33. (int)ft : Integer.MAX_VALUE);
    34. }
    35. threshold = newThr;//将新的阈值赋给成员变量
  1. //第二步将原来数组的中的元素转移到新的数组当中
  2. @SuppressWarnings({
  3. "rawtypes","unchecked"})
  4. Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//创建新容量的数组
  5. //将新建立数组赋值到HashMap成员变量
  6. table = newTab;
  7. if (oldTab != null) {
  8. for (int j = 0; j < oldCap; ++j) {
  9. Node<K,V> e;
  10. if ((e = oldTab[j]) != null) {
  11. //将旧数组[j]处置为空
  12. oldTab[j] = null;
  13. if (e.next == null)
  14. newTab[e.hash & (newCap - 1)] = e;//重新计算在新数组中的位置
  15. else if (e instanceof TreeNode)//如果该节点为红黑树结点则根据红黑树进行转换
  16. ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
  17. else {
  18. // preserve order
  19. Node<K,V> loHead = null, loTail = null;
  20. Node<K,V> hiHead = null, hiTail = null;
  21. Node<K,V> next;
  22. do {
  23. next = e.next;
  24. if ((e.hash & oldCap) == 0) {
  25. //生成两个链表
  26. if (loTail == null)
  27. loHead = e;
  28. else
  29. loTail.next = e;
  30. loTail = e;
  31. }
  32. else {
  33. if (hiTail == null)
  34. hiHead = e;
  35. else
  36. hiTail.next = e;
  37. hiTail = e;
  38. }
  39. } while ((e = next) != null);
  40. if (loTail != null) {
  41. //形成两个链表,接下来就是将两个链表分别插入不同位置
  42. loTail.next = null;
  43. newTab[j] = loHead;
  44. }
  45. if (hiTail != null) {
  46. hiTail.next = null;
  47. newTab[j + oldCap] = hiHead;
  48. }
  49. }
  50. //解释一下:为什么是j和j+oldcap的位置?
  51. //如果原始数组长度16则新数组的长度必然为32,根据hash值计算在数组中存放的位置为(n-1)&hash
  52. //原始数组 最后决定位置的参考值为最后4位
  53. //扩容之后 最后决定位置的参数值为最后5位
  54. //其实也就是说如果倒数第五位为0时即在原来位置,如果该位为1时即为原来位置+原来数组长度
  55. //其实这样说来就是if语句中条件判断的来源了
  56. }
  57. }
  58. }
  59. return newTab;
  60. }

removeNode(删除)

主要三大步:

  1. 根据不同的情况查找到对应的结点
  2. 再根据不同的情况进行移除
  3. 修改其大小

    // @param matchValue if true only remove if value is equal
    //
    @param movable if false do not move other nodes while removing
    final Node removeNode(int hash, Object key, Object value,

    1. boolean matchValue, boolean movable) {
    2. Node<K,V>[] tab; Node<K,V> p;
    3. int n, index;
    4. if ((tab = table) != null && (n = tab.length) > 0 &&
    5. (p = tab[index = (n - 1) & hash]) != null) {
    6. Node<K,V> node = null, e; K k; V v;
    7. if (p.hash == hash &&
    8. ((k = p.key) == key || (key != null && key.equals(k))))
    9. node = p;//如果通过hash直接找到对应数组元素,则把p赋给node
    10. else if ((e = p.next) != null) {
    11. //如果该结点下为链表或红黑树
    12. if (p instanceof TreeNode)
    13. node = ((TreeNode<K,V>)p).getTreeNode(hash, key);//在红黑树中寻找待删除结点
    14. else {
    15. //如果为链表
    16. do {
    17. if (e.hash == hash &&
    18. ((k = e.key) == key ||
    19. (key != null && key.equals(k)))) {
    20. node = e;
    21. break;
    22. }
    23. p = e;
    24. } while ((e = e.next) != null);
    25. }
    26. }
    27. //如果node不为空说明找到了要删除的Node节点
    28. //并且matchValue为false
    29. //或者matchValue为true,并且node的value等于参数中的value
    30. //或者matchValue为true,并且value放法的equals返回true
    31. //则可以执行删除操作
    32. if (node != null && (!matchValue || (v = node.value) == value ||
    33. (value != null && value.equals(v)))) {
    34. if (node instanceof TreeNode)//如果为红黑树结点则采用树删除结点的方式
    35. ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
    36. else if (node == p) //该节点为链表的第一个结点则直接下移
    37. tab[index] = node.next;
    38. else //如果既不是treenode类型,又等于节点p就修改链表使p的后继节点等于node的后继节点
    39. p.next = node.next;
    40. //将修改次数++
    41. ++modCount;
    42. --size;//将size--
    43. afterNodeRemoval(node);
    44. return node;
    45. }
    46. }
    47. return null;
    48. }

getNode(查找)

  1. final Node<K,V> getNode(int hash, Object key) {
  2. Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
  3. if ((tab = table) != null && (n = tab.length) > 0 &&
  4. (first = tab[(n - 1) & hash]) != null) {
  5. if (first.hash == hash && // always check first node
  6. ((k = first.key) == key || (key != null && key.equals(k))))
  7. return first;//如果第一个存在数组中的就是则直接放回
  8. if ((e = first.next) != null) {
  9. if (first instanceof TreeNode)//如果为树则利用树的形式进行查找
  10. return ((TreeNode<K,V>)first).getTreeNode(hash, key);
  11. do {
  12. //如果不是树,则利用链表的形式
  13. if (e.hash == hash &&
  14. ((k = e.key) == key || (key != null && key.equals(k))))
  15. return e;
  16. } while ((e = e.next) != null);
  17. }
  18. }
  19. return null;
  20. }

面试官爱问的问题

  1. hashMap的底层数据结构

    1. jdk 1.8 采用数组+链表+红黑树的方式
    2. jdk 1.8之前采用数组+链表

    当链表过长,则会严重影响HashMap的性能,红黑树搜索时间复杂度是O(logn),而链表是O(n)。因此,JDK1.8对数据结构做了进一步的优化,引入了红黑树,链表和红黑树在达到一定条件会进行转换:

    当链表超过8且数组长度(数据总量)超过64才会转为红黑树

    将链表转换成红黑树前会判断,如果当前数组的长度小于64,那么会选择先进行数组扩容,而不是转换为红黑树,以减少搜索时间。

  2. hashmap的特点

    1. hashmap存取是无序的
    2. 键和值位置都可以是null,但是键位置只能是一个null
    3. 键位置是唯一的,底层的数据结构是控制键的
    4. jdk1.8前数据结构是:链表+数组jdk1.8之后是:数组+链表+红黑树
    5. 阈值(边界值)>8并且数组长度大于64,才将链表转换成红黑树,变成红黑树的目的是提高搜索速度,高效查询
  3. 解决hash冲突方法有哪些?hashmap中用的哪种?

    1. 开放定址法
      该方法也叫做再散列法,其基本原理是:当关键字key的哈希地址p=H(key)出现冲突时,以p为基础,产生另一个哈希地址p1,如果p1仍然冲突,再以p为基础,产生另一个哈希地址p2,…,直到找出一个不冲突的哈希地址
    2. 再Hash法
      这种方法就是同时构造多个不同的哈希函数: Hi=RH1(key) i=1,2,…,k。当哈希地址Hi=RH1(key)发生冲突时,再计算Hi=RH2(key)……,直到冲突不再产生。这种方法不易产生聚集,但增加了计算时间。
    3. 链地址法(hashmap使用方式)
      其基本思想: 将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。
    4. 平方寻址法

      即是发生冲突时,用发生冲突的单元H(key), 加上 1²、 2²等,即H(key) + 1²,H(key) + 2²,H(key) + 3²…直到找到空闲单元。f(i)也可以构造为:±i^2,i=1,2,3,…,k。

      在hashmap中使用了拉链寻址法(即链式寻址法)

  4. 为什么hashmap中数组长度大于64之后,链表长度大于8之后才转换为红黑树?

    1. 我们都知道数组是可以通过下标直接访问的,即时间复杂度为O(1),而链表的查询的时间复杂度为O(N),红黑树的查询的时间复杂度为O(logn)
    2. 虽然红黑树的查询速度是相比链表来说是比较高的,但是转换为红黑树的过程是非常费时间和空间的
    3. 经过官方的测试hash冲突是很少会出现8次以上的,从官方来讲也是不太想去将其转换为树结构的
    4. 就数组为什么要扩展到64就源码注释来说是为了防止数组的长度大小的改变来引起树化,简单来说就是防止多次链表和树的转换
  5. 为什么重写equals方法要重写hashcode方法

    1. equals 方法和 hashCode 方法是 Object 类中的两个基础方法,它们共同协作来判断两个对象是否相等。
    2. 如果只重写了 equals 方法,那么默认情况下,Set 进行去重操作时,会先判断两个对象的 hashCode 是否相同,此时因为没有重写 hashCode 方法,所以会直接执行 Object 中的 hashCode 方法,而 Object 中的hashCode 方法对比的是两个不同引用地址的对象,所以结果是 false,那么 equals 方法就不用执行了,直接返回的结果就是 false:两个对象不是相等的,于是就在 Set 集合中插入了两个相同的对象。
    3. 但是,如果在重写 equals 方法时,也重写了 hashCode 方法,那么在执行判断时会去执行重写的 hashCode 方法,此时对比的是两个对象的所有属性的 hashCode 是否相同,于是调用 hashCode 返回的结果就是 true,再去调用 equals 方法,发现两个对象确实是相等的,于是就返回 true 了,因此 Set 集合就不会存储两个一模一样的数据了,于是整个程序的执行就正常了。

    总结 hashCode 和 equals 两个方法是用来协同判断两个对象是否相等的,采用这种方式的原因是可以提高程序插入和查询的速度,如果在重写 equals 时,不重写 hashCode,就会导致在某些场景下,例如将两个相等的自定义对象存储在 Set 集合时,就会出现程序执行的异常,为了保证程序的正常执行,所以我们就需要在重写 equals 时,也一并重写 hashCode 方法才行。

  6. hashMap中的容量为什么要保证是2的n次幂

    其的主要目的还是为了减小hash冲突的可能,当容量为2的整数幂次时在计算在数组中的位置时,与hash进行与运算(n-1)&hash,n-1保证左边为1111这样的二进制数字,做与运算的规则为有0则为0,全1才为1,这在一定程度上极大的减小了hash冲突的可能。

  7. 加载因子为什么为0.75

    loadFactory越趋近于1,那么数组中存放的数据(entry也就越来越多),数据也就越密集,也就会有更多的链表长度处于更长的数值,我们的查询效率就会越低,添加数据时,产生hash冲突的概率也会更高

    默认的loadFactory是0.75,loadFactory越小,越趋近于0,数组中个存放的数据(entry)也就越少,表现得更加稀疏

    0.75是对空间和时间效率的一种平衡选择

    如果负载因子小一些比如是0.4,那么初始长度16*0.4=6,数组占满6个空间就进行扩容,很多空间可能元素很少甚至没有元素,会造成大量的空间被浪费

    如果负载因子大一些比如是0.9,这样会导致扩容之前查找元素的效率非常低

    就0.75并不一定非要使用他,可以根据自己的数据的需求去适当改变,只是一种比较好的建议。

  8. 一般用什么作为hashmap中的key?

    一般情况下会选择不可变的值或对象作为Map中的key,其实一切对象都可以作为Key值。如:Integer、Long、String、Object等。但是在实际工作中,最常用的使用String作为Key值。

    主要原因如下:

    1. 使用Object作为Key值的时候,如class Person (里面包含,姓名,年龄,性别,电话等属性)作为Key。当Person类中的属性改变时,导致hashCode的值也发生变化,变化后,map.get(key)因为hashCode值的变化,而无法找到之前保存的value值,同样,删除也取不到值。解决方案是重写HashCode方法
    2. 避免使用Long,Integer做key。由于拆箱装箱的过程将可能创建新的对象,导致取不到数据。
  9. hashmap为什么线程不安全?

    首先我们通过看hashmap的源码,其中的相关方法从hashmap的插值、移除元素、以及查找等一系写的方法都没有对其有线程安全的处理。自然也就线程不安全了。

    在多线程高并发的情况下任意一条语句都有可能被cpu执行,这就很难去保证线程的安全,数据的不丢失问题了

  10. 为什么在计算hash值时为什么要让低16位和高16位进行异或处理(见上述hash值的计算)
  11. hashmap中为什么选用红黑树而没有选用avl树

    1. 红黑树和AVL树都是最常用的平衡二叉搜索树,它们的查找、删除、修改都是O(logn)
    2. Avl是严格的平衡查找树,因此可以提供更快的查找速度,一般使用在读取查找密集型任务,但由于其的条件相对来说是比较苛刻的(从根到任何叶子的最短路径和最长路径之间的差异最多为1),这就对于添加和删除是很不友好的,真是因为其的严格平衡性,有可能因为添加和删除操作就会导致树进行左旋或者右旋操作,变得没有那么不在节省时间。但对红黑树来说就没那么严苛,从根到任何叶子的最短路径和最长路径之间的差异最多可以为2,相对来说这就会使得左旋以及右旋的操作变得少一些。
    3. 就实际操作来说Avl树对平衡的调节上是比较麻烦的且是不太好操作的
  12. hashmap如何进行遍历

    就实际和普遍使用来说hashmap有两种遍历方式

    1. 利用EntrySet对象进行遍历

      1. for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
      2. System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue());
      3. }
    2. 利用keyset获取到对应的key通过key得到具体的vlue进行遍历

      1. for (Integer key : map.keySet()) {
      2. System.out.println("Key = " + key);
      3. }
    3. 利用迭代器进行遍历

      1. Iterator<Map.Entry<Integer, Integer>> entries = map.entrySet().iterator();
      2. while (entries.hasNext()) {
      3. Map.Entry<Integer, Integer> entry = entries.next();
      4. System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue());
      5. }
      6. Iterator<Map.Entry> entries = map.entrySet().iterator();
      7. while (entries.hasNext()) {
      8. Map.Entry entry = (Map.Entry) entries.next();
      9. Integer key = (Integer) entry.getKey();
      10. Integer value = (Integer) entry.getValue();
      11. System.out.println("Key = " + key + ", Value = " + value);
      12. }
    4. 利用jdk8新特性进行遍历

      1. map.forEach((k, v) -> System.out.println("key: " + k + " value:" + v));
  13. HashMap的最大容量为什么是2的30次方(1左移30)?

    在java中int类型为有符号的整形即最高位为符号位,符号位为1时代表该数为负数,int类型在java中占用4个字节即32位,1>>30位之后,其实已经占用了31位,如果再进行移位将变成负值,无法作为容量进行操作。

  14. HashMap初始化容量,设置多少合适。

    我们知道创建一个HashMap对象如果不指定初始化容量initialCapacity的话,HashMap的默认容量是16,但在阿里手册上这样写着建议使用下面的方式进行初始化,但这个初始化容量应该如何去填写呢?真的是需要多少创建多少吗?比如说我现在需要7个空间,就要传一个7吗?其实并非这样,当我们将初始容量传入7时实际上jdk会将容量改为8,也就是距离7最小的2的整数幂次,这样算来如果乘以加载因子0.75的话,我们只能往里面添加6个数据,如果在想添加将会触发扩容机制,其实我们知道扩容是一个很费时间以及内存的一件事情,所以要7个元素的空间传7并不是一个好的选择,所以阿里规范中提出了 expectedSize / 0.75F + 1.0F;这样得出的结果为10,最终我们将会得到一个16容量的map集合,可以添加的元素个数为12个,虽然在一定程度上浪费了空间,但减少了一次扩容,如果我们的数据量比较大,在没有进行对hashmap进行初始化容量设置的话,使用默认值给定的16这将会导致n多次的扩容,这就会导致大量的时间以及内存浪费在上面。

    1. public HashMap(int initialCapacity) {
    2. //指定初始容量,使用默认加载因子
    3. this(initialCapacity, DEFAULT_LOAD_FACTOR);
    4. }

样写着建议使用下面的方式进行初始化,但这个初始化容量应该如何去填写呢?真的是需要多少创建多少吗?比如说我现在需要7个空间,就要传一个7吗?其实并非这样,当我们将初始容量传入7时实际上jdk会将容量改为8,也就是距离7最小的2的整数幂次,这样算来如果乘以加载因子0.75的话,我们只能往里面添加6个数据,如果在想添加将会触发扩容机制,其实我们知道扩容是一个很费时间以及内存的一件事情,所以要7个元素的空间传7并不是一个好的选择,所以阿里规范中提出了 expectedSize / 0.75F + 1.0F;这样得出的结果为10,最终我们将会得到一个16容量的map集合,可以添加的元素个数为12个,虽然在一定程度上浪费了空间,但减少了一次扩容,如果我们的数据量比较大,在没有进行对hashmap进行初始化容量设置的话,使用默认值给定的16这将会导致n多次的扩容,这就会导致大量的时间以及内存浪费在上面。

  1. ```java
  2. public HashMap(int initialCapacity) {//指定初始容量,使用默认加载因子
  3. this(initialCapacity, DEFAULT_LOAD_FACTOR);
  4. }
  5. ```

发表评论

表情:
评论列表 (有 0 条评论,162人围观)

还没有评论,来说两句吧...

相关阅读

    相关 HashMap底层实现原理

    数据结构中有数组和链表这两个结构来存储数据。   数组存储区间是连续的,占用内存严重,故空间复杂的很大。但数组的二分查找时间复杂度小,为O(1);数组的特点是:寻址容易,插入

    相关 HashMap底层实现原理

    数据结构中有数组和链表这两个结构来存储数据。 数组存储区间是连续的,占用内存严重,故空间复杂度很大。但数组的二分查找时间复杂度小,为O(1);数组的特点是:寻址容易,插入和删