ArrayList 源码分析

前言快速到底

  集合的作用就是以一定的方式组织、存储数据。分析集合,有四点要特别注意:

1、是否允许存储 null

2、是否允许存储重复数据

3、是否有序,有序的意思是读取数据的顺序和存放数据的顺序是否一致

4、是否线程安全

ArrayList

首先我们来看一下 ArrayList 类在 Collection 集合框架中的结构图,蓝线代表继承关系绿线代表接口实现;从 https://blog.csdn.net/m0_37241851/article/details/59488596 贴过来的一张图

图 1-1:ArrayList 关系图

从 ArrayList 的结构图来看 ArrayList 类

  • 继承 AbstractList,实现了 List。提供了添加、删除、修改、遍历等功能
  • 实现 RandomAccess 接口,提供了随机访问功能,也就是通过索引快速访问数组元素
  • 实现 Cloneable 接口,重写clone( )函数,可以被复制
  • 实现 java.io.Serializable 接口,支持序列化,能通过序列化去传输

看一下 ArrayList 的定义,它是支持泛型的

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable

ArrayList 是最常用的集合之一了,ArrayList 底层存储数据是通过数组实现的。接下来,我们看一下 ArrayList 集合里面有哪些重要的属性

   // 数组默认的初始容量
    private static final int DEFAULT_CAPACITY = 10;

    // 用于创建一个空的实例
    private static final Object[] EMPTY_ELEMENTDATA = {};

    // 用于保存 List 数据的数组
    private transient Object[] elementData;

    // 数组中元素的个数
    private int size;  // 表示 ArrayList 里面元素的个数,size 只随着调用 add、remove 方法的次数进行自增或者自减的,所以 add 了一个 null 进入 ArrayList,size 也会加 1;size() 方法返回的值就是这个值

我们看一下 ArrayList 中的方法摘要:

// 属于 Collection 接口的方法

boolean add(E object) boolean addAll(Collection<? extends E> collection) void clear() boolean contains(Object object) boolean containsAll(Collection<?> collection) boolean equals(Object object) int hashCode() boolean isEmpty() boolean remove(Object object) boolean removeAll(Collection<?> collection) boolean retainAll(Collection<?> collection) int size() <T> T[] toArray(T[] array) Object[] toArray()
// 属于 AbstractCollection 抽象类中的方法

void                add(int location, E object)
boolean             addAll(int location, Collection<? extends E> collection)
E                   get(int location)
int                 indexOf(Object object)
int                 lastIndexOf(Object object)
ListIterator<E>     listIterator(int location)
ListIterator<E>     listIterator()
E                   remove(int location)
E                   set(int location, E object)
List<E>             subList(int start, int end)
// ArrayList 自己新增的方法
Object clone() void ensureCapacity(int minimumCapacity) void trimToSize() void removeRange(int fromIndex, int toIndex)

从上面的类的 API 来看事实上涉及到集合的本身的东西基本就在 Collection 接口中定义好了

接下来我们看看源码

首先先来看看构造方法,ArrayList 提供了三种方式的构造器,第一种是我们最常用的,但是在 ArrayList 存储的数据很多的时候并不好,原因在下面将会解释。

// ArrayList 无参数构造参数,默认容量 10
public ArrayList() {
  super();
  this.elementData = EMPTY_ELEMENTDATA;
}

第二种是自定义初始化容量的构造方法

// ArrayList 带容量大小的构造函数。
public ArrayList(int initialCapacity) {
  super();
  if (initialCapacity < 0)
  throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
  
this.elementData = new Object[initialCapacity]; // 在这里把 elementData 数组的长度变成了 20 }

Demo

public class Test
{
    public static void main(String[] args)
    {
        ArrayList<Object> arrayList = new ArrayList<>(20);
        System.out.println(arrayList.size());
    }
}

运行结果

0

我们看一下 size() 方法

public int size() {
  return size;
}

显然,结果是 0 不奇怪,要知道这个赋初始化容量改变的是 elementData[ ] 数组 的长度,它不再是 0 了,而是 20;注意区分 list 的元素个数用 size 表示,而 elementData 的 length 就是数组的长度

public class Test
{
    public static void main(String[] args)
    {
        Object[] obj = new Object[20];
        System.out.println(obj.length);
    }
}

运行结果

20

第三种

// 构造一个包含指定 collection 的元素的列表,这些元素是按照该 collection 的迭代器返回它们的顺序排列的。
public ArrayList(Collection<? extends E> c) {
  elementData = c.toArray();   // 调用 toArray() 方法把 collection 转换成数组,这个 toArray 方法调用的要是 System.arraycopy() 方法
  size = elementData.length;   // 把数组的长度赋值给 ArrayList 的 size 属性(这里的 size = elementData.length,并不意味着其他地方也是的)
  

  
// c.toArray might (incorrectly) not return Object[] (see 6260652) 这种情况暂时还不了解   if (elementData.getClass() != Object[].class)     elementData = Arrays.copyOf(elementData, size, Object[].class); }

接下来看一下 add(E e) 方法

// 添加单个元素
public
boolean add(E e) {   ensureCapacityInternal(size + 1); // 进行扩容检查 Increments modCount!!   elementData[size++] = e;  // 在这里我要解释以下 size 只有通过 add 和 remove 方法才会改变,对于上面第二个构造方法,设置了初始化容量与 size 大小无关   return true; // 这个按照顺序插入元素的 add 方法返回值是 true }

当 ArrayList 进行 add 操作的时候,如果添加的元素超出了数组的长度,怎么办?

add 方法会去调用下面的方法,根据传入的最小需要容量 minCapacity 来和数组的容量长度对比,若 minCapactity 大于或等于数组容量,则需要进行扩容。

private void ensureCapacityInternal(int minCapacity) { // 最小需要容量 = 当前数组元素个数 + 1
        if (elementData == EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }

    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // 最小需要容量 > 当前数组元素个数的时候;其实是必然的,因为 minCapacity = size + 1,而 elementData.lenth = size
        if (minCapacity - elementData.length > 0) // 所以,minCapacity - elementData.length = 1
            grow(minCapacity);            // 所以,每次 add 的时候都会进行扩容,复制的操作

// 注意:上面分析的只是针对于第一种构造方法;如果使用第二种构造方法,比如开始设置了 elementData 数组的长度为 20,那么这里
就不会成立,下面就不会进行 grow 扩容,也不会继续数组赋值,所以如果我们能够预测我们的 arrayList 存储多少值,我们就先分配好
数组的容量,后面就不会每次添加元素的收进行扩容。
}

扩容的时候会去调用grow()方法来进行动态扩容,在grow中采用了位运算,我们知道位运算的速度远远快于整除运算:

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

// 这才是动态扩展的精髓,看到这个方法,ArrayList 瞬间被打回原形
private void grow(int minCapacity) {
     int oldCapacity = elementData.length;
     // 将 oldCapacity 右移一位,其效果相当于 oldCapacity/2,整句的结果就是设置新数组的容量扩展为原来数组的 1.5 倍
     int newCapacity = oldCapacity + (oldCapacity >> 1);
     // 再判断一下新数组的容量够不够,够了就直接使用这个长度创建新数组, 
     // 不够就将数组长度设置为需要的长度
     if (newCapacity - minCapacity < 0)
         newCapacity = minCapacity;
     //判断有没超过最大限制,如果超出限制则调用 hugeCapacity
     if (newCapacity - MAX_ARRAY_SIZE > 0)
         newCapacity = hugeCapacity(minCapacity);
     // 将原来数组的值 copy 新数组中去, ArrayList 的引用指向新数组
     // 这会新创建数组,如果数据量很大,重复的创建的数组,那么还是会影响效率,
     // 因此鼓励在合适的时候通过构造方法指定默认的 capaticy 大小
     elementData = Arrays.copyOf(elementData, newCapacity);
 }
private static int hugeCapacity(int minCapacity) {
  if (minCapacity < 0) // overflow
  throw new OutOfMemoryError();
  return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}

有一点需要注意的是,容量拓展,是创建一个新的数组,然后将旧数组上的数组 copy 到新数组,这是一个很大的消耗,所以在我们使用 ArrayList 时,最好能预计数据的大小,在第一次创建时就申请够内存。

看一下 JDK1.6 的动态扩容的实现原理:

public void ensureCapacity(int minCapacity) {
  modCount++;
  int oldCapacity = elementData.length;
  if (minCapacity > oldCapacity) {
    Object oldData[] = elementData;
    int newCapacity = (oldCapacity * 3)/2 + 1;
    if (newCapacity < minCapacity)
    newCapacity = minCapacity;
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
  }
}

从代码上,我们可以看出区别: 

  • 在容量进行扩展的时候,其实例如整除运算将容量扩展为原来的 1.5 倍加 1,而 jdk1.7 是利用位运算,从效率上,jdk1.7 就要快于 jdk1.6。     
  • 在算出 newCapacity 时,其没有和 ArrayList 所定义的 MAX_ARRAY_SIZE 作比较,为什么没有进行比较呢,原因是 jdk1.6 没有定义这个MAX_ARRAY_SIZE 最大容量,也就是说,其没有最大容量限制的,但是 jdk1.7 做了一个改进,进行了容量限制。

 接下来我们看以下这个 add(int index, E element) 方法

// 在指定位置添加元素
public void add(int index, E element) {
  rangeCheckForAdd(index);

  ensureCapacityInternal(size + 1);  // 扩容检查

  // 将 index 位置后面的数组元素统一后移一位,把 index 位置空出来

  System.arraycopy(elementData, index, elementData, index + 1,size - index); // 这个方法多熟悉熟悉
  elementData[index]
= element;   size ++; }

add 方法暂时就到这里,其余的大同小异,现在看看删除方法

// 根据索引删除
public
E remove(int index) {
  rangeCheck(index);   modCount
++;
  E oldValue
= elementData(index);   int numMoved = size - index - 1; // 要删除元素的后面还有多少个元素
  
if (numMoved > 0) //也就是说如果删除的是最后一个元素,numMoved = 0,后面没有元素了就不需要拷贝了
  System.arraycopy(elementData, index
+1, elementData, index, numMoved);
  elementData[
--size] = null; // 把最后一个元素置为 null,clear to let GC do its work   return oldValue; // 这个 remove(int index) 方法有意思,还返回一个被删除的元素 }

再看一下另一个根据元素来删除的 remove 方法

 public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }
private void fastRemove(int index) {
        modCount++;
        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
    }

通过删除方法,可以发现 ArrayList 中允许存储 NULL 值

通过元素值删除时,只删除查找到的 第一个

但添加元素时有扩容操作,但是删除元素时却没有缩容操作,那如果数组被删除到很小,那数组中大量空间将会被闲置,这时候 ArrayList 提供了 trimToSize()方法,可以将数组大小设置为当前 size。不过这个方法需要手动自己调用,ArrayList 中的其他方法不会调用。

public void trimToSize() {
        modCount++;
        if (size < elementData.length) {
            elementData = Arrays.copyOf(elementData, size);
        }
    }

在这里它会取两个中最小的作为数组的长度

System.arraycopy(original, 0, copy, 0,Math.min(original.length, newLength));

ArrayList的优缺点

优点

1、ArrayList 底层以数组实现,是一种随机访问模式,再加上它实现了 RandomAccess 接口,因此查找也就是 get 的时候非常快。

2、ArrayList 在顺序添加一个元素的时候非常方便,只是往数组里面添加了一个元素而已

缺点

1、删除元素的时候,涉及到一次元素复制,如果要复制的元素很多,那么就会比较耗费性能

2、插入元素的时候,涉及到一次元素复制,如果要复制的元素很多,那么就会比较耗费性能

因此,ArrayList比较适合顺序添加、随机访问的场景

总结

查找开销是 O(1) 级别,插入和删除是 O(n) 级别

ArrayList 会在每次添加新元素时,自己进行扩容操作,扩展为原来的1.5倍大小

ArrayList 和 Vector 的区别

ArrayList 是线程非安全的,这很明显,因为 ArrayList 中所有的方法都不是同步的,在并发下一定会出现线程安全问题。那么我们想要使用 ArrayList 并且让它线程安全怎么办?一个方法是用 Collections.synchronizedList 方法把你的 ArrayList 变成一个线程安全的 List,比如:

List<String> synchronizedList = Collections.synchronizedList(list);
synchronizedList.add("aaa");
synchronizedList.add("bbb");
for (int i = 0; i < synchronizedList.size(); i++)
{
    System.out.println(synchronizedList.get(i));
}

另一个方法就是 Vector,它是 ArrayList 的线程安全版本,其实现 90% 和 ArrayList 都完全一样,区别在于:

1、Vector 是线程安全的,ArrayList 是线程非安全的

2、Vector 可以指定增长因子,如果该增长因子指定了,那么扩容的时候会每次新的数组大小会在原数组的大小基础上加上增长因子;如果不指定增长因子,那么就给原数组大小 * 2,源代码是这样的:

int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);

为什么 ArrayList 的 elementData 是用 transient 修饰的?

最后一个问题,我们看一下 ArrayList 中的数组,是这么定义的:

private transient Object[] elementData;

不知道大家有没有想过,为什么 elementData 是使用 transient 修饰的呢?关于这个问题,说说我的看法。我们看一下 ArrayList 的定义:

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable

看到 ArrayList 实现 Serializable 接口,这意味着 ArrayList 是可以被序列化的,用 transient 修饰 elementData 意味着我不希望 elementData 数组被序列化。这是为什么?因为序列化 ArrayList 的时候,ArrayList 里面的 elementData 未必是满的,比方说 elementData 有 10 的大小,但是我只用了其中的 3 个,那么是否有必要序列化整个 elementData 呢?显然没有这个必要,因此 ArrayList 中重写 writeObject 方法:

private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{
  // Write out element count, and any hidden stuff
  int expectedModCount = modCount;
  s.defaultWriteObject();
  // Write out array length
  s.writeInt(elementData.length);
    // Write out all elements in the proper order.
  for (int i=0; i<size; i++)
    s.writeObject(elementData[i]);
  if (modCount != expectedModCount) {
    throw new ConcurrentModificationException();
    }
}

每次序列化的时候调用这个方法,先调用 defaultWriteObject() 方法序列化 ArrayList 中的非 transient 元素,elementData 不去序列化它,然后遍历elementData,只序列化那些有的元素,这样:

1、加快了序列化的速度

2、减小了序列化之后的文件大小

不失为一种聪明的做法,如果以后开发过程中有遇到这种情况,也是值得学习、借鉴的一种思路

 

转载于:https://www.cnblogs.com/tkzL/p/8890741.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值