1.HashMap
- 1.HashMap原理
jdk 8之后使用的结构是数据+链表+红黑树,底层维护了一个node类型的数组table,使用的hash算法(散列算法),hashcode:通过字符串算出他的ascii码,然后对数组长度进行取模,算出哈希表中的下标进行存放。
- 2.HashMap中put()如何实现的
-
jdk 7中 首先检查大小,看是否需要扩容(默认元素超过最大值的0.75时扩容),如果需要扩容就进行扩容, jdk8中先插入元素,在判断是否需要扩容
-
然后计算出key的hashcode,根据hashcode定位数值所在的bucketIndex
-
如果该位置上没有元素,就直接插入,结束
-
如果该位置上有元素就使用equal比较是否相同
-
如果key相同就把新的value替换旧的value,结束
-
如果key不同,就继续遍历,找到根节点,如果没找到key的话,就构造一个新的节点,然后把节点插入到链表尾部,表示put成功(jdk 1.8 之后链表长度超过阈值就会转化为红黑树)
- 3.HashMap中get()如何实现的
对输入的key的值计算hash值, 首先判断hashmap中的数组是否为空和数组的长度是否为0,
如果为空和为0,则直接放回null
如果不为空和0,计算key对应的数组下标,判断对应位置上的第一个node是否满足条件,
如果满足条件,直接返回
如果不满足条件,判断当前node是否是最后一个,
如果是,说明不存在key,则返回null
如果不是最后一个,判断是否是红黑树,
如果是红黑树,则使用红黑树的方式获取对应的key,
如果不是红黑树,遍历链表是否有满足条件的,如果有,直接放回,否则返回null
- 4.为什么HashMap线程不安全
HashMap的线程不安全主要体现在下面两个方面:
1.在JDK1.7中,当并发执行扩容操作时会造成环形链和数据丢失的情况。
2.在JDK1.8中,在并发执行put操作时会发生数据覆盖的情况。
- 5.HashMap1.7和1.8有哪些区别
(1)JDK1.7用的是头插法,而JDK1.8及之后使用的都是尾插法,那么他们为什么要这样做呢?因为JDK1.7是用单链表进行的纵向延伸,当采用头插法时会容易出现逆序且环形链表死循环问题。但是在JDK1.8之后是因为加入了红黑树使用尾插法,能够避免出现逆序且链表死循环的问题。
(2)扩容后数据存储位置的计算方式也不一样:1. 在JDK1.7的时候是直接用hash值和需要扩容的二进制数进行&(这里就是为什么扩容的时候为啥一定必须是2的多少次幂的原因所在,因为如果只有2的n次幂的情况时最后一位二进制数才一定是1,这样能最大程度减少hash碰撞)(hash值 & length-1)
(3)在JDK1.7的时候是先扩容后插入的,这样就会导致无论这一次插入是不是发生hash冲突都需要进行扩容,如果这次插入的并没有发生Hash冲突的话,那么就会造成一次无效扩容,但是在1.8的时候是先插入再扩容的,优点其实是因为为了减少这一次无效的扩容,原因就是如果这次插入没有发生Hash冲突的话,那么其实就不会造成扩容,但是在1.7的时候就会急造成扩容
(4)而在JDK1.8的时候直接用了JDK1.7的时候计算的规律,也就是扩容前的原始位置+扩容的大小值=JDK1.8的计算方式,而不再是JDK1.7的那种异或的方法。但是这种方式就相当于只需要判断Hash值的新增参与运算的位是0还是1就直接迅速计算出了扩容后的储存方式。
- 6.解决hash冲突的时候,为什么用红黑树,以及产生冲突时定位目标值
java8不是用红黑树来管理hashmap,而是在hash值相同的情况下(且重复数量大于8),用红黑树来管理数据。 红黑树相当于排序数据,可以自动的使用二分法进行定位,性能较高。一般情况下,hash值做的比较好的话基本上用不到红黑树。
首先计算hash值,定位桶位,判断桶中元素类型是Node还是TreeNode,如果是Node表明是链表,如果是TreeNode为红黑树结构,链表直接遍历,红黑树直接查找。
- 7.红黑树的效率高,为什么一开始不用红黑树存储
(1)put和remove过程中,红黑树要通过左旋,右旋、变色这些操作来保持平衡,另外构造红黑树要比构造链表复杂,在链表的节点不多的时候,从整体的性能看来, 数组+链表+红黑树的结构可能不一定比数组+链表的结构性能高。就好比杀鸡焉用牛刀的意思。
(2)HashMap频繁的扩容,会造成底部红黑树不断的进行拆分和重组,这是非常耗时的。因此,也就是链表长度比较长的时候转变成红黑树才会显著提高效率。
看到这里就不难明白了,红黑树虽然查询效率比链表高,但是结点占用的空间大,只有达到一定的数目才有树化的意义,这是基于时间和空间的平衡考虑。
- 8.不用红黑树,用二叉查找树可以不
1.红黑树不追求"完全平衡",即不像AVL那样要求节点的 |balFact| <= 1,它只要求部分达到平衡,但是提出了为节点增加颜色,红黑是用非严格的平衡来换取增删节点时候旋转次数的降低,任何不平衡都会在三次旋转之内解决,而AVL是严格平衡树,因此在增加或者删除节点的时候,根据不同情况,旋转的次数比红黑树要多。
就插入节点导致树失衡的情况,AVL和RB-Tree都是最多两次树旋转来实现复衡rebalance,旋转的量级是O(1)
删除节点导致失衡,AVL需要维护从被删除节点到根节点root这条路径上所有节点的平衡,旋转的量级为O(logN),而RB-Tree最多只需要旋转3次实现复衡,只需O(1),所以说RB-Tree删除节点的rebalance的效率更高,开销更小!
2. AVL更平衡,结构上更加直观,时间效能针对读取而言更高;维护稍慢,空间开销较大。
3. 红黑树,读取略逊于AVL,维护强于AVL,空间开销与AVL类似,内容极多时略优于AVL,维护优于AVL。*
- 9.为什么阀值是8才转为红黑树
红黑树中的TreeNode是链表中的Node所占空间的两倍,虽然红黑树的查找效率为O(logN),要优于链表的O(N),但是当链表长度比较小的时候,即使全部遍历后时间复杂度也不会太高。所以要寻找一种,时间和空间的平衡,即在链表长度达到一个阈值后再转为红黑树。那么为什么HashMap红黑树的阈值为什么是8呢?
首先和hashcode碰撞次数的泊松分布有关。在负载因子0.75(HashMap默认)的情况下,单个hash槽内元素个数为8的概率小于百万分之一,大于8时转为红黑树,小于等于6时转为链表。而原作者在选择链表元素个数时选择了8是根据概率统计而选择的。 之所以是8,是因为Java源码的贡献者在进行大量实验发现,hash碰撞发生8次的概率已经降到了0.00000006,几乎为不可能事件,如果真的碰撞发生了8次,那么这个时候说明由于元素本身和hash函数的原因(用户自己实现hash函数有误),此次操作的hash碰撞的可能性非常大了,后续还有可能会继续发生hash碰撞。所以,这个时候就应该将链表转换为红黑树了。
- 10.为什么退化为链表的阈值是6
主要是一个过渡,避免链表和红黑树之间频繁的转换。如果阈值是7的话,删除一个元素红黑树就必须退化为链表,增加一个元素就必须树化,来回不断的转换结构无疑会降低性能,所以阈值才不设置的那么临界。
- 11.hash冲突有哪些解决办法
1.开放定址法(线性探测再散列,二次探测再散列,伪随机探测再散列)
2.再哈希法
3.链地址法(Java hashmap就是这么做的)
4.建立一个公共溢出区
- 12.HashMap在什么条件下扩容
当哈希表的size达到阀值(就是当前table值的0.75倍(负载因子为0.75))或者当某一个table里的某个节点为链表且大于7并且当前table没有超过64,那么会进行扩容
- 13.HashMap中hash函数怎么实现的,还有哪些hash函数的实现方式
高16 bit 不变,低16 bit 和高16 bit 做了一个异或(得到的 hashcode 转化为32位二进制,前16位和后16位低16 bit和高16 bit做了一个异或) (n·1) & hash = -> 得到下标
- 14.为什么不直接将hashcode作为哈希值去做取模,而是要先高16位异或低16位
因为如果算出来的hashcode值过大,比如hashcode是10000,那么在hash表就得是个大小为10001的数组才能放下,这会大量浪费内存,为了避免这种情况,进行取模运算,这样对应的hash表的size较小,节省内存并且不易产生hash冲突
- 15.为什么扩容是2的次幂
我们可以看到它求hash的过程,将32位的hashCode值向右移动16位,高位补0,也就是只要了高16位,这是为什么呢?因为hashcode的计算方法导致哈希值的差异主要在高位,而 (n - 1) & hash是忽略了容量以上的高位的,所以 使用h >>>16就是为了避免类似情况的哈希冲突
- 16.链表的查找的时间复杂度是多少
O(n)
- 17.红黑树
红黑树,一种二叉查找树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。
通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的
【1】性质1. 节点是红色或黑色。
【2】性质2. 根节点是黑色。
【3】性质3 每个叶节点是黑色的。
【4】性质4 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
【5】性质5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
其他基础相关
1.==与equals区别
==对比的是栈中的值,基本类型的话比的是大小,引用类型比的是堆中的对象地址
equals 对比的是内存空间的值是否相同
2.为什么匿名内部类和局部内部类只能访问被final修饰的变量
因为要保证变量一致,其实内部类里面拿到的只是外部类变量的一个copy对象,如果内部类在使用变量时发生了改变,可是外部类并没有重新赋值,这就造成了变量不一致,所以直接用final修改变量,使其不可改变只能被使用
3.string stringBuffer stringBulider区别
string是final修饰的,每次创建都是重新生成一个对象 stringbuffer和stringbulider都是在原对象上进行操作,不会重新创建对象 stringbuffer是线程安全的,stringbulider是线程不安全的 stringbuffer是用syn修饰的 性能上stringbulider > stringbuffer >string
如果一个变量经常变动,那么考虑使用stringbuffer和stringbulider,如果是多线程,使用stringbuffer
4.接口与抽象类区别
抽象类里面可以实现普通函数,而接口里面函数都是abs修饰的
抽象类可以是很多类型的,而接口只能是public static final
抽象类只能继承一个,而接口可以实现多个
5.list和set区别
list有序的可重复的,按照元素进入顺序排,允许多个null值,可以用迭代器取值,也可以通过get(i)下标取值
set是无序的不可重复的,最多只能允许一个null值,只能用迭代器取值
LinkedHashSet
6.Arraylist与Linkedlist区别
7.sleep wait yield join区别
sleep是thread的静态本地方法,调用sleep后线程或进入阻塞状态,让出cpu,如果有锁的话不释放锁对象,其他线程无法进入
wait是object的本地方法,调用wait会使该线程进入等待池,释放锁,通过notify或notifyall唤醒线程
yield 进行就绪状态,释放cpu执行权,但是依然持有cpu执行资格,下次重新分配依然可以分到cpu执行
join进入阻塞状态 ,假如线程B调用线程a,那么b进入阻塞状态,直到a线程执行完成