02 HashMap

1. HashMap的底层数据结构
  • HashMap底层是数组+链表+红黑树的结构,其中红黑树是JDK1.8加入的,数组中的每个位置存放的是节点,1.7中叫Entry,1.8中叫Node。每一个节点都会保存自身的hash值、key、value、以及next指针。
  • 在put操作时,将元素的key的hashCode 值的高16位和低16位异或得到key的 hash 值,然后将key的 hash 值和数组长度 length -1 的值进行与运算来得到元素所在桶的位置。如果出现冲突,即多个元素对应到一个桶,就采用拉链法,将他们组织成链表放在桶中,1.7采用的是头插法,1.8采用的是尾插法。当链表元素个数>=8,链表就会转成红黑树。<=6时红黑树又会还原成链表。当数组中元素的个数超过数组容量✖️负载因子时,就会进行2倍扩容。
  • HashMap默认加载因子是0.75f,默认初始容量是16
2. HashMap的put的过程
  • 首先通过元素的key的 hashCode 值的高16位和低16位的异或得到 hash 值,hash 值和 length -1 进行与操作,得到桶的位置。
  • 如果桶中没有节点,就将元素插入桶中;如果有节点,就需要和桶中链表的节点进行比较,看是否相同(判断相等的方法是:首先判断两个key的hash值是否相等,由于 hash 值是通过 hashCode 高低16位异或得来的,所以此处需要重写 hashCode 方法来保证相同的对象返回相同的 hash 值,不同的对象返回不同的 hash 值,如果相等再通过equals方法判断key是否相等,所以还需要重写equals方法。两个操作都相等,则判定相同)。如果相同,就对这个节点进行覆盖,如果不同,就以尾插法的方式插入到链表尾部。
  • 如果链表长度>=8,就把链表转成红黑树,如果链表长度≤6,就把红黑树还原成链表。(中间的差值7可以防止链表和树之间频繁的转换)
3. HashMap的get的过程

  首先将元素 hashCode 值的高16位和低16位的异或得到 hash 值,hash 值和 length -1 进行与操作,得到桶的位置。如果桶为空,就直接返回 null。否则就判断桶的第一个节点是否为要查询的 key,是的话就返回 value 。如果不是,就判断桶中节点是红黑树还是链表,并按照各自的方法来查找。(getNode()和getTreeNode())

4. HashMap、HashTable、ConcurrentHashMap 的区别?
  • 结构:三者底层结构都是数组+链表,其中HashMap和ConcurrentHashMap在1.8又引入了红黑树的结构,并且采用尾插法。
  • 并发安全性:HashMap不安全,他的方法都没有加锁,在多线程环境下无法保证上一秒 put 的值,在下一秒 get 的时候还是原值。HashTable和ConcurrentHashMap是线程安全的,HashTable实现线程安全的方式是使用synchronized对整个HashTable加锁,效率比较低;ConcurrentHashMap 1.7是把整个Map分为若干个Segment,通过ReentrantLock对每个Segment单独加锁,1.8是通过 Synchronized 和 CAS对每个Node进行加锁,效率比较高。
  • 存值:HashMap的key和value可以为null,HashTable和ConcurrentHashMap的key和value都不能为null。解答
  • 初始容量:HashMap和ConcurrentHashMap的默认初始容量是16,采取两倍扩容方式;HashTable的默认初始容量是11,2倍+1的扩容方式。
5. 为什么HashTable和ConcurrentHashMap的key和value都不能为null

value不能为null
  在多线程的环境中,当通过get(k)方法获取到的是null时,无法判断,它是put(k,v)的时候value为null,还是这个key从来没有做过映射。系统无法判断出这种模糊不清的情况 假如线程1调用m.contains(key)返回true,然后在调用m.get(key),这时的m可能已经不同了。因为线程2可能在线程1调用m.contains(key)时,删除了key节点,这样就会导致线程1得到的结果不明确,产生多线程安全问题,因此,Hashmap和ConcurrentHashMap的key和value不能为null。
  HashMap允许key和value为null,在单线程时,调用contains()和get()不会出现问题。
  HashTable和ConcurrentHashMap需要保证多线程的安全性,HashMap不需要保证,他是为单线程情况服务的。

6. HashMap1.7和1.8的区别?
  • 1.7 采用Entry数组和链表结构,如果hash函数特别差会导致链表很长,查找时间复杂度就是O(n);1.8对这个进行了改进,采用Node数组+链表+红黑树结构,当链表元素≥8时,就转换成红黑树结构。最差时间复杂度也是O(logn)
  • 1.7采用头插法插入链表中,1.8采用尾插法插入链表中
  • 1.8 将hash 值的运算改为通过 hashCode 高低16位异或来得到,让高低位数据都参与到 hash 值的计算中,这样可以减少碰撞的几率
7. 为什么阀值是8?

  因为泊松分布,在负载因子默认为0.75时,单个桶内元素个数为8的概率小于百万分之一。所以将7作为一个分水岭,等于7时不转换,大于等于8时才进行转换,小于等于6时就退化为链表。

8. 为什么链表转红黑树?
  • 红黑树是二叉排序树,查找效率比链表高,所以当链表达到一定长度时,要转化成红黑树。
9. 为什么不直接用红黑树? 而选择先用链表,再转红黑树?

  因为红黑树需要进行左旋、右旋、变色这些操作来保持平衡,新增节点的效率比较慢。当桶中元素个数小于8时,链表结构已经能保证查询性能了。当元素个数大于8时,此时就需要红黑树来加快查询速度。因此,如果一开始就用红黑树结构,元素太少,增删效率又比较慢,性能比较低。

10. 不用红黑树,用二叉查找树可以么?

  可以,但是二叉查找树在特殊情况下会变成一条线性结构,比如只有左子树,这就跟原来使用的链表结构一样了,遍历查找会很慢。

11. 为什么1.7用头插法,1.8改成了尾插法?

  1.7中采用头插法,是因为作者认为后插入的节点被访问的可能性大一些,所以用了头插法可以提高检索效率。但是后来发现头插法可能会在多线程环境中形成死循环,所以在1.8就使用的尾插法。

12. 为什么头插法会形成死循环?尾插法就不会?(扩容为什么会发生死锁)
  • HashMap是线程不安全的,多个线程可以同时对hashMap进行扩容。
  • 比如说一个桶中放着A->B这样一个链表,扩容时如果采用头插法,链表就会倒转变成B->A,在多线程环境中,如果多个线程一起扩容,就可能会出现环形链表,从而造成死循环。具体原理
  • 但是如果使用尾插法,在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题了。
  • 依然存在的问题:
    • 虽然1.8 尾插法避免了扩容时可能出现死循环的问题,但HashMap在多线程的环境下还是不安全的,因为 HashMap 的源码中,put 和 get 方法都没有加同步锁,在多线程的环境下,就可能出现数据丢失,所以线程安全依然无法保证。
13. 为什么HashMap要用数组和链表的结构?

  数组通过随机访问来快速确定桶的位置,利用元素的key的hash值对数组长度取模得到; 链表插入删除快,而且可以解决hash冲突问题,当出现hash值一样的情形,就在数组上的对应位置形成一条链表。

14. 为什么HashMap不用ArrayList?

  hashMap的数组容量是2的n次方,做取模运算的效率高,而ArrayList的扩容机制是1.5倍的扩容,容量不一定能满足2的n次方的要求。

15. hashCode 怎么对应桶的位置?
  • 将key的hashCode 的高16位和低16位进行异或得到 hash 值
  • 再将得到的hash值和数组长度-1进行与操作,就得到数组下标的位置了
    index = hash & (Length - 1)
16. 为什么要高16位和低16位异或操作?

  因为数组的长度 length 比较小时它的二进制高位都是0,与 hashCode 进行与运算的话,无论 hashCode 的高位怎么变,对运算的结果都不会产生影响,从而导致碰撞的几率会变大。于是将 hashCode 的高16位和低16位异或,让高低位数据也可以参与到Hash的计算中,可以减少hash碰撞。(同时位运算也不会有太大的开销)

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
17. HashMap什么时候扩容,扩容过程?
  • 当HashMap的数组Size 超过 容量✖️负载因子时进行扩容(默认负载因子是0.75)
  • 扩容过程:首先创建一个新的Entry空数组,长度是原数组的2倍。然后遍历原数组,把所有的Entry重新hash到新数组。之所以要重新hash而不是直接复制,是因为数组容量改变,桶的位置也会改变。index = hash &(新Length - 1)
18. HashMap的主要参数都有哪些?
  • 初始化容量为16,这个16是以位运算的形式写的,因为位运算效率高。
  • 最大容量 2的30 ,也是以位运算的形式写的,之所以定义这么大是为了防止频繁的扩容。
  • 负载因子 0.75f。
  • 阈值8,大于等于8时链表会转化为红黑树
  • 阈值6,小于等于6时会退化回链表
19. 负载因子为什么是0.75?

  表示数组的稀疏程度,越大,数组存放的 entry 就越多,也就越稠密,太大会导致查找效率变低;越小,数组存放的 entry 就越少,也就越稀疏,太小会导致数组利用率低。
  节点出现的频率在hash桶中遵循泊松分布,当桶中元素到达8个的时候,概率已经变得非常小,也就是说用0.75作为加载因子,每个桶的链表长度超过8个是几乎不可能的。

20. HashMap默认长度为什么设置初始化为16?

  HashMap的容量是2的n次方,作者认为16是一个折中、使用较多的值,所以设置成16。

21. HashMap为什么容量是2的n次方
  • 因为HashMap计算hash值后,要用hash%length来确定桶的位置,由于取模的消耗较大,所以采用hash&(length-1)这个方法,而hash&(length-1)==hash%length成立的前提是当length是2的幂次方。
  • 因为2的n次方实际就是1后面n个0,2的n次方-1,实际就是n个1。 所以保证容积是2的n次方,是为了保证hash值和(length-1)之间进行按位与操作时,每一位与的都是1 ,也就是和1111…111进行与运算。这样就可以保证均匀分布而且不同位置不碰撞了。
22. 你一般用什么作为HashMap的key?
  • key可以为null,hash值为0,放在数组的第一个位置。
  • 一般用Integer、String这种不可变类当HashMap的key,String最为常用。原因有两点:
    (1)因为字符串是不可变的,所以在它创建的时候hashcode就被缓存了,不需要重新计算,这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。所以HashMap中的键往往都使用的字符串。
    (2)因为获取对象的时候要用到equals()和hashCode()方法,那么重写这两个方法是非常重要的,而Integer和String这些类已经很规范的重写了hashCode()以及equals()方法。
23. 用可变类当HashMap的key有什么问题

  hashcode可能发生改变,导致put进去的值,无法get出来。

24. HashMap如何转成线程安全的?
  • 替换成Hashtable,Hashtable通过对整个表上锁实现线程安全,效率比较低。
  • 使用Collections类的synchronizedMap方法将hashMap包装一下。
  • 使用ConcurrentHashMap,它使用分段锁来保证线程安全
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
对于MD5加密和HashMap,这两个概念是不相关的。MD5是一种常用的哈希算法,用于将任意长度的数据转换为固定长度的哈希值。而HashMap是Java中的一种数据结构,用于存储键值对。 如果你想要使用MD5对HashMap中的值进行加密,你需要遍历HashMap中的每个值,将其转换为字符串,然后使用MD5算法对字符串进行加密。下面是一个示例代码: ```java import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.HashMap; import java.util.Map; public class Main { public static void main(String[] args) { // 创建一个HashMap Map<String, String> hashMap = new HashMap<>(); hashMap.put("key1", "value1"); hashMap.put("key2", "value2"); hashMap.put("key3", "value3"); // 对HashMap中的值进行MD5加密 Map<String, String> encryptedHashMap = new HashMap<>(); try { MessageDigest md = MessageDigest.getInstance("MD5"); for (Map.Entry<String, String> entry : hashMap.entrySet()) { String key = entry.getKey(); String value = entry.getValue(); byte[] valueBytes = value.getBytes(); byte[] encryptedBytes = md.digest(valueBytes); StringBuilder sb = new StringBuilder(); for (byte b : encryptedBytes) { sb.append(String.format("%02x", b)); } String encryptedValue = sb.toString(); encryptedHashMap.put(key, encryptedValue); } } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } // 打印加密后的HashMap for (Map.Entry<String, String> entry : encryptedHashMap.entrySet()) { System.out.println(entry.getKey() + ": " + entry.getValue()); } } } ``` 这段代码首先创建了一个HashMap,并向其中添加了一些键值对。然后,使用MD5算法对HashMap中的值进行加密,并将加密后的值存储在另一个HashMap中。最后,打印加密后的HashMap

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值