ArrayList

面试题:
方同学,听说你最近在家很努力学习HashMap?那考你个ArrayList知识点
你看下面这段代码输出结果是什么?

    public static void main(String[] args) {
        List<String>list=new ArrayList<>(16);
        list.add(2,"1");
        System.out.println(list.get(0));
    }

嗯?不知道,眼睛看题,看我脸干啥?好好好,告诉你吧,这样会报错!至于为什么,回家看看书吧。
在这里插入图片描述
那下面我分析一下ArrayList的源码

ArrayList的数据结构:Array + List = 数组 + 列表 = ArrayList = 数组列表即就是ArrayList
ArrayList的数据结构是基于数组实现的,只不过这个数组不像我们普通定义的数组,它可以在ArrayList的管理下插入数据时按需动态扩容、数据拷贝等操作

源码分析解答刚才的问题在哪里

1:初始化

List<String> list = new ArrayList<String>(10);

public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

 /**
  * Constructs an empty list with the specified initial capacity.
  *
  * @param  initialCapacity  the initial capacity of the list`在这里插入代码片`
  * @throws IllegalArgumentException if the specified initial capacity
  *         is negative
  */
 public ArrayList(int initialCapacity) {
     if (initialCapacity > 0) {
         this.elementData = new Object[initialCapacity];
     } else if (initialCapacity == 0) {
         this.elementData = EMPTY_ELEMENTDATA;
     } else {
         throw new IllegalArgumentException("Illegal Capacity: "+
                                            initialCapacity);
     }
 }

1.1 add方法

ArrayList<String> list = new ArrayList<String>();
list.add("aaa");
list.add("bbb");
list.add("ccc");

1.2内部类方式

 ArrayList<String> list = new ArrayList<String>(){
            {
                add("aaa");
                add("bbb");
                add("ccc");
            }
        };

1.3 Arrays.asList

 ArrayList<String> list = new ArrayList<String>(Arrays.asList("aaa", "bbb", "ccc"));

注意:
Arrays.asList 构建的集合,不能赋值给 ArrayList
Arrays.asList 构建的集合,不能再添加元素
Arrays.asList 构建的集合,不能再删除元素

那这到底为什么呢,因为Arrays.asList构建出来的List与new ArrayList得到的List,压根就不是一个List!类关系图如下;
在这里插入图片描述
从以上的类图关系可以看到;

这两个List压根不同一个东西,而且Arrasys下的List是一个私有类,只能通过asList使用,不能单独创建。
另外还有这个ArrayList不能添加和删除,主要是因为它的实现方式,可以参考Arrays类中,这部分源码;private static class ArrayList extends AbstractList implements RandomAccess, java.io.Serializable
此外,Arrays是一个工具包,里面还有一些非常好用的方法,例如;二分查找Arrays.binarySearch、排序Arrays.sort等

1.4 Collections.ncopies

ArrayList<Integer> list = new ArrayList<Integer>(Collections.nCopies(10, 0));

Collections.nCopies 是集合方法中用于生成多少份某个指定元素的方法,接下来就用它来初始化ArrayList,这会初始化一个由10个0组成的集合。

2. 插入

ArrayList对元素的插入,其实就是对数组的操作,只不过需要特定时候扩容.

2.1普通插入

List<String> list = new ArrayList<String>();
list.add("aaa");
list.add("bbb");
list.add("ccc");

当我们依次插入添加元素时,ArrayList.add方法只是把元素记录到数组的各个位置上了,源码如下;

/**
 * Appends the specified element to the end of this list.
 *
 * @param e element to be appended to this list
 * @return <tt>true</tt> (as specified by {@link Collection#add})
 */
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

这是插入元素时候的源码,size++自增,把对应元素添加进去。

2.2插入时扩容

在前面初始化部分讲到,ArrayList默认初始化时会申请10个长度的空间,如果超过这个长度则需要进行扩容,那么它是怎么扩容的呢?

从根本上分析来说,数组是定长的,如果超过原来定长长度,扩容则需要申请新的数组长度,并把原数组元素拷贝到新数组中,如下图;
在这里插入图片描述
图中介绍了当List结合可用空间长度不足时则需要扩容,这主要包括如下步骤;

  1. 判断长度充足;ensureCapacityInternal(size + 1);
  2. 当判断长度不足时,则通过扩大函数,进行扩容;grow(int minCapacity)

扩容的长度计算;int newCapacity = oldCapacity + (oldCapacity >> 1);,旧容量 +
旧容量右移1位,这相当于扩容为原来容量的(int)3/2。 4. 10,扩容时:1010 + 1010 >> 1 = 1010 + 0101
= 10 + 5 = 15 2. 7,扩容时:0111 + 0111 >> 1 = 0111 + 0011 = 7 + 3 = 10

  1. 当扩容完以后,就需要进行把数组中的数据拷贝到新数组中,这个过程会用到

Arrays.copyOf(elementData, newCapacity);,但他的底层用到的是;System.arraycopy

System.arraycopy;

@Test
public void test_arraycopy() {
    int[] oldArr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int[] newArr = new int[oldArr.length + (oldArr.length >> 1)];
    System.arraycopy(oldArr, 0, newArr, 0, oldArr.length);
    
    newArr[11] = 11;
    newArr[12] = 12;
    newArr[13] = 13;
    newArr[14] = 14;
    
    System.out.println("数组元素:" + JSON.toJSONString(newArr));
    System.out.println("数组长度:" + newArr.length);
    
    /**
     * 测试结果
     * 
     * 数组元素:[1,2,3,4,5,6,7,8,9,10,0,11,12,13,14]
     * 数组长度:15
     */
}

拷贝数组的过程并不复杂,主要是对System.arraycopy的操作。
上面就是把数组oldArr拷贝到newArr,同时新数组的长度,采用和ArrayList一样的计算逻辑;oldArr.length + (oldArr.length >> 1)

2.3 指定位置插入

list.add(2, "1");

到这,终于可以说说面试题,这段代码输出结果是什么,如下;

Exception in thread "main" java.lang.IndexOutOfBoundsException: Index: 2, Size: 0
	at java.util.ArrayList.rangeCheckForAdd(ArrayList.java:665)
	at java.util.ArrayList.add(ArrayList.java:477)
	at org.itstack.interview.test.ApiTest.main(ApiTest.java:14)

其实,一段报错提示,为什么呢?我们翻开下源码学习下。

2.3.1容量验证

public void add(int index, E element) {
    rangeCheckForAdd(index);
    
    ...
}

private void rangeCheckForAdd(int index) {
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

  • 指定位置插入首先要判断rangeCheckForAdd,size的长度。

  • 通过上面的元素插入我们知道,每插入一个元素,size自增一次size++。

  • 所以即使我们申请了10个容量长度的ArrayList,但是指定位置插入会依赖于size进行判断,所以会抛出IndexOutOfBoundsException异常。

在这里插入图片描述
指定位置插入的核心步骤包括;

  • 判断size,是否可以插入。
  • 判断插入后是否需要扩容;ensureCapacityInternal(size + 1);。
  • 数据元素迁移,把从待插入位置后的元素,顺序往后迁移。
  • 给数组的指定位置赋值,也就是把待插入元素插入进来。

部分源码:

public void add(int index, E element) {
	...
	// 判断是否需要扩容以及扩容操作
	ensureCapacityInternal(size + 1);
    // 数据拷贝迁移,把待插入位置空出来
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    // 数据插入操作                  
    elementData[index] = element;
    size++;
}

这部分源码的主要核心是在,System.arraycopy,上面我们已经演示过相应的操作方式。
这里只是设定了指定位置的迁移,可以把上面的案例代码复制下来做测试验证。

测试

List<String> list = new ArrayList<String>(Collections.nCopies(9, "a"));
System.out.println("初始化:" + list);

list.add(2, "b");
System.out.println("插入后:" + list);

结果

初始化:[a, a, a, a, a, a, a, a, a]
插入后:[a, a, b, a, a, a, a, a, a, a]

Process finished with exit code 0

指定位置已经插入元素1,后面的数据向后迁移完成

3.删除

有了指定位置插入元素的经验,理解删除的过长就比较容易了,如下图;

在这里插入图片描述

public E remove(int index) {
    rangeCheck(index);
    modCount++;
    E oldValue = elementData(index);
    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
    return oldValue;
}

删除的过程主要包括:

校验是否越界;rangeCheck(index);
计算删除元素的移动长度numMoved,并通过System.arraycopy自己把元素复制给自己。
把结尾元素清空,null。

这里我们做个例子:

@Test
public void test_copy_remove() {
    int[] oldArr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int index = 2;
    int numMoved = 10 - index - 1;
    System.arraycopy(oldArr, index + 1, oldArr, index, numMoved);
    System.out.println("数组元素:" + JSON.toJSONString(oldArr));
}

设定一个拥有10个元素的数组,同样按照ArrayList的规则进行移动元素。
注意,为了方便观察结果,这里没有把结尾元素设置为null。
测试结果:

数组元素:[1,2,4,5,6,7,8,9,10,10]

Process finished with exit code 0

可以看到指定位置 index = 2,元素已经被删掉。
同时数组已经移动用元素4占据了原来元素3的位置,同时结尾的10还等待删除。这就是为什么ArrayList中有这么一句代码;elementData[–size] = null

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

方大拿拿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值