1. 为什么需要哈希表, 哈希表是什么?Hash函数是什么?
- 数组和列表想要查询某个元素是否存在, 或者获取某个元素的位置效率低下
- 散列表(hash table)把key,value映射到表中的某个位置来访问记录,以加快查找速度
- hash函数是散列算法, 一种消息摘要
2. 解决hash碰撞的方法
int值是有限的,只有2^32个, 所以通过hash()计算的值必然会有重复, 此时会产生冲突,碰撞, 二维数组法会浪费大量的内存, 所以采用拉链法, jdk8后采用红黑树进一步优化
减少碰撞:
- 将key的哈希值与高16位换到低16位做异或运算;
- HashMap默认初始化长度为16,并且每次自动扩展或者是手动初始化容量时,必须是2的幂。
- 扰动函数
- 使用string或integer作为key(final修饰,并且重写了equal()hashCode()方法)
3. hashMap的特点
- 实现快速存取, key,value允许为空,key相同和覆盖,
- 底层是hash表, 不保证有序
- hashmap线程不安全
4. hashap底层实现
put()
1.计算关于key的hashcode值(与Key.hashCode的高16位做异或运算)
2.如果散列表为空时,调用resize()初始化散列表
3.如果没有发生碰撞,直接添加元素到散列表中去
4.如果发生了碰撞(hashCode值相同),进行三种判断
4.1:若key地址相同或者equals后内容相同,则替换旧值
4.2:如果是红黑树结构,就调用树的插入方法
4.3:链表结构,循环遍历直到链表中某个节点为空,尾插法进行插入,插入之后判断链表个数是否到达变成红黑树的阙值8;也可以遍历到有节点与插入元素的哈希值和内容相同,进行覆盖。
5.如果桶满了大于阀值,则resize进行扩容
resize()
当数组table的size达到阙值时即++size > load factor * capacity 时,重新分配一个新数组,新数组是老数组的2倍长,然后遍历整个老结构,把所有的元素挨个重新hash分配到新结构中去。
get()
对key的hashCode进行hashing,与运算计算下标获取bucket位置,如果在桶的首位上就可以找到就直接返回,否则在树中找或者链表中遍历找,如果有hash冲突,则利用equals方法去遍历链表查找节点
5. HashMap、HashTable是什么关系?
底层都是数组加链表实现,共有一个map接口 hashtable单独继承了一个dictionary抽象类, 并且线程安全 hashmap的kv可以为空而hashtable不可以,会出现空指针异常
6. HashMap的线程安全
HashMap线程不安全在哪?
-
死循环 多线程扩容中会产生死循环的现象,使用了头插法,并且链表反转,但是再jdk8中,对resize()方法做出了调整,头节点用一次尾插法
-
数据丢失/脏读 两个线程同时使用put()方法,且两个key指向同一个散列桶 ab取得相同的头节点,此时线程a操作头节点后,b在处理的头节点就会丢失或覆盖a操作的数据
如何规避HashMap的线程不安全?
集合工具类Collections里有关于集合包装成线程同步的方法,从而解决多线程并发下用hashmap的线程安全的问题
内部实现也很简单, 就是等同于hashtable, 只是对当前传入对象, 加了对象锁
使用ConcurrentHashMap这是hashmap线程安全的替代品,
使用了synchronized修饰符和CAS来保证并发安全, 数据结构跟HashMap1.8的结构类似synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍
自平衡的二叉查找树红黑树
- 节点是红色或黑色。
- 根节点是黑色。
- 每个叶子节点都是黑色的空节点(NIL节点)。
- 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
- 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
6. 有序的Map
HashMap 是不是有序的?无序的
有没有有序的Map实现类呢?有 TreeMap 和 LinkedHashMap
TreeMap 和 LinkedHashMap 是如何保证它的顺序的? TreeMap 是通过实现 SortMap 接口,能够把它保存的键值对根据 key 排序,基于红黑树,从而保证 TreeMap 中所有键值对处于有序状态。 LinkedHashMap 则是通过插入排序(就是你put的时候的顺序是什么,取出来的时候就是什么样子)和访问排序(改变排序把访问过的放到底部)让键值有序。