java基础 和 map相关面试题

198 阅读9分钟

1.HashMap

image.png

image.png

image.png

- 1.HashMap原理

jdk 8之后使用的结构是数据+链表+红黑树,底层维护了一个node类型的数组table,使用的hash算法(散列算法),hashcode:通过字符串算出他的ascii码,然后对数组长度进行取模,算出哈希表中的下标进行存放。

image.png

- 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频繁的扩容,会造成底部红黑树不断的进行拆分和重组,这是非常耗时的。因此,也就是链表长度比较长的时候转变成红黑树才会显著提高效率。

image.png 看到这里就不难明白了,红黑树虽然查询效率比链表高,但是结点占用的空间大,只有达到一定的数目才有树化的意义,这是基于时间和空间的平衡考虑

- 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.建立一个公共溢出区

image.png

- 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

image.png

4.接口与抽象类区别

抽象类里面可以实现普通函数,而接口里面函数都是abs修饰的
抽象类可以是很多类型的,而接口只能是public static final
抽象类只能继承一个,而接口可以实现多个

image.png

5.list和set区别

list有序的可重复的,按照元素进入顺序排,允许多个null值,可以用迭代器取值,也可以通过get(i)下标取值
set是无序的不可重复的,最多只能允许一个null值,只能用迭代器取值

LinkedHashSet

image.png

image.png

6.Arraylist与Linkedlist区别

image.png

image.png

7.sleep wait yield join区别

sleep是thread的静态本地方法,调用sleep后线程或进入阻塞状态,让出cpu,如果有锁的话不释放锁对象,其他线程无法进入

wait是object的本地方法,调用wait会使该线程进入等待池,释放锁,通过notify或notifyall唤醒线程

yield 进行就绪状态,释放cpu执行权,但是依然持有cpu执行资格,下次重新分配依然可以分到cpu执行

join进入阻塞状态 ,假如线程B调用线程a,那么b进入阻塞状态,直到a线程执行完成

image.png

8.对线程安全的理解:

image.png

9.threadlocal内存泄漏的问题

image.png