3、Java集合源码分析-ArrayList

1. 介绍

  1. ArrayList是一个实现了List接口的变长数组。它实现了List定义的操作,并可以存储任意类型的元素(包括null)。
  2. 内部数据使用数组存储,因此从ArrayList获取数据是常量级的。
  3. ArrayList是不同步的,多个线程同时操作一个ArrayList实例的时候,可能会出现不可预估的问题。

这是ArrayList类的定义,继承自AbstractList,实现了List接口、RandomAccess(随机访问接口)、Cloneable(可复制)、Serializable(可序列化接口)。

1.1. 实现RandomAccess接口

List实现使用的标记接口,表明它支持快速(通常是常量时间)随机访问。该接口的目的主要是允许常用算法改变其行为,以便在应用于随机或者顺序访问列表时提供良好的性能。

1.2. 实现Cloneable接口

标记接口,调用Object.clone()方法的时候必须实现这个接口,否则会抛出异常:CloneNotSupportedException。

1.3. 实现Serializable接口

标记接口,表示能被序列化。

1.4. 实现List接口,继承AbstractList

我们看AbstractList源码我们会发现它继承了List接口,所以这里其实可以不用实现List接口,这个应该是Java开发者遗留的小问题。

  1. List接口中规定了实现List接口的子类需要实现的方法,比如常用的get、add、remove、set、indexOf等等。
  2. AbstractList给出了一些默认的实现,子类可以重写或者直接复用。

2. 字段

  1. 内部数组

  1. 元素数量

  1. 静态常量

重点看下面定义的两个Object数组都是空的数组实例。

这两个的作用是用于了解ArrayList在添加元素的时候是如何扩容的。会在后面详细解释。

3. 构造器

  1. 无参构造

  1. 参数为初始容量的构造器

注意:无参构造器和初始容量为0时,内部维护的数组被设定了两个不一样的空数组。

这里在后面添加元素时扩容方式不同

  1. 参数为集合的构造器

注意:传入集合为空的时候和初始容量为0的构造器一样,都是空数组1;区别于无参构造器。

4. 添加数据+扩容机制(重点)

ArrayList内部是通过维护了一个Object数组实现的,向ArrayList中添加数据其实就是向数组添加数据。

但是我们知道数组的大小是在创建的时候就确定了的,但是我们在向ArrayList中添加数据的时候,好像没有这个问题,这就是因为它的扩容机制。

  1. 添加元素

  1. 计算并确定内部容量

  1. 扩容(如果需要的话)

  1. 总结一下

(1)通过无参构造器创建一个新对象,初始容量是0。

(2)第1次添加元素时,创建一个长度为10的数组,并将该元素赋值到数组的第1个位置。

(3)第2次添加元素时,size+1<数组长度10,直接添加到数组的第2个位置。

(4)第10次添加元素时,size+1=10=数组长度10,直接添加到数组的第10个位置。

(5)第11次添加元素时,size+1=11>数组长度11,需要扩容,计算得到新的数组长度应该为10+10/2=15,创建一个长度为15的数组将之前数组中的数据复制进去。将元素添加到新数组的第11个位置。

(6)一直添加元素,直到由于int溢出导致计算出来的新长度<size+1时,扩容的长度设置为size+1。创建新数组复制数据并添加新数组。

(7)添加元素到达临界值时,进入大数判断,将新长度设置为int的最大值,创建新数组复制数据并添加新数组。

(8)第int的最大值+1次添加元素时,抛出越界异常。

5. 删除元素

5.1. 根据索引删除元素

由于删除指定索引位置的元素,在数组中的实现是将当前元素删除后,将索引后面所有的数据往前移动一位。即先计算当前元素后是否还有数据,由于索引从0开始,所以size-index-1 就是index后面元素的总数;如果后面有数据的话,内部的Object自复制(将index+1开始的所有的数据重新复制到Object数组:从index开始)。

此时由于最后一个位置没有被覆盖,所以需要将最后一个位置删除,设置为null,主要是将真实数据的引用移除以便于垃圾回收。

来举个栗子:

  1. 先判断索引4位置后还有几个数据,size(8)-index(4)-1=3
  2. 有数据,需要从index+1=5索引开始共计3个数据复制到数组从index=4开始的位置。

用图片表示为:

5.2. 删除指定元素(只删除第一次出现的)

小贴士:开发中如果碰到可能会有null的地方,记得做null判断,防止空指针异常。

我们发现,再找到第一个相同的元素之后,直接return了,所以这个方法只能删除第一个匹配到的元素。

6. 修改元素

7. 查询元素

7.1. 根据索引查询元素

7.2. 根据元素查询索引(碰到的第一个)

8. 遍历方式(重点)

8.1. 普通for循环

ArrayList list = new ArrayList();
for (int i = 0; i < 10; ++i) {
    list.add(i);
}
for (int i = 0; i < list.size(); ++i) {
    System.out.print(list.get(i) + " ");
}

8.2. 迭代器(重点)

ArrayList list = new ArrayList();
for (int i = 0; i < 10; ++i) {
    list.add(i);
}
Iterator it = list.iterator();
while (it.hasNext()) {
    System.out.print(it.next() + " ");
}

来,我们看下具体的实现即注意事项:

我们在文章就给出了ArrayList的继承关系,ArrayList实现了List接口,List继承自Collection接口,Collection接口继承自Iterable接口,在这个接口中有一个方法是:

我们可以通过该方法获取一个迭代器来辅助进行集合遍历。

我们来看下ArrayList是如何实现该方法的。

在ArrayList的内部实现中,是返回了一个Itr对象。

8.2.1. 基础属性

8.2.2. 获取元素、删除元素

8.2.3. 注意事项

我们可以看到在next()和remove()中都进行checkForComodification()判断,表示在进行迭代的过程中不可以直接调用Arraylist的add()、remove()对列表进行修改。

如果我们需要在迭代过程中需要进行元素删除的话,可以使用迭代器中的remove()方法。

8.3. 增强for循环(迭代器变种)

ArrayList list = new ArrayList();
for (int i = 0; i < 10; ++i) {
    list.add(i);
}
for (Object i : list) {
    System.out.print(i + " ");
}

这种方法是Java的一种语法糖,通过编译后的class文件反编译后我们可以看到会变为使用迭代器

ArrayList list = new ArrayList();
int i;
for(i = 0; i < 10; ++i) {
    list.add(i);
}

Iterator var4 = list.iterator();
while(var4.hasNext()) {
    Object i = var4.next();
    System.out.print(i + " ");
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值