集合是Java面试必考的高频知识点,虽然大家平时多少都会在用,但是对这个体系和底层实现很少有较全面的认知。
本文参考了《极客时间-Java核心技术36讲》和《牛客网-Java开发岗高频面试题全解析》,在较为系统的复习之后总结于此。希望对同样的正在准备找工作的同学有所帮助
首先,放上一张整体分类图,从图中可以看出,Java的集合类起源于两个接口,一个是Map
,另一个是Collection
。平时我们所能接触到的包括HashMap,ArrayList,HashSet
等都直接或间接的实现了这两个接口中的一个。而Collection
接口的分支包括了List
接口和Set
接口。这样一来,我们常见的Map,List
和Set
就都包含到了。
说到集合肯定大家都知道Collection
接口,但是不一定知道Map
接口是和Collection
接口平级的,所以Map
接口不是Collection
接口的子接口。
下面我们就以Map、Set、List
作为三个分支来进行讲解
Map
接口的实现类主要有:HashMap、TreeMap、LinkedHashMap、Hashtable
、ConcurentHashMap
和Properties
等Set
接口的实现类主要有:HashSet、TreeSet、LinkedHashSet
等List
接口的实现类主要有:ArrayList、LinkedList、Stack、Vector
等为了能更清楚的了解下他们之间的关系,我找了两张结构的图也放到这里
Collection
接口及其分支的结构Map
接口及其分支的结构同一类下的各个实现类不但名字有点相似,功能也都大体一致,所以同一类型的实现类之间的比较就是面试的热点,下面,我们就把常用的分成几组进行比较吧
Hashtable
的Key
和Value
均不能为nullHashMap
的Key
和Value
允许为空,允许有一个null的Key
和多个null的Value
TreeMap
的Value
允许为空Comparator
接口时,key 不可以为nullComparator
接口时,若未对 null 情况进行判断,不能为空;若针对null实现了,可以为空,但是不能通过get()方法获取,只能遍历获得值。Hashtable
和HashMap
是无序的TreeMap
是有序的,TreeMap
由红黑树实现,默认是升序排列,可自定义实现Comparator
接口实现排序方式Hashtable
初始化默认容量为11,且不要求底层数组容量为2的整数次幂。HashMap
初始化默认容量为16,要求容量一定为2的整数次幂TreeMap
底层实现是红黑树,就不需要初始化默认大小和扩容了Hashtable
的方法函数全部都是同步的(synchronized
实现),保证了线程安全性。当多线程访问Hashtable
时,效率极低,所以Hashtable
现在已经不推荐使用了HashMap
和TreeMap
不支持线程同步,所以是线程不安全的HashMap
线程不安全主要是考虑到了多线程环境下进行扩容可能会出现HashMap
死循环Hashtable
的线程安全是由于内部实现put
和remove
操作时使用synchronized
进行了同步,所以单个方法线程安全。而HashMap
并没有,所以当一个线程执行remove
操作时另一个线程执行get操作就可能出现越界现象LinkedHashMap
基于HashMap和双向链表实现,TreeMap
基于红黑树实现TreeMap
的有序是指基于Key
进行内部排序LinkedHashMap
的有序分为插入顺序和访问顺序,默认插入顺序Hashtable
通过synchronized
锁住整个结构实现线程同步,效率低ConcurrentHashMap
采用分段锁实现线程同步,效率明显提高ConcurrentHashMap是面试的重点,篇幅较大,所以单独成篇
直达链接
HashMap
的底层实现数据结构为数组+链表形式HashMap
的底层实现采用数组+链表+红黑树的形式,有效的解决了链表太长导致的查询速度变慢问题HashMap
的初始容量是16HashMap
的加载因子是0.75HashMap
的扩容增量是1倍因为Java要求的是2的整数幂值
表示当存储容量超过当前容量的0.75倍事触发扩容,如第一次扩容当超过16*0.75=12之后,就开始扩容
这其实是出于容量和性能之间平衡的结果:
所以综合了以上情况就取了一个 0.5 到 1.0 的平均数 0.75 作为加载因子
指每次扩容的容量是原来容量的一倍,即容量每次翻倍
2的N次幂有助于减少碰撞的几率,空间利用率大
对上面这句话做一个解释:
HashMap
中,通过key
的hash
值与length-1
进行&运算,实现了key
定位。length
为2的N次幂时,length-1
的二进制为111…(全1)的形式,在于hash
值进行&操作时会很快,而且没有空间浪费length
不是2的N次幂时,如length=15
,那么length-1=14
,二进制值为1110,进行&操作时,0001、0011、0101、0111、1011、1001、1101这几个位置就不能存放值,空间浪费巨大!!!put()
方法传递键值对进行存储时,先对键调用hashCode
()的方法,返回bucket
的位置存储实体对象,当bucket
位置发生冲突,也就是发生hash冲突时,会在每个bucket
后面接上一个链表(JDK8及之后的版本如果冲突数超过MIN_TREEIFY_CAPACITY
会使用红黑树)来解决,将新存储的键值对放在表头(bucket
)中get()
方法时,首先根据hashCode
()获取到bucket
的值,然后再通过equals
方法在链表或者红黑树中查找HashMap进行添加的流程如图:
HashMap
里面默认的负载因子大小为0.75,也就是说,当Map中的元素个数(包括数组,链表和红黑树中)超过了16*0.75=12之后开始扩容。将会创建原来HashMap
大小的两倍的bucket
数组,来重新调整map
的大小,并将原来的对象放入新的bucket
数组中。这个过程叫作rehashing
,因为它调用hash方法找到新的bucket
位置。在JDK 8中,新的下标位置等于原下标位置 + 原数组长度
但是要注意,在JDK 7 环境下,在多线程环境下,HashMap
扩容会导致死循环
在JDK7中,扩容会调用transfer方法,代码如下
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry e : table) {
while (null != e) {
Entry next = e.next; // 第5行 关键步骤1
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i]; //关键步骤2
newTable[i] = e; //关键步骤3
e = next; //关键步骤4
}
}
}
创建两个线程A,B,当线程A执行完了关键步骤1之后,失去时间片,线程B获得时间片完成扩容,然后当线程A再次获得时间片时,就会出现死循环现象。
分析:
单线程或者线程不冲突情况下,正常扩容如图所示
并发情况下
当线程A执行完关键步骤1之后,将e指向了a、e.next指向了b
此时失去时间片,由线程B完成了扩容
当线程A再次获得时间片时,扩容实际已经完成了,但是e依然指向a、e.next指向了b,于是继续进行扩容,于是在a与b之间不断的发生循环,造成死循环
ps:此处图片引自知乎
key
的hash
地址p=hase(key)
出现冲突时,以某种探测技术递归的寻找空白地址,直到找到返回。如:以p为基础再产生另一个hash
地址p1,如果p1也冲突了就继续找直到找到hash
地址相同的构成一个单链表,单链表的头指针放在hash
表的第i个单元中。JDK8之前的HashMap
就是使用的拉链法String
和Integer
这样的包装类非常适合作为HashMap
的键。因为他们都是final
类型的类,而且重写了equals
和hashCode
方法,避免了键值对改写,有效提高HashMap
性能
hashCode 和 equals 的一些基本约定
equals
相等,hashCode
一定要相等hashCode
也要重写 equals
hashCode
需要保持一致性,状态改变返回的哈希值仍然要一致Comparable
实现比较简单,但是当需要重新定义比较规则的时候,必须修改源代码,即修改User类里边的compareTo
方法Comparator
接口不需要修改源代码,只需要在创建TreeMap
的时候重新传入一个具有指定规则的比较器即可Vector
和ArrayList
底层使用动态数组实现
LinkedList
底层使用双向链表实现,可当做堆栈、队列、双端队列使用
Vector
是线程安全的,效率较低,已经不推荐使用了,多线程环境下现在推荐使用CopyOnWriteArrayList
替代ArrayList
ArrayList
和LinkedList
是线程不安全的Vector
和ArrayList
当数组满的时候,会自动创建扩容后的数组,并拷贝原有数组数据
Vector
扩容增量是1倍ArrayList
扩容增量是50%LinkedList
采用双向链表不需要扩容Vector
和ArrayList
在随机存取方面效率高,适合随机访问较多的场景LinkedList
在节点的增删方面效率高,适合于频繁对节点进行操作的场景HashSet
底层使用hash表实现equals
方法,是否为trueHashSet
的contains
方法使用HashMap
得containsKey
方法实现TreeSet
底层使用了红黑树来实现,其实底层实现还是TreeSet,只是用来其中的KeyComparable
或者Comparator
接口实现Iterator
可以遍历list和set集合;ListIterator
只能用来遍历list集合Iterator
前者只能前向遍历集合;ListIterator
可以前向和后向遍历集合ListIterator
其实就是实现了前者,并且增加了一些新的功能Arrays.asList
方法实现,转换之后不可以使用add/remove
等修改集合的相关方法,因为该方法返回的其实是一个Arrays
的在这里插入代码片
内部私有的一个类ArrayList
,本质上还是一个数组List.toArray
实现,这里最好传入一个类型一样的数组,大小为list.size()
Collection
是一个顶层集合接口,其子接口包括List
和Set
;Collections
是一个集合工具类,可以操作集合,比如说排序,二分查找,拷贝集合,寻找最大最小值等。 总而言之:带s的大都是工具类。以上就是我目前所掌握的Java面试相关的题目,后续发现有没有写到的比较重要的再补充上来。
有问题可以给我留言,看到了会及时的进行回复
更多Java面试复习笔记和总结可访问我的面试复习专栏《Java面试复习笔记》,或者访问我另一篇博客《Java面试核心知识点汇总》查看目录和直达链接