在之前的文章中,已经发布了常见的面试题,这里我花了点时间整理了一下对应的解答,由于个人能力有限,不一定完全到位,如果有解答的不合理的地方还请指点,在此谢过。
本文主要描述的是java面试中几乎必问的集合内容,java的集合是jdk里面重要的内容,也是我们平时开发过程中最常用到的,所以无论是否为了准备面试,我们都要掌握好集合相关的知识。既然是重点,那就意味着集合类在java面试中的题目会非常多,尤其是hashmap,由于其设计精美,细节优化突出,常常是面试中集合的首选,所以我会把hashmap单独使用一篇文章来记录。
能简单说明一下你了解的集合么?
Java的集合分为两大类,collection和map。Collection是一组数据集合类型,可以理解为动态数组。其下包含list和set两个大类,List和set的最大区别是是否可以有重复元素,list是一组有序的可重复的数组,set是一组无序的不可重复的数组。Map是存入键值对,是一种key-value的形式。Map底下有两个大的实现类treemap和hashmap。具体看下图为集合类中常用的集合图,在介绍的时候,我们一般介绍常用的就行。
说说ArrayList、linkedlist、vector的区别?
其实三者都是collection接口下面的,都是动态数组。具体的差异点如下表:
| ArrayList | Linkedlist | Vector |
底层实现 | 数组 | 链表 | 数组 |
线程安全 | 不安全 | 不安全 | 安全 |
使用频次 | 最高 | 较高 | 不使用 |
扩容 | 增加0.5倍 | 链表,没有扩容概念 | 增加1倍 |
Vector是早期jdk实现的线程安全的集合,其实现基本是在ArrayList的方法上加上了锁同步,导致性能下降,现在基本已经使用concurrent系列替换了。ArrayList和linkedlist的核心区别在于底层实现,ArrayList基于数组的实现存在扩容机制,同时数组可以随机访问0(1),如果是尾部添加元素的话也是o(1)。如果是中间某个位置添加元素的话,arraylist调用的是系统拷贝函数(System.arraycopy),会把后面的内容一次性全部往后挪一位,所以时间复杂度也是o(1),当然了删除也类似,这样添加和删除也基本实现了o(1),而linkedlist的优势在于随机位置的删除和添加,但是它在找到哪个位置的时候,是线性的,所以基本上linkedlist在访问和删除上已经没有什么优势了,扩容可能是它的唯一优势。没有什么特殊情况,我们一般都是使用arraylist,也很少使用linkedlist和vector。
说说hashset和treeset的区别?
其实这两者底层是hashmap和treemap的延伸,所以两种在区别上和hashmap和treemap一致。Hashset是以hash表为主要的存储结构,treeset底层是以红黑树为主要结构存储的。Hashset是以key的hashcode值和equals方法来确定顺序的,而treeset是通过Comparable(外部比较器)和Comparator(内部比较器)来指定顺序的。
如何在循环中安全的删除一个ArrayList的数据?
这是一个基本的操作了,直接给出正确的删除方法哈,如下:
public static void main(String[] args) throws Exception {
List<String> list = new ArrayList<>();
list.add("1");
list.add("1");
//使用for循环删除 删不全,删除不了,不可取
for (int i =0;i<list.size();i++){//删除之后,size为1,i也为1,所以直接退出循环
if (list.get(i).equals("1")){
list.remove(i);//删除一个之后,size变为1
}
}
//----------------------------------------
List<String> list1 = new ArrayList<>();
list1.add("1");
list1.add("1");
//1.8之前 使用迭代器删除 可以删除
Iterator<String> iterator = list1.iterator();
while (iterator.hasNext()){
if (iterator.next().equals("1")){
iterator.remove();
}
}
//-------------------------------------------------
List<String> list2 = new ArrayList<>();
list2.add("1");
list2.add("1");
//1.8 版本之后 使用removeIf 可以删除
list2.removeIf(s -> s.equals("1"));//底层实现就是迭代器
}
Jdk1.8之前使用迭代器,1.8之后使用removeif函数。
说说你理解的fail-fast和fail-safe?
fail-fast是java一种快速判断是否存在多线程操作集合的一种机制。在使用迭代器对集合进行遍历过程中,一旦发现容器的数据被修改,就会抛出concurrentModificationException异常导致遍历失败。常见于hashmap和ArrayList的实现中。我们看下ListItr是怎么判断是否有修改。
private class Itr implements Iterator<E> {
int expectedModCount = modCount;//初始值相等
//快速判断主要在判断修改的数据和期待修改的是否一致
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
expectedModCount在迭代器中除了将modCount的值赋值之外,没有其他的修改,那就意味着如果要不等,那就是modCount的值已经修改了,而modCount 的值修改意味着其他线程在调用list的add或者remove的操作。
Fail-safe是基于集合的克隆进行处理的,当容器的值修改之后,克隆的集合并不会受到影响。在concurrent包中基本都是fail-safe的。这个是一个cow(copy on write)原理的应用。其优点是不会触发concurrentModificationException,但是其遍历的是当时集合的一个快照,如果数据同时在这个时候添加就无法遍历了。
说说ArrayList的扩容机制?
直接上源码分析一下:
//入参:扩容之后的最小值
private void grow(int minCapacity) {
// 原来的容量
int oldCapacity = elementData.length;
//新的容量是原来的1.5倍,这里右移1位类似除以2.jdk源码有很多位运算
int newCapacity = oldCapacity + (oldCapacity >> 1);
//1.5倍后比最小值还小,直接变成最小值
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 是否大于定义的最大值,如果大于就是整型的最大值,否则就是MAX_ARRAY_SIZE
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 数据拷贝,使用系统拷贝函数
elementData = Arrays.copyOf(elementData, newCapacity);
}
从源码上分析,扩容经过了几步判断:直接是原来的1.5 -》和入参比较-》和maxARRAY比较。
说说hashset是怎么保证元素唯一的?
这个我们还是直接从源码入手进行分析,我们先看下添加一个元素的流程。
public boolean add(E e) {
return map.put(e, PRESENT)==null;//如果存在,返回oldvalue,不存在,返回null
}
//底层是hashmap,我们看下hashmap的实现,value是一个固定的object
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//...
//判断是否存在,hash 值和equals的方法都要相等
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//...
}
从上面的流程看出,如果hash和equals都相等的话,那就认为是存在了。这个会在 hashmap中重点说明一下putval方法。
comparable 和 Comparator的区别是什么?
Comparator在包java.util下,而Comparable在包java.lang下。Comparable 是在集合内部定义的方法实现的排序,Comparator 是在集合外部实现的排序。Comparble是一个对象支持的比较需要实现的接口,比如string,Byte都有实现这个接口。当一个对象定义的比较方法无法满足要求时,这个时候,我们可以考虑实现comparator接口,这就是两种的区别所在。
下面我们举个例子来说明一下:
public static void main(String[] args) throws Exception {
Test test1 = new Test(1,2);
Test test2 = new Test(2,1);
Test[] testArray = new Test[]{test1,test2};
Arrays.sort(testArray);//结果顺序test1,test2
Arrays.sort(testArray,new ComparatorTest());//结果顺序test2,test1
}
//定义新的比较函数,不用去修改类
public static class ComparatorTest implements Comparator<Test>{
@Override
public int compare(Test o1, Test o2) {
return o1.b.compareTo(o2.b);
}
}
//定义类的比较函数
public static class Test implements Comparable<Test>{
private Integer a;
private Integer b;
Test(int a,int b){
this.a = a;
this.b = b;
}
@Override
public int compareTo(Test o) {
return this.a.compareTo(o.a);
}
}
在上面的例子中,定义一个test类,定义类的同时定义了comparable接口,实现类内比较。又定义了一个ComparatorTest类,用于那些类无法满足的比较器。
本文介绍的集合侧重点在集合的collection,下一节我们会重点介绍map,尤其是hashmap。如果有时间的话,建议看下jdk源码的集合类,如果有问题的话,可以看下公众号里面的源码解析或者留言一起探讨哈。
本文的内容就这么多,如果你觉得对你的学习和面试有些帮助,帮忙点个赞或者转发一下哈,谢谢。
想要了解更多java内容(包含大厂面试题和题解)可以关注公众号,也可以在公众号留言,帮忙内推阿里、腾讯等互联网大厂哈