问:Arraylist 的动态扩容机制是如何自动增加的?简单说说你理解的流程?
答:当在 ArrayList 中增加一个对象时 Java 会去检查 Arraylist 以确保已存在的数组中有足够的容量来存储这个新对象(默认为 10,最大容量为 int 上限,减 8 是为了容错),如果没有足够容量就新建一个长度更长的数组(原来的1.5倍),旧的数组就会使用 Arrays.copyOf 方法被复制到新的数组中去,现有的数组引用指向了新的数组。下面代码展示为 Java 1.8 中通过 ArrayList.add 方法添加元素时,内部会自动扩容,扩容流程如下
//确保容量够用,内部会尝试扩容,如果需要
ensureCapacityInternal(size + 1)
//在未指定容量的情况下,容量为DEFAULT_CAPACITY = 10
//并且在第一次使用时创建容器数组,在存储过一次数据后,数组的真实容量至少DEFAULT_CAPACITY
private void ensureCapacityInternal(int minCapacity) {
//判断当前的元素容器是否是初始的空数组
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//如果是默认的空数组,则 minCapacity 至少为DEFAULT_CAPACITY
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
//通过该方法进行真实准确扩容尝试的操作
private void ensureExplicitCapacity(int minCapacity) {
modCount++; //记录List的结构修改的次数
//需要扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
//扩容操作
private void grow(int minCapacity) {
//原来的容量
int oldCapacity = elementData.length;
//新的容量 = 原来的容量 + (原来的容量的一半)
int newCapacity = oldCapacity + (oldCapacity >> 1);
//如果计算的新的容量比指定的扩容容量小,那么就使用指定的容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//如果新的容量大于MAX_ARRAY_SIZE(Integer.MAX_VALUE - 8)
//那么就使用hugeCapacity进行容量分配
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
//创建长度为newCapacity的数组,并复制原来的元素到新的容器,完成ArrayList的内部扩容
elementData = Arrays.copyOf(elementData, newCapacity);
}
问:请写出下面代码片段的运行结果及原因?
ArrayList<Integer> list = new ArrayList<Integer>();
list.add(1);
list.add(2);
list.add(3);
Integer[] array1 = new Integer[3];
list.toArray(array1);
Integer[] array2 = list.toArray(new Integer[0]);
System.out.println(Arrays.equals(array1, array2)); //1 结果是什么?为什么?
Integer[] array = {1, 2, 3};
List<Integer> list = Arrays.asList(array);
list.add(4); //2 结果是什么?为什么?
Integer[] array = {1, 2, 3};
List<Integer> list = new ArrayList<Integer>(Arrays.asList(array));
list.add(4); //3 结果是什么?为什么?
答:这题的运行结果及原因解释分别如下。
1 输出为 true,因为 ArrayList 有两个方法可以返回数组 Object[] toArray() 和 <T> T[] toArray(T[] a),第一个方法返回的数组是通过 Arrays.copyOf 实现的,第二个方法如果参数数组长度足以容纳所有元素就使用参数数组,否则新建一个数组返回,所以结果为 true。
2 会抛出 UnsupportedOperationException 异常,因为 Arrays 的 asList 方法返回的是一个 Arrays 内部类的 ArrayList 对象,这个对象没有实现 add、remove 等方法,只实现了 set 等方法,所以通过 Arrays.asList 转换的列表不具备结构可变性。
3 当然可以正常运行咯,不可变结构的 Arrays 的 ArrayList 通过构造放入了真正的万能 ArrayList,自然就可以操作咯。
问:为什么 ArrayList 的增加或删除操作相对来说效率比较低?能简单解释下为什么吗?
答:ArrayList 在小于扩容容量的情况下其实增加操作效率是非常高的,在涉及扩容的情况下添加操作效率确实低,删除操作需要移位拷贝,效率是低点。因为 ArrayList 中增加(扩容)或者是删除元素要调用 System.arrayCopy 这种效率很低的方法进行处理,所以如果遇到了数据量略大且需要频繁插入或删除的操作效率就比较低了,具体可查看 ArrayList 的 add 和 remove 方法实现,但是 ArrayList 频繁访问元素的效率是非常高的,因此遇到类似场景我们应该尽可能使用 LinkedList 进行替代效率会高一些。
问:简单说说 Array 和 ArrayList 的区别?
答:这题相当小儿科,Array 可以包含基本类型和对象类型,ArrayList 只能包含对象类型;Array 的大小是固定的,ArrayList 的大小是动态变化的;ArrayList 提供了更多的方法和特性,譬如 addAll()、removeAll()、iterator() 等。
问:下面四个方法有什么问题吗?为什么?
public void remove1(ArrayList<Integer> list) {
for(Integer a : list){
if(a <= 10) {
list.remove(a);
}
}
}
public void remove2(ArrayList<Integer> list) {
Iterator<Integer> it = list.iterator();
while(it.hasNext()) {
if(it.next() <= 10) {
it.remove();
}
}
}
public void remove3(ArrayList<Integer> list) {
Iterator<Integer> it = list.iterator();
while(it.hasNext()) {
it.remove();
}
}
public void remove4(ArrayList<Integer> list) {
Iterator<Integer> it = list.iterator();
while(it.hasNext()) {
it.next();
it.remove();
it.remove();
}
}
答:这个问题很小儿科,答案如下。
remove1 方法会抛出 ConcurrentModificationException 异常,这是迭代器的一个陷阱,foreach 遍历编译后实质会替换为迭代器实现(普通for循环不会抛这个异常,因为list.size方法一般不会变,所以只会漏删除),因为迭代器内部会维护一些索引位置数据,要求在迭代过程中容器不能发生结构性变化(添加、插入、删除,修改数据不算),否则这些索引位置数据就失效了,避免的方式就是使用迭代器的 remove 方法。
remove2 方法可以正常运行,无任何错误。
remove3 方法会抛出 IllegalStateException 异常,因为使用迭代器的 remove 方法前必须先调用 next 方法,next 方法会检测容器是否发生了结构性变化,然后更新 cursor 和 lastRet 值,直接不调用 next 而 remove 会导致相关值不正确。
remove4 方法会抛出 IllegalStateException 异常,理由同 remove3,remove 调用一次后 lastRet 值会重置为 -1,没有调用 next 去设置 lastRet 的情况下再直接调一次 remove 自然就状态异常了。
问:下面程序片段的 remove 方法想删除 list 列表中的所有 "android",请分别说说其各个 remove 方法有没有问题?为什么?该怎么解决?
ArrayList<String> list = new ArrayList<String>();
list.add("java");
list.add("android");
list.add("android");
list.add("c");
list.add("c++");
list.add("c");
public void remove1(ArrayList<String> list) {
for (inti=0; i<list.size(); i++) {
String s = list.get(i);
if (s.equals("android")) {
list.remove(s);
}
}
}
public void remove2(ArrayList<String> list) {
for (Strings: list) {
if (s.equals("android")) {
list.remove(s);
}
}
}
答:上面程序的 remove1 方法和 remove2 方法运行情况如下。
remove1 方法执行后第二个 “android” 字符串没有被删掉,因为我们可以看下 ArrayList 的 remove 方法(ArrayList 有两个同名不同参的 remove 方法)实现核心代码:
public boolean remove(Object o) {
if (o == null) {
......
} else {
//一般元素走这个分支
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
}
可以看到最终删除会执行 System.arraycopy 方法而导致删除元素时涉及到数组元素的移动,所以在遍历第一个字符串 "android" 时因为符合删除条件,所以将该元素从数组中删除,并且将后一个元素移动(也就是第二个字符串 "android")至当前位置,导致下一次循环遍历时后一个字符串 "android" 并没有被遍历到,所以无法删除,同时由于每删除一次 size 也减一,所以其实每次都会向前移位,也会导致越来越多的元素无法被遍历获取。解决的办法就是倒序遍历删除(千万不要认为将 for 循环里 list.size() 提到外边先变量取值就可以删除,这样会报列表索引越界),这种方法的效率相对好些,如下:
public void remove1(ArrayList<String> list) {
for(int i=list.size()-1; i>=0; i--) {
String s = list.get(i);
if (s.equals("android")) {
list.remove(s);
}
}
}
因为数组倒序遍历时即使发生元素删除也不影响后序元素的遍历。
remove2 方法运行会报 for-each 著名的并发修改异常 java.util.ConcurrentModificationException,因为迭代器内部会维护一些索引位置数据,要求在迭代过程中容器不能发生结构性变化(添加、插入、删除,修改数据不算),否则这些索引位置数据就失效了,修改的方式就是使用迭代器的 remove 方法替换 remove2 中的实现即可。
问:下面程序的输出是什么?为什么?
public class Demo {
public static void main(String[] args) {
int[] data = {1, 2, 3, 4};
List list = Arrays.asList(data);
System.out.println(list.size());
}
}
答:打印结果为:1。首先我们看下 asList 方法的实现源码,如下:
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
可以看到返回的 Arrays 的内部类 ArrayList 构造方法接收的是一个类型为 T 的数组,而基本类型是不能作为泛型参数的,所以这里参数 a 只能接收引用类型,自然为了编译通过编译器就把上面的 int[] 数组当做了一个引用参数,所以 size 为 1,要想修改这个问题很简单,将 int[] 换成 Integer[] 即可。所以原始类型不能作为 Arrays.asList 方法的参数,否则会被当做一个参数。
问:下面程序能正常运行吗?为什么?
public class Demo {
public static void main(String[] args) {
Integer[] data = {1, 2, 3, 4};
List<Integer> list = Arrays.asList(data);
list.add(5);
}
}
答:上面程序运行抛出 UnsupportedOperationException 异常。理论上正常的 ArrayList 都是支持 add 方法的,这里为什么会不支持呢?因为 Arrays.asList 返回的 ArrayList 是 Arrays 的静态内部私有类实现,不是常用的那个 ArrayList,这里的 ArrayList 继承自 AbstractList,但是只实现了 size、toArray、get、set、contains 几个方法,其他常见的 add、remove 等方法都没实现,所以才抛出异常了。此外像 ArrayList<Integer> list = Arrays.asList(1, 2, 3); 这样的代码是无法编译通过的,因为 list 已经不是常用的 ArrayList 了,Arrays 内部的 ArrayList 是私有的。所以说 Arrays.asList 返回的 List 是一个不可变长度的列表,此列表不再具备原 List 的很多特性,因此慎用 Arrays.asList 方法。
问:下面程序的输出结果是什么?
ArrayList<String> list = new ArrayList<>();
list.add("android");
Vector<String> vector = new Vector<>();
vector.add("android");
System.out.println(list.equals(vector));
答:上面程序运行结果为 true。因为集合列表的相等只关心元素数据的比较,其 equals 方法都是 AbstractList 中实现的,比较的依据是通过迭代器遍历元素挨个 equals 比较,所以自然为 true,其他集合 Map、Set 同理。