JAVA面试题库-ArrayList和HashMap相关面试题(盲区)

1.ArrayList相关面试题

1.1 ArrayList是⼀个什么东西?可以用来干嘛?

ArrayList就是数组列表,主要用来装载数据。
当我们装载的是基本类型的数据int,long,boolean,short,byte…的时候我们只能存储他们对应的包装类。
它的主要底层实现是数组Object[] elementData。
相比于LinkedList,它的特点为:查询效率⾼,增删效率低,线程不安全。使⽤频率很⾼。

1.2 为啥线程不安全还使用他呢?

这其实问到的还是这几个集合的区别。
因为我们正常使⽤的场景中,都是⽤来查询,不会涉及太频繁的增删,如果涉及频繁的增删,可以使⽤LinkedList,如果你需要线程安全就使⽤Vector,这就是三者的区别了,实际开发过程中还是ArrayList使
⽤最多的。
不存在⼀个集合⼯具是查询效率⼜⾼,增删效率也⾼的,还线程安全的,⾄于为啥⼤家看代码就知道了,因为数据结构的特性就是优劣共存的,想找个平衡点很难,牺牲了性能,那就安全,牺牲了安全那
就快速。

1.3 既然底层实现是数组,但是数组的大小是定长的,如果我们不断的往里面添加数据的话,不会有问题吗?

这其实考的是ArrayList的底层实现,包括增删。。
ArrayList可以通过构造方法在初始化的时候指定底层数组的大小。
通过⽆参构造方法的方式ArrayList()初始化,则赋值底层数Object[] elementData为⼀个默认空数组,数组容量为0,只有真正对数据进⾏添加add时,才分配默认DEFAULT_CAPACITY = 10的初始容量。
至于为什么是10,不予深究,如果面试问,就说他爸爸规定的。(其实是调研之后觉得10比较圆满而已)
如果新增的时候,长度大于初始容量10,则会重新定义⼀个⻓度为10+10/2的数组也就是新增⼀个⻓度为15的数组。
然后把原数组的数据,原封不动的复制到新数组中,这个时候再把指向原数的地址换到新数组,ArrayList就这样完成了⼀次改头换⾯。
增:
他有指定index新增,也有直接新增的,在这之前他会有⼀步校验⻓度的判断,就是说如果⻓度不够,是需要扩容的。在扩容的时候,⽼版本的jdk和8以后的版本是有区别的,8之后的效率更⾼了,采⽤了位运算,右移⼀位,其实就是除以2这个操作。1.7的时候3/2+1 ,1.8直接就是3/2。
指定位置新增:
就是数组的copy,在index的位置去新增⼀个元素,他复制了⼀个数组,是从index的位置开始的,然后把它放在了index+1的位置,这样就可以把这个元素放到index这个位置了。
删:
删除其实跟新增是⼀样的,不过叫是叫删除,但是在代码⾥⾯我们发现,他还是在copy⼀个数组。
如果我们需要删除index这个位置的元素,那代码他就复制⼀个index+1开始到最后的数组,然后把它放到index开始的位置,这样index这个位置的元素就被覆盖了,但是给了你被删除的感觉。
同理他的效率也低,因为数组如果很⼤的话,⼀样需要复制和移动的位置就⼤了。

1.4 ArrayList的遍历和LinkedList遍历性能比较如何?

论遍历ArrayList要比LinkedList快得多,ArrayList遍历最⼤的优势在于内存的连续性,CPU的内部缓存结构会缓存连续的内存片段,可以⼤幅降低读取内存的性能开销

--------------------------------------------------------------------------------------

2.HashMap相关面试题

2.1 HashMap的结构原理?

HashMap是我们⾮常常⽤的数据结构,由数组链表组合构成的数据结构。
⼤概如下,数组⾥⾯每个地⽅都存了Key-Value这样的实例,因为他本身所有的位置都为null,在put插⼊的时候会根据key的hash去计算⼀个index值。

2.2 刚刚提到了链表,为啥需要链表,链表又是怎么样子的呢?

这首先要回忆,哈希hash是什么?
哈希是一种函数,它把任何数字或者字符串输入,通过散列算法,转化成一个固定长度的输出。通过输出我们不可能反向推得输入,除非尝试了所有的可能的输入值。
然后我们再来说链表
我们都知道数组长度是有限的,在有限的长度⾥⾯我们使⽤哈希,哈希本身就存在概率性,就是”九日日“和”日日九“我们都去hash有⼀定的概率会⼀样,这种极端情况会把两个不容的输入值hash到⼀个值上,那就形成了链表。

2.3 说到链表,新的Entry节点在插入链表的时候,是怎么插入的呢?

java8之前是头插法,就是说新来的值会取代原有的值,原有的值就顺推到链表中去,因为写这个代码的作者认为后来的值被查找的可能性更⼤⼀点,提升查找的效率。
但是,在java8之后,都是所用尾部插⼊了。
为什么用尾部插入?
这就属于盲区了,一般情况也不会问这么细吧。。
使⽤头插会改变链表的上的顺序,但是如果使⽤尾插,在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题了。
Java7在多线程操作HashMap时可能引起死循环,原因是扩容转移后前后链表顺序倒置,在转移过程中修改了原来链表中节点的引⽤关系。
Java8在同样的前提下并不会引起死循环,原因是扩容转移后前后链表顺序不变,保持之前节点的引⽤
关系。
因为java8之后链表有红⿊树的部分,⼤家可以看到代码已经多了很多if else的逻辑判断了,红⿊树的引⼊巧妙的将原本O(n)的时间复杂度降低到了O(logn)。

2.4 那是不是意味着Java8就可以把HashMap⽤在多线程中呢?

我认为即使不会出现死循环,但是通过源码看到put/get⽅法都没有加同步锁,多线程情况最容易出现的就是:⽆法保证上⼀秒put的值,下⼀秒get的时候还是原值,所以线程安全还是⽆法保证。

2.5 HashMap的扩容机制

其实数组容量是有限的,数据多次插⼊的,到达⼀定的数量就会进⾏扩容,也就是resize,影响因素有两个,一个是Capacity:HashMap当前⻓度。另一个是LoadFactor:负载因⼦,默认值0.75f。

2.6 HashMap的默认初始化⻓度是多少?

初始化⼤⼩是16
为什么应该没人问,如果有的话。。。
我们在创建HashMap的时候,阿⾥巴巴规范插件会提醒我们最好赋初值,而且最好是2的幂。
这样是为了位运算的⽅便,位与运算⽐算数计算的效率⾼了很多,之所以选择16,是为了服务将Key映射到index的算法

2.7 为啥我们重写equals⽅法的时候需要重写hashCode方法呢?

因为在java中,所有的对象都是继承于Object类。Ojbect类中有两个方法equals、hashCode,这两个⽅法都是⽤来比较两个对象是否相等的。
在未重写equals⽅法我们是继承了object的equals⽅法,那⾥的 equals是⽐较两个对象的内存地址,显然我们new了2个对象内存地址肯定不⼀样
(对于值对象,==比较的是两个对象的值;对于引⽤对象,比较的是两个对象的地址)
HashMap是通过key的hashCode去寻找index的,那index⼀样就形成链表了,我们去get的时候,他就是根据key去hash然后计算出index,那我怎么怎么找到具体的值呢,用equals,所以如果我们对equals⽅法进⾏了重写,建议⼀定要对hashCode⽅法重写,以保证相同的对象返回相同的hash值,不同的对象返回不同的hash值。不然⼀个链表的对象,你哪⾥知道你要找的是哪个,毕竟hashCode都⼀样。

2.8 怎么处理HashMap在线程安全的场景?

⼀般都会使⽤HashTable或者ConcurrentHashMap,但是因为前者的并发度的原因基本上没啥使⽤场景了,所以存在线程不安全的场景我们都使⽤的是ConcurrentHashMap。
HashTable很简单粗暴,直接在⽅法上锁,并发度很低,最多同时允许⼀个线程访问,ConcurrentHashMap就好很多了,1.7和1.8有较⼤的不同,不过并发度都比前者好太多了。

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值