大厂高频面试题:谈谈你对ArrayList的理解?
面试中经常碰到ArrayList的问题,比如:
- 请你说说ArrayList和LinkList的区别?
- 请你谈一下对ArrayList的理解?
- ArrayList是线程安全的吗?
- ArrayList<String> list = new ArrayList<>(20); 中的list扩充几次?
本篇就讲一讲ArrayList的知识点。
1.什么是ArrayList?
ArrayList是个动态数组,实现List接口,主要用来存储数据,如果存储基本类型的数据,如int,long,boolean,short,byte,那只存储它们对应的包装类。它的特点是:
增删慢:每次删除元素,都需要更改数组长度、拷贝以及移动元素位置。
查询快:由于数组在内存中是一块连续空间,因此可以根据地址+索引的方式快速获取对应位置上的元素。
2.ArrayList线程安全吗?
ArrayList是线程不安全的。
当开启多个线程操作List集合,向ArrayList中增加元素,同时去除元素。最后输出list中的所有数据,会出现几种情况:
①有些元素输出为Null;②数组下标越界异常。
有两种解决方案:
public static Vector<Object> vector= new Vector<Object>();
List<String> list = Collections.synchronizedList(new ArrayList<>());
为什么ArrayList线程不安全,我们还使用它?
因为大多数的场景中,查询操作使用频率高,增删操作的使用频率低。如果涉及频繁的增删,可以使用LinkedList,实际开发过程中还是ArrayList使用最多的。 不存在一个集合既查询效率高,又增删效率高,还线程安全的,因为数据结构的特性就是优劣共存的,想找个平衡点很难,牺牲了性能,那就安全,牺牲了安全那就快速。
3.ArrayList默认的数组大小是多少?
private static final int DEFAULT_CAPACITY = 10;
ArrayList默认数组大小是10。
public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }
默认的构造方法,构造一个初始容量为10的空列表。
4.ArrayList如何扩容?
// grow扩容方法 private void grow(int minCapacity) { // 记录扩容前的数组长度 int oldCapacity = elementData.length; // 位运算,右移动一位。 整体相当于newCapacity =oldCapacity + 0.5 * oldCapacity int newCapacity = oldCapacity + (oldCapacity >> 1); // 如果扩容后的长度小于当前的数据量,那么就将当前的数据量的长度作为本次扩容的长度 if (newCapacity - minCapacity < 0) newCapacity = minCapacity; // 判断新数组的长度是否大于可分配数组的最大值 if (newCapacity - MAX_ARRAY_SIZE > 0) // 将扩容长度设置为最大可用长度 newCapacity = hugeCapacity(minCapacity); // 拷贝,扩容,构建一个新的数组 elementData = Arrays.copyOf(elementData, newCapacity); }
5.ArrayList<String> list = new ArrayList<>(20); 中的list扩充几次?
public ArrayList(int initialCapacity) { //判断initialCapacity是否大于0 if (initialCapacity > 0) { //创建一个数组,且指定长度为initialCapacity this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { //如果initialCapacity容量为0,把EMPTY_ELEMENTDATA的地址赋值给elementData this.elementData = EMPTY_ELEMENTDATA; // 如果初始容量小于0,则会出现 IllegalArgumentException 异常 } else { throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); } }
6.ArrayList频繁扩容导致添加性能急剧下降,如何处理?
使用ArrayList时,可以 new ArrayList(大小)构造方法来指定集合的大小,以减少扩容的次数,提高写入效率。
7.ArrayList插入或删除元素一定比LinkedList慢么?
取决于删除的元素离数组末端有多远,ArrayList拿来作为堆栈来用还是挺合适的,push和pop操作完全不涉及数据移动操作。
8.如何复制某个ArrayList到另一个ArrayList中去?
public static void main(String[] args) { ArrayList<String> list = new ArrayList<String>(); list.add("云"); list.add("烟"); list.add("成"); list.add("雨"); Object o = list.clone(); System.out.println(o); System.out.println(list); }
public Object clone() { try { ArrayList<?> v = (ArrayList<?>) super.clone(); v.elementData = Arrays.copyOf(elementData, size); v.modCount = 0; return v; } catch (CloneNotSupportedException e) { // this shouldn't happen, since we are Cloneable throw new InternalError(e); } }
public static void main(String[] args) { ArrayList<String> list = new ArrayList<String>(); list.add("aaa"); list.add("bbb"); list.add("ccc"); ArrayList<String> list1 = new ArrayList<>(list); for (String s : list1) { System.out.println(s); } }
public ArrayList(Collection<? extends E> c) { // 将集合构造中的集合对象转成数组,且将数组的地址赋值给elementData elementData = c.toArray(); // 将elementData的长度赋值给 集合长度size,且判断是否不等于 0 if ((size = elementData.length) != 0) { // 判断elementData 和 Object[] 是否为不一样的类型 if (elementData.getClass() != Object[].class) //如果不一样,使用Arrays的copyOf方法进行元素的拷贝 elementData = Arrays.copyOf(elementData, size, Object[].class); } else { // 用空数组代替 this.elementData = EMPTY_ELEMENTDATA; } }
public static void main(String[] args) { ArrayList<String> list = new ArrayList<>(); list.add("Hello"); list.add("world"); list.add("!"); ArrayList<String> list1 = new ArrayList<>(); list1.addAll(list); System.out.println(list); System.out.println(list1); }
public boolean addAll(Collection<? extends E> c) { //把集合的元素转存到Object类型的数组中 Object[] a = c.toArray(); //记录数组的长度 int numNew = a.length; //调用方法检验是否要扩容,且让增量++ ensureCapacityInternal(size + numNew); //调用方法将a数组的元素拷贝到elementData数组中 System.arraycopy(a, 0, elementData, size, numNew); //集合的长度+=a数组的长度 size += numNew; //只要a数组的长度不等于0,即说明添加成功 return numNew != 0; }
9.ArrayList用来做队列合适么?
队列一般是FIFO(先入先出)的,如果用ArrayList做队列,需要在数组尾部追加数据,数组头部删除数组,反过来也可以。但是无论如何总会有一个操作会涉及到数组的数据搬迁,这个是比较耗费性能的。
虽然ArrayList不适合做队列,但是数组是非常合适的。比如ArrayBlockingQueue内部实现就是一个环形队列,它是一个定长队列,内部是用一个定长数组来实现的。另外著名的Disruptor开源Library也是用环形数组来实现的超高性能队列,具体原理不做解释,比较复杂。简单点说就是使用两个偏移量来标记数组的读位置和写位置,如果超过长度就折回到数组开头,前提是它们是定长数组。
10.ArrayList和LinkedList的区别?
ArrayList:
- 基于动态数组的数据结构
- 对于随机访问的get和set,ArrayList要优于LinkedList
- 对于随机操作的add和remove,ArrayList不一定比LinkedList慢 (ArrayList底层由于是动态数组,因此并不是每次add和remove的时候都需要创建新数组)
LinkedList:
- 基于双向链表的数据结构(双向链表遍历效率可能优于单向链表,因为双向链表可以在查找元素时,判断靠近头还是靠近尾,如果靠近头从头开始找,如果靠近尾从尾开始找)
- 对于顺序操作,LinkedList不一定比ArrayList慢
- 对于随机操作,LinkedList效率明显较低