Java——ArrayList的add()原理解析和remove()遇到的坑

淩亂°似流年 2022-02-20 07:16 339阅读 0赞

虽然ArrayList是可变数组,但是为了提高性能我们在使用中应尽量提前估算容量,add()的时间复杂度为O(1),但是扩容会拉低性能,所以定义时应估算容量,减少扩容次数;remove()方法,每次删除要移动后边数组,所以时间复杂度为O(n),为提高性能,尽可能删除最后的数据。

一.Add()源码解析

数组扩容这是对ArrayList效率影响比较大的一个因素,虽然ArrayList可以自己扩容,但是扩容过程的空间复杂度应该是O(2n),因为需要拷贝一个次旧的数组。

现在看一下源码:

构造函数

new ArrayList()返回的是一个空的Object,所以新建的ArrayList初始容量为零。

new ArrayList(K)当K>0时,开辟K个空间;K=0,返回空的Object。

  1. private static final int DEFAULT_CAPACITY = 10;
  2. private static final Object[] EMPTY_ELEMENTDATA = {};
  3. private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
  4. transient Object[] elementData;
  5. private int size;
  6. public ArrayList() {
  7. this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
  8. }
  9. public ArrayList(int initialCapacity) {
  10. if (initialCapacity > 0) {
  11. this.elementData = new Object[initialCapacity];
  12. } else if (initialCapacity == 0) {
  13. this.elementData = EMPTY_ELEMENTDATA;
  14. } else {
  15. throw new IllegalArgumentException("Illegal Capacity: "+
  16. initialCapacity);
  17. }
  18. }

接下来看一下add()函数。ArrayList首次扩容,容量为10,以后每次扩容将新开辟空间,容量将会增加为原来的1.5倍,再将原来的数组复制到新的空间。

  1. public boolean add(E e) {
  2. ensureCapacityInternal(size + 1);//add,长度+1,增量后调用下边函数
  3. elementData[size++] = e;
  4. return true;
  5. }
  6. private void ensureCapacityInternal(int minCapacity) {
  7. if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
  8. //如果数组为空的话,min=Math.max(10,1)=10;
  9. minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
  10. }
  11. //调用下方函数
  12. ensureExplicitCapacity(minCapacity);
  13. }
  14. private void ensureExplicitCapacity(int minCapacity) {
  15. modCount++;
  16. //数组长度<10,add后的长度大于现有的数组长度,说明数组应该扩容了,调用下方函数
  17. if (minCapacity - elementData.length > 0)
  18. grow(minCapacity);
  19. }
  20. //扩容函数
  21. private void grow(int minCapacity) {
  22. // overflow-conscious code
  23. //获取旧数组长度
  24. int oldCapacity = elementData.length;
  25. //新数组长度=原来的1.5倍;通过右移1位,即oldCapacity/2
  26. int newCapacity = oldCapacity + (oldCapacity >> 1);
  27. if (newCapacity - minCapacity < 0)
  28. newCapacity = minCapacity;
  29. if (newCapacity - MAX_ARRAY_SIZE > 0)
  30. newCapacity = hugeCapacity(minCapacity);
  31. // minCapacity is usually close to size, so this is a win:
  32. //拷贝,new 新的长度的数组,赋值
  33. elementData = Arrays.copyOf(elementData, newCapacity);
  34. }

二.remove()的注意问题

每次删除一个数,后边的元素会往前移动,所以remove()方法有一些坑大家会忽略掉。

比如下面的代码,想要将偶数删除,输出结果是?

  1. @Test
  2. public void testRemove2(){
  3. List<Integer> integers = new ArrayList<>(5);
  4. integers.add(1);
  5. integers.add(2);
  6. integers.add(2);
  7. integers.add(4);
  8. integers.add(5);
  9. for (int i = 0; i < integers.size(); i++) {
  10. if (integers.get(i)%2==0){
  11. integers.remove(i);
  12. }
  13. }
  14. System.out.println(integers);
  15. }

结果是:

  1. [1,2,5]

显然,结果和需求是不相符的。因为在删除第一个 2 的时候,后边的数字统一往前移动,但是下标 i 会继续+1,所以下一次循环 i 指向的是 4 ,所以第二个2根本没有执行 if 代码块,自然没有删除。

下面的代码也是很常见的错误写法:

  1. @Test
  2. public void testRemove5(){
  3. List<String> strings = new ArrayList<>();
  4. strings.add("a");
  5. strings.add("b");
  6. strings.add("c");
  7. strings.add("d");
  8. int size = strings.size();
  9. for (int i = 0; i < size; i++) {
  10. strings.remove(i);
  11. }
  12. }

数组size是变化的,用遍历size长度肯定是不对的,所以运行结果会报错。

正确写法如下:使用Iterator遍历删除是最安全的。

  1. @Test
  2. public void testRemove7(){
  3. List<String> strings = new ArrayList<>();
  4. strings.add("a");
  5. strings.add("b");
  6. strings.add("c");
  7. strings.add("d");
  8. Iterator<String> iterator = strings.iterator();
  9. while (iterator.hasNext()){
  10. String next = iterator.next();
  11. iterator.remove();
  12. }
  13. System.out.println(strings);
  14. }

发表评论

表情:
评论列表 (有 0 条评论,339人围观)

还没有评论,来说两句吧...

相关阅读