Java类集高频面试题以及详解(必问)
类集在面试中被问到的概率非常高,下面是我整理的有关类集方面的高频面试点。
1.ArrayList、Vector、LinkedList的关系与区别
- ArrayList、Vector、LinkedList都属于List接口的常用子类,其中ArrayList、Vector底层基于数组实现,LinkedList基于链表实现
- Vector出现版本JDK1.0;ArrayList和LinkedList出现版本JDK1.0
- Vector支持较老的迭代器Enumeration,而ArrayList不支持
- ArrayList采用懒加载策略,第一次向集合中添加元素时,初始化对象数组容量为10;扩容策略为原先数组的1.5倍;线程不安全,性能较高;适合在频繁查找以及尾部的插入场景下使用ArrayList
- Vector在产生对象时就初始化内部数组(默认大小为10);扩容策略为原先数组的2倍;采用synchronized同步方法,线程安全,性能很低(读读互斥)
- LinkedList采用异步处理,线程不安全;适用于频繁在任意位置进行元素的插入与删除
2.jcl中fail-fast机制以及fail-safe机制?
-
什么是快速失败策略?
优先考虑出现异常的情况,当异常产生时,直接抛出异常(ConcurrentModificationException),程序终止。
-
如何抛出ConcurrentModificationException异常?
modCount记录当前集合修改的次数,exceptedModCount记录获取当前集合迭代器的修改次数,当modCount != expectedModCount时,就会抛出该异常。但是在一开始的时候, expectedModCount初始值默认等于modCount,并且expectedModCount在整个迭代过程除了一开始赋予初始值modCount外,并没有再发生改变,发生改变的只有modCount,当另一个线程(并发修改)或者同一个线程遍历过程中,调用相关方法使集合的个数发生改变,就会使modCount(modCount ++)发生变化,这样就会抛出ConcurrentModificationException异常。
-
如何解决fail-fast?
1)遍历不要修改集合内容;
2)使用迭代器内部的删除方法;
3)使用fail-safe集合 -
fail-safe集合?
juc包下的(ConcyrrentHashMap、CopyOnWriteArrayList)都属于fail-safe -
fail-fast意义?
避免多线程场景下数据‘脏读’的问题,也就是说现在拿到的数据不是最新的所以禁止作!
3.Set与Map的关系?
- Set接口相当于穿了马甲的Map接口,本质上Set接口的子类都是使用Map来存储元素的,都是将元素存储到Map接口的key中,Set中的value都是用一个空的Object对象
4.hashCode()与equals()的区别?
- hashCode返回相等,equlas不一定相等(hash碰撞)
- equals返回相等,hashCode一定相等
5.Java中实现一个类的两个对象大小比较的方式(内部排序与外部排序的区别)?
- 在Java中,若想实现自定义类的比较,需要实现内部比较器Comparable接口或者外部比较器Comparator接口;
- Comparable是排序接口,若一个类实现了Comparable接口,意味着该类支持排序,是一个内部比较器接口(自身去和别人比);
- Comparator接口是比较器接口,类的本身不支持排序(类本身没有实现Comparable接口),专门有若干个第三方的比较器(实现了Comparator接口的类)来进行类的排序,是一个外部比较器(策略模式)
- 实现Comparator接口进行第三方排序,这种方式更灵活,可以轻松改变策略进行第三方的排序算法
6.HashMap、TreeMap、Hashtable的关系与区别?
- HashMap、TreeMap、Hashtable都是Map的常用子类,HashMap底层基于哈希表+红黑树(JDK1.8之后),Hashtable基于哈希表,TreeMap基于红黑树;
- HashMap采用懒加载策略,采用异步处理,线程不安全,性能较高
- Hashtable产生对象时初始化内部哈希表(默认大小为16),采用synchronized同步方法,线程安全,性能很低(读读互斥)
- HashMap中键值对都允许为null
TreeMap中键不能为null,值可以为null
Hashtable中键与值都不能为null
7.HashMap内部源码分析(负载因子、树化策略、扩容策略、内部哈希算法)
-
负载因子:float DEFAULT_LOAD_FACTOR = 0.75f
树化阈值:int TREEIFY_THRESHOLD = 8
解树化阈值:int UNTREEIFY_THRESHOLD = 6 -
树化逻辑:当前桶中链表的长度大于等于8并且哈希表长度大于等于64开始树化,否则只是简单的扩容处理
-
解树化逻辑:当红黑树个数在扩容或者删除时个数小于等于6,在下一次resize过程中会将红黑树退化成链表来节省空间
-
为何引入红黑树?
为了防止链表过长而导致查找性能急剧降低,引入红黑树后时间复杂度从O(n)优化成O(log2^n) -
为什么负载因子为0.75?
根据科学的统计计算得出0.75是最合适的,如果负载因子>0.75,会增加了哈希表的利用率,哈希冲突概率明显增加;如果负载因子<0.75,会降低哈希表的利用率,导致扩容频繁。 -
为何不直接使用HashCode方法?
返回值普遍较大,需要开辟大量空间,浪费资源 -
为何哈希表长度必须为2^n?
保证哈希表中所有索引位置都有可能被访问到 -
为何源码中h>>>16?
取出高16位参与运算是因为有些数据计算出的哈希值主要的差异是在高位,而HashMap中哈希寻址是忽略高位的,这种策略可以有效避免哈希碰撞
8.ConcurrentHashMap如何高效实现线程安全?
-
在ConcurrentHashMap没有出现以前,jdk使用hashtable来实现线程安全,但是Hashtable是将整个hash表锁住,所以效率很低下。
-
ConcurrentHashMap将数据分别放到多个Segment中,默认16个,每一个Segment中又包含了多个HashEntry列表数组,
-
对于一个key,需要经过三次hash操作,才能最终定位这个元素的位置,这三次hash分别为:
先进行一次hash操作,得到hash值h1,也即h1 = hash1(key);
将得到的h1的高几位进行第二次hash,得到hash值h2,也即h2 =hash2(h1高几位),通过h2能够确定该元素的放在哪个Segment;
将得到的h1进行第三次hash,得到hash值h3,也即h3 = hash3(h1),通过h3能够确定该元素放置在哪个HashEntry。 -
每一个Segment都拥有一个锁,当 进行写操作时,只需要锁定当前操作的Segment,而其它Segment中的数据是可以访问的。
9.JDK1.7与JDK1.8ConcurrentHashMap的设计区别?
- JDK1.7中Segment是ReentrantLock的子类,将HashTable整张表加锁,一把锁优化为16个Segment,锁的是当前的Segment,不同的Segment之间还是异步,Segment初始化为16后不可再次扩容
- JDK1.8中ConcurrentHashMap锁进一步细化,结构类似于HashMap,锁的是当前桶的头节点,锁的个数进一步提升,也就是说,锁会随着哈希表扩容而增加,支持并发线程的数量进一步提升,内部使用CAS和synchronized来保证线程安全