删除类问题

有一个 ArrayList,数据是 2、3、3、3、4,中间有四个 3,现在我通过for (int i = 0; i < list.size(); i++) 的方式,想把值是 3 的元素删除,请问可以删除干净么?最终删除的结果是什么,为什么?删除代码如下:
以下三个问题对于 LinkedList 结果相同。

  1. @Test
  2. public void test18() {
  3. List<String> list = new ArrayList<String>() {{
  4. add("2");
  5. add("3");
  6. add("3");
  7. add("3");
  8. add("3");
  9. add("4");
  10. }};
  11. // remove() 删除后,打印的结果为:[2, 3, 3, 4]
  12. for (int i = 0; i < list.size(); i++) {
  13. if (list.get(i).equals("3")) {
  14. list.remove(i);
  15. i--;
  16. }
  17. }
  18. // iterator.remove() 删除后,打印结果:[2, 4]
  19. Iterator<String> iterator = list.iterator();
  20. while (iterator.hasNext()) {
  21. String s = iterator.next();
  22. if (s.equals("3")) {
  23. iterator.remove();
  24. }
  25. }
  26. // 方案3
  27. while(list.remove("3"));
  28. }

答:
remove() 不能删干净,最终的结果为 [2, 3, 3, 4],因为:每次 remove() 删除一个元素后,该元素后面的元素就会往前移动,而此时循环的 i 在不断地增长,最终会使每次删除 3 的后一个 3 被遗漏没有进行判断,导致删除不掉。 解决办法,在 remove() 后 i—
Iterator.remove() 可以删干净,因为它的底层源码实现在删除之后 cursor(下一个要返回的索引)仍为删除前索引值,不存在删除后遗漏判断问题。
while(list.remove("3")); 就可以删干净。不过性能较差。

ArrayList 和 LinkedList 应用场景不同


ArrayList 更适合于快速的查找匹配,不适合频繁新增删除。
LinkedList 更适合于经常新增和删除,对查询反而很少的场景。


如果是顺序插入的话,ArrayList 比 LinkedList 快,即调用无参的 add()
LinkedList 只有随机插入的时候可能快于 ArrayList,即调用 add(int index, E element) 插入,这个可能因素决定于 ArrayList 移动的元素个数。
一般情况下,都是使用无参的 add(),所以 ArrayList 使用的多。

ArrayList 和 LinkedList 两者的最大容量


ArrayList 有最大容量,为 Integer 的最大值,大于这个值 JVM 是不会为数组分配内存空间的。
LinkedList 底层是双向链表,理论上可以无限大。但源码中,LinkedList 实际大小用的是 int 类型,这也说明了 LinkedList 不能超过 Integer 的最大值,不然会溢出。

ArrayList 和 LinkedList 如何处理 null 值


ArrayList 和 LinkedList 都允许新增和删除 null 值,且删除时,都是找到第一个值为 null 的元素删除。

ArrayList 和 LinedList 线程不安全,为什么


当两者是非共享变量时,比如说仅仅是在方法里面的局部变量时,是没有线程安全问题的。
只有当两者是共享变量时,才会有线程安全问题。
共享变量的主要问题在于多线程环境下,所有线程任何时刻都可对数组和链表进行操作,这会导致值被覆盖,甚至混乱的情况。
如果有线程安全问题,在迭代的过程中,会频繁报 ConcurrentModificationException 的错误,意思是在我当前循环的过程中,数组或链表的结构被其它线程改变了。

ArrayList 和 LinkedList 的线程安全问题


Java 源码中推荐使用 Collections.synchronizedList() 进行解决 ,Collections.synchronizedList 的返回值是 List 的,每个方法都加了 synchronized 锁,保证了在同一时刻,数组和链表只会被一个线程所修改。
或者采用 CopyOnWriteArrayList 并发 List 来解决线程安全问题。

List 集合 -> 数组

List 集合转化成数组,我们通常使用 toArray() 的有参构造,该方法有一个坑,当数组初始化大小不够时,方法内部会构造一个长度为 length 的新数组 copyOf() 并返回,而没有用给参数列表中给定的数组 copyOf。
以下 demo 用于演示坑:

  1. @Test
  2. public void test18() {
  3. List<Integer> list = new ArrayList<Integer>() {{
  4. add(1);
  5. add(2);
  6. add(3);
  7. add(4);
  8. }};
  9. // 无参 toArray 返回的是 Object[],无法向下转化成 List<Integer>,编译都无法通过
  10. // List<Integer> list2 = list.toArray();
  11. Integer[] integers = new Integer[2];
  12. Integer[] array0 = list.toArray(integers);
  13. // 打印结果为:[null, null]
  14. System.out.println(Arrays.toString(integers));
  15. // 打印结果为:[1, 2, 3, 4]
  16. System.out.println(Arrays.toString(array0));
  17. Integer[] array1 = list.toArray(new Integer[list.size()]);
  18. // 打印结果为:[1, 2, 3, 4]
  19. System.out.println(Arrays.toString(array1));
  20. Integer[] array2 = list.toArray(new Integer[list.size() + 2]);
  21. // 打印结果为:[1, 2, 3, 4, null, null]
  22. System.out.println(Arrays.toString(array2));
  23. }

toArray() 的底层源码实现如下:

  1. public <T> T[] toArray(T[] a) {
  2. // 如果数组长度不够,按照 List 的大小进行拷贝,return 的时候返回的都是正确的数组
  3. if (a.length < size)
  4. // Make a new array of a's runtime type, but my contents:
  5. // 创建一个新的数组的运行时类型,但我的内容:
  6. return (T[]) Arrays.copyOf(elementData, size, a.getClass());
  7. System.arraycopy(elementData, 0, a, 0, size);
  8. if (a.length > size)
  9. a[size] = null;
  10. return a;
  11. }