Java面试——集合篇
1.Java中常用的容器有哪些?
容器主要包括 Collection
和 Map
两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。
如图:
面试官追问:说说集合有哪些类及他们各自的区别和特点?
- Set(所有实现类都不支持重复的元素)
TreeSet
基于红黑树实现,支持有序性操作,例如根据一个范围查找元素的操作。但是查找效率不如 HashSet,HashSet 查找的时间复杂度为 O(1),TreeSet 则为 O(logN)。HashSet
基于HashMap实现,HashSet
的每个元素实际上是HashMap
的一个键,而对应的值(value)是一个固定的虚拟对象。支持快速查找,但不支持有序性操作。并且失去了元素的插入顺序信息,也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的。LinkedHashSet
是 HashSet 的子类,并且其内部是通过 LinkedHashMap 来实现的。内部使用双向链表维护元素的插入顺序。
- List
ArrayList
基于动态数组实现,支持随机访问。Vector
和 ArrayList 类似,但它是线程安全的,底层方法用synchronized修饰。LinkedList
基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList 还可以用作栈、队列和双向队列。
- Queue
LinkedList
可以用它来实现双向队列。PriorityQueue
基于堆结构实现,可以用它来实现优先队列。ArrayQueue
基于数组实现,可以用它实现双端队列,也可以作为栈。
面试官追问:说说Map有哪些类及他们各自的区别和特点?
TreeMap
基于红黑树实现。HashMap
1.7基于数组+链表实现,1.8基于数组+链表+红黑树。链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。HashTable
和 HashMap 类似,但它是线程安全的,这意味着同一时刻多个线程可以同时写入 HashTable 并且不会导致数据不一致。它是遗留类,不应该去使用它。(现在可以使用 ConcurrentHashMap 来支持线程安全,并且 ConcurrentHashMap 的效率会更高(1.7 ConcurrentHashMap 引入了分段锁(锁的是链表头,细粒度锁,不是锁整个map集合), 1.8 引入了红黑树)。)LinkedHashMap
继承自 HashMap。使用双向链表来维护元素的顺序,顺序为插入顺序或者最近最少使用(LRU)顺序。
2. Arraylist 和 LinkedList的区别?
ArrayList
:底层是基于数组实现的,查找快,增删较慢。LinkedList
不支持高效的随机元素访问,而ArrayList
支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)
方法)。LinkedList
:底层是基于链表实现的。确切的说是循环双向链表(JDK1.6之前是双向循环链表、JDK1.7之后取消了循环),查找慢、增删快。LinkedList链表由一系列表项连接而成,一个表项包含3个部分︰元素内容、前驱表和后驱表。因此内存空间占用比ArrayList 更多。
面试官追问:ArrayList的增删一定比LinkedList要慢吗?
不一定的。
- 如果增删都是在末尾来操作(每次调用的都是
remove()
和add()
),此时 ArrayList就不需要移动和复制数组来进行操作了。如果数据量有百万级的时,速度是会比 LinkedList 要快的。 - 如果删除操作的位置是在中间。由于LinkedList的消耗主要是在遍历上,ArrayList的消耗主要是在移动和复制上(底层调用的是
arrayCopy()
方法,是native方法)。LinkedList 的遍历速度是要慢于ArrayList的复制移动速度的。如果数据量有百万级的时,还是ArrayList要快。
3.ArrayList实现 RandomAccess接口有何作用?
查看源码我们发现实际上 RandomAccess
接口中什么都没有定义。
从源码可以看出RandomAccess 接口只是一个标志接口,只要List集合实现这个接口,就能支持快速随机访问。通过查看Collections
类中的binarySearch()
方法,可以看出,判断List是否实现RandomAccess接口来实行indexedBinarySerach(list, key)
或 iteratorBinarySerach(list, key)
方法。再通过查看这两个方法的源码发现:实现RandomAccess接口的List集合采用一般的 for循环遍历,而未实现这接口则采用迭代器,即ArrayList 一般采用for循环遍历,而 LinkedList 一般采用迭代器遍历。
public static <T>
int binarySearch(List<? extends Comparable<? super T>> list, T key) {
if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
return Collections.indexedBinarySearch(list, key);
else
return Collections.iteratorBinarySearch(list, key);
}
面试官追问:为何LinkedList却没实现这个接口?
ArrayList
底层是数组,而 LinkedList
底层是链表。
数组天然支持随机访问,时间复杂度为 O(1),所以称为快速随机访问。链表需要遍历到特定位置才能访问特定位置的元素,时间复杂度为 O(n),所以不支持快速随机访问。
ArrayList
实现了 RandomAccess
接口,就表明了他具有快速随机访问功能。 RandomAccess
接口只是标识,并不是说 ArrayList
实现 RandomAccess
接口才具有快速随机访问功能的!
4.Vector 和 ArrayList 的区别?
他们两个都实现了List
接口。底层数据结构都是数组。
不同的是:
- vector通过
remove
、add
等方法加上synchronized
关键字实现线程同步,所以是线程安全的。而ArrayList是线程不安全的 - 由于vector使用了
synchronized
进行加锁,所以性能不如ArrayList - Vector 扩容时,如果未指定扩容递增值
capacityIncrement
,或该值不大于 0 时,每次扩容为原来的2
倍,否则扩容量为capacityIncrement
的值。ArrayList每次扩容为旧容量的1.5
倍
5. ArrayList 的扩容机制?
第一种情况:list中第一次添加数据
这种情况下,因为DEFAULT_CAPACITY = 10; 默认初始的容量(CAPACITY)
所以minCapacity会被赋值为较大的那个,也就是10。所以这里需要扩容(minCapacity(10) - elementData.length(0) > 0)进入扩容方法。
而进入扩容方法以后,会走第一次初始化数组长度那个if判断。
第二种情况:添加数据,但不需要扩容
这种情况下,根本不会走到扩容方法。
第三种情况:本次添加数据又需要扩容了
这种情况下,进入扩容方法以后,会扩容原来容量的1.5倍。最后还会进行数组拷贝,把原数组元素拷贝到,新增容量后的数组中!
总结:
当使用add方法的时候首先调用
ensureCapacityInternal
方法,传入size+1
进去,检查是否需要扩容如果空数组
DEFAULTCAPACITY_EMPTY_ELEMENTDATA
,就初始化为默认大小10,获取“默认的容量”和要扩容的大小两者之间的最大值和当前数组长度比较,如果
if (minCapacity - elementData.length > 0)
执行grow
扩容方法将数组扩容为原来的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量
再检查新容量newCapacity 是否超出了ArrayList所定义的最大容量,若超出了,则调用
hugeCapacity()
来比较minCapacity和 MAX_ARRAY_SIZE,如果minCapacity大于MAX_ARRAY_SIZE,则新容量则为Interger.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE(MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8
)ArrayList 中copy数组的核心就是
System.arraycopy
方法,将original数组的所有数据复制到copy数组中,这是一个本地方法
6.Array和ArrayList有何区别?
- Array(就是数组)可以容纳基本类型和对象,而ArrayList(集合)只能容纳对象
- Array是指定大小的,ArrayList 的容量是根据需求自动扩展
- ArrayList提供了更多的方法和特性,比如:addAll(),removeAll(),iterator()等等
面试官追问:什么时候更适合使用Array?
- 如果列表的大小已经指定,大部分情况下是存储和遍历它们可以使用Array
- 对于基本类型数据,ArrayList 使用自动装箱来减少编码工作量;而当处理固定大小的基本数据类型的时候,这种方式相对比较慢,这时候应该使用Array
- 如果你要使用多维数组,使用
[][]
比 List更容易
7.comparable和comparator的区别?
comparable
接口出自java.lang
包,可以理解为一个内比较器,因为实现了comparable接口的类可以和自己比较,要和其他实现了Comparable接口类比较,可以使用compareTo(objectobj)
方法。compareTo方法的返回值是int,有三种情况:- 返回正整数(比较者大于被比较者)
- 返回0(比较者等于被比较者)
- 返回负整数(比较者小于被比较者)
comparator
接口出自java.util
包,它有一个compare(object obj1,object obj2)
方法用来排序,返回值同样是int,有三种情况,和compareTo类似。
它们之间的区别:
- 很多包装类都实现了comparable接口,像Integer、string等。所以直接调用
co1lections.sort()
直接可以使用。如果对类里面自带的自然排序不满意,而又不能修改其源代码的情况下,使用comparator
就比较合适。 - 此外使用
comparator
可以避免添加额外的代码与我们的目标类耦合,同时可以定义多种排序规则,这一点是comparable接口没法做到的 - 从灵活性和扩展性讲
Comparator
更优,故在面对自定义排序的需求时,可以优先考虑使用comparator接口。
详细讲解:面试官:元素排序Comparable和Comparator有什么区别?-CSDN博客
8.Collection和Collections有什么区别?
Collection
:是最基本的集合接口,它提供了对集合对象进行基本操作的通用接口方法。一个Collection代表一组Object,即Collection的元素。它的直接继承接口有List,Set 和Queue。Collections
:是不属于Java的集合框架的,它是集合类的一个工具类。此类不能被实例化,服务于Java的Collection框架。它包含有关集合操作的静态多态方法,实现对各种集合的搜索、排序、线程安全等操作。