问:ArrayList 与 LinkedList 使用普通 for 循环遍历谁快谁慢?为什么?
答:ArrayList 比 LinkedList 使用普通 for 循环遍历快,具体怎么快见如下分析。
ArrayList 普通 for 循环遍历之所以快可以先看下 ArrayList 的 get 方法实现,如下:
public E get(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
return (E) elementData[index];
}
可以看到 ArrayList 的 get 方法只是通过数组下标从数组里面拿一个元素而已,所以 get 方法的时间复杂度是 O(1) 常数,和数组的大小没有关系,只要给定数组的位置就能定位到数据,而 Java 中 ArrayList 是基于数组实现的,数组是内存地址连续的空间,取值其实是地址的偏移,所以自然块的多。
LinkedList 普通 for 循环遍历之所以快可以先看下 LinkedList 的 get 方法实现,如下:
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
从上面代码可以看出由于 LinkedList 是双向链表,所以 node 方法里面的 if 判断条件是在计算 index 在前一半还是后一半,然后决定是前序遍历还是后序遍历,这样二分法遍历查找会快很多,但是在 for 循环里面 LinkedList 在 get 任何一个位置元素时都会把前面的元素走一遍,随着 LinkedList 的容量越大这个遍历也就越大,所以其遍历的时间复杂度为 O(N/2),N 为 LinkedList 的容量,所以自然效率就比较低了。
问:为什么说集合的不同列表应该选择不同的遍历方式,举例谈谈你的认识?
答:这是一个观点题,但是有有力的依据支撑,如果有研究过不同的 List 源码结构和迭代器实现就能比较好的做出答案。
首先举例譬如 ArrayList 是直接通过数组来进行存储,所以在使用下标的方式循环遍历的时候性能最好,通过下标可以直接取数据,速度最快,而且由于 ArrayList 是实现了 RandomAccess 接口(随机存取),这个接口是一个标记接口,表明了 ArrayList 集合的数据元素之间没有关联,位置间没有索引依赖。而如果对于 ArrayList 使用 for-each 或者迭代器进行遍历就没有 index 索引遍历效率高了,因为迭代器强制将 RandomAccess 的 ArrayList 建立了前后遍历关系,且在每次遍历过程中进行了一堆判断,所以相对来说对于 ArrayList 来说遍历使用普通 index 比迭代器要效率高些,但是差距不是十分明显。
接着再拿 LinkedList 来说,其为双向链表的实现存储,前后元素是通过链表索引建立关联的,无法直接取到对应的下标,因此在使用普通的 index 索引下标遍历时就需要计算对应的元素在哪,二分法决定头部还是尾部遍历,然后一步步的遍历找到元素,所以在遍历中每次都要从头查找元素位置,十分低效率。而迭代器的实现就是指向下一个元素,迭代器直接通过 LinkedList 的指针进行遍历,一次遍历就能找到每个合适的元素,所以 LinkedList 在使用迭代器遍历时效率最高。
问:下面程序片段的输出结果是什么?为什么?
List<String> list = new ArrayList<>();
list.add("android");
list.add("java");
List<String> list1 = new ArrayList<>(list);
List<String> list2 = list.subList(0, list.size());
list2.add("unix C");
System.out.println(list.equals(list1));
System.out.println(list.equals(list2));
答:输出为 false、true。
因为通过构造方法创建的 list1 实质上新的列表,其内部实现是通过 copyof 动作生成的,生成的列表与原列表没有任何关系(虽然是浅拷贝,但是由于元素是 String 类型,所以可以理解成是深拷贝),所以当我们对 list 添加了元素后再与 list1 进行对比会发现没有任何关系,而依据集合的比较都是比较元素值,所以 list 与 list1 自然不相等。对于 list2 来说 subList 产生的集合列表只是一个视图,所有的修改操作都会作用于原集合列表上,所以修改 list2 就相当于修改了 list 集合,不信可以去看 AbstractList 和 ArrayList 中 subList 的实现,虽然生成了新的 ArrayList public 内部类 SubList 实例,但是该实例中每个操作都传递操作了外部类 ArrayList 的对应操作,所以返回 true。
问:如何优雅的删除列表区间,譬如有一个 List 里面有 100 个元素,现在想删除 30 到 50 之间的元素,怎么写代码最优雅?
答:这个问题其实比较坑,转不过弯就很容易不知所措,其实常见的删除操作
都是通过遍历删除区间的,譬如 index 索引从 30 开始到 50 结束,总之都得两三句代码,而问题提到了优雅就是说美观简洁咯,下面的方式即可:
list.subList(30, 50).clear();
所以说遇到类似场景还是使用这种优雅方式吧,一行代码简洁大方。
问:下面程序片段运行结果是什么?为什么?
List<String> list = new ArrayList<>();
list.add("android");
list.add("java");
List<String> sublist = list.subList(0, list.size());
list.add("unix C");
System.out.println("list size="+list.size());
System.out.println("sublist size="+sublist.size());
答:这个程序运行会在 sublist.size() 处报错 ConcurrentModificationException。因为 subList 取出的列表只是原列表的一个视图,原数据集合修改了后 subList 取出的子列表不会重新生成新的列表,而 SubList 中每个方法都有修改计数检测,后面再对子列表操作时就检测到计数器与预期不相等了,所以抛出异常,具体感兴趣可以去查看 ArrayList 的 SubList 内部类实现。切记通过 subList 生成子列表后不要再操作原列表。
问:下面程序有问题吗?为什么?
ArrayList<String> list = new ArrayList<>();
list.add("android");
ArrayList<String> subList = (ArrayList<String>) list.subList(0, 1);
subList.add("unix");
答:这个程序运行会抛出 java.lang.ClassCastException: java.util.ArrayList$SubList cannot be cast to java.util.ArrayList 异常。
因为 subList 返回的 List 是 ArrayList 内部类 SubList(继承自 AbstractList),看起来都是 List 的实现,但是不是同一个子类,无法强转为 ArrayList,修改方案为 subList 的返回接收声明为 List<String> 类型即可,心里明白这个 List 是 SubList 就行。
问:下面程序的输出是什么?为什么?
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 同理。
问:简单说说 Iterator 和 ListIterator 的区别?
答:区别主要如下。
-
ListIterator 有 add() 方法,可以向 List 中添加对象,而 Iterator 不能。
-
ListIterator 和 Iterator 都有 hasNext() 和 next() 方法,可以实现顺序向后遍历,但是 ListIterator 有 hasPrevious() 和 previous() 方法,可以实现逆向(顺序向前)遍历,Iterator 就不可以。
-
ListIterator 可以定位当前的索引位置,通过 nextIndex() 和 previousIndex() 可以实现,Iterator 没有此功能。
-
都可实现删除对象,但是 ListIterator 可以实现对象的修改,通过 set() 方法可以实现,Iterator 仅能遍历,不能修改。
-
ListIterator 是 Iterator 的子接口。
注意:容器类提供的迭代器都会在迭代中间进行结构性变化检测,如果容器发生了结构性变化,就会抛出 ConcurrentModificationException,所以不能在迭代中间直接调用容器类提供的 add、remove 方法,如需添加和删除,应调用迭代器的相关方法。
问:为什么使用 for-each 时调用 List 的 remove 方法元素会抛出 ConcurrentModificationException 异常?
答:首先Java 提供了一个 Iterable 接口返回一个迭代器,常用的 Collection<E>、List<E>、Set<E> 等都实现了这个接口,该接口的 iterator() 方法返回一个标准的 Iterator 实现,实现 Iterable 接口允许对象成为 for-each 语句的目标来遍历底层集合序列,因此使用 for-each 方式遍历列表在编译后实质是迭代器的形式实现。之所以会出现 ConcurrentModificationException 异常我们需要去看下最常见的 ArrayList 中 iterator() 方法的实现(别的集合 iterator 类似),如下:
private class Itr implements Iterator<E> {
protected int limit = ArrayList.this.size; //集合列表的个数尺寸
int cursor; //下一个元素的索引位置
int lastRet = -1; //上一个元素的索引位置
int expectedModCount = modCount;
public boolean hasNext() {
return cursor < limit;
}
@SuppressWarnings("unchecked")
public E next() {
//modCount用于记录ArrayList集合的修改次数,初始化为0,
//每当集合被修改一次(结构上面的修改,内部update不算),
//如add、remove等方法,modCount + 1,所以如果modCount不变,
//则表示集合内容没有被修改。
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
int i = cursor;
//如果下一个元素的索引位置超过了集合长度抛出异常
if (i >= limit)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
//调用一次cursor加一次
cursor = i + 1;
//返回当前一个元素
return (E) elementData[lastRet = i];
}
public void remove() {
//lastRet每次在remove成功后都需要在next()中重新赋值,
//否则调用一次后再调用为-1异常,因此使用迭代器的remove方法
//前必须先调用next()方法。
if (lastRet < 0)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
limit--;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
......
}
通过上面的源码发现迭代操作中都有判断 modCount!=expectedModCount 的操作,在 ArrayList 中 modCount 是当前集合的版本号,每次修改(增、删)集合都会加 1,expectedModCount 是当前迭代器的版本号,在迭代器实例化时初始化为 modCount,所以当调用 ArrayList.add() 或 ArrayList.remove() 时只是更新了 modCount 的状态,而迭代器中的 expectedModCount 未修改,因此才会导致再次调用 Iterator.next() 方法时抛出 ConcurrentModificationException 异常。而使用 Iterator.remove() 方法没有问题是因为 Iterator 的 remove() 方法中有同步 expectedModCount 值,所以当下次再调用 next() 时检查不会抛出异常。这其实是一种快速失败机制,机制的规则就是当多个线程对 Collection 进行操作时若其中某一个线程通过 Iterator 遍历集合时该集合的内容被其他线程所改变,则抛出 ConcurrentModificationException 异常。
因此在使用 Iterator 遍历操作集合时应该保证在遍历集合的过程中不会对集合产生结构上的修改,如果在遍历过程中需要修改集合元素则一定要使用迭代器提供的修改方法而不是集合自身的修改方法,此外 for-each 循环遍历的实质是迭代器,使用迭代器的 remove() 方法前必须先调用迭代器的 next() 方法且不允许调用一次 next() 方法后调用多次 remove() 方法。
问: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() 等。