java集合系列2-从源码解析ArrayList

不经历风雨,怎能见彩虹。做个快乐的程序员!
注:本文是基于 JDK1.8.0_144 版本
ArrayList 继承 AbstractList并实现了包括 List 在内的一堆接口。大伙平时用的应该也比较多,如果你没有研究过源码的话,可能还是会心虚。毕竟在面试的时候人家不会只问你会不会 add(),get()这些东东,而是问一些原理方面问题,比如:
为什么 ArrayList 更新会比查询的性能慢很多?
ArrayList 一般都是如何遍历?
ArrayList 为什么是线程不安全的?

等等,接下来我们就带着这些问题来分析源码,相信看完后你对 ArrayList 的印象定会透彻很多清晰很多。

1. 变量

/**
     * Default initial capacity.
     * 默认初始容量
     */
    private static final int DEFAULT_CAPACITY = 10;

    /**
     * Shared empty array instance used for empty instances.
     * 空数组,ArrayList基于数组实现,更新数组时是通过创建新数组并拷贝原数组和更新的内容
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /**
     * Shared empty array instance used for default sized empty instances. We
     * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
     * first element is added.
     * 空对象,若使用默认构造函数创建,则默认对象内容默认是该值
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     * The array buffer into which the elements of the ArrayList are stored.
     * The capacity of the ArrayList is the length of this array buffer. Any
     * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
     * will be expanded to DEFAULT_CAPACITY when the first element is added.
     * 当前数据存放的地方,transient-当前对象不参与序列号
     */
    transient Object[] elementData; // non-private to simplify nested class access

    /**
     * The size of the ArrayList (the number of elements it contains).
     * 当前数组长度
     * @serial
     */
    private int size;

这一部分比较简单,看注释即可,上面的代码只是在源码的基础上增加了中文注释

2. 构造函数

ArrayList 包含三个构造函数,下面我们一个个分析

2.1 带 int 类型的构造函数

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);
        }
    }
若传入参数,则代表指定 ArrayList 的初始化数组长度。参数大于等于0时,使用用户给定的参数初始化;参数小于0时,抛出异常

2.2 无参构造器

public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
若不传参数,则使用默认构造方法创建对象

2.3 带 Collection 对象的构造函数

public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }
2.3 带 Collection 对象的构造函数
1)把 Collection 转换为数组并将数组的地址赋给 elementData
2)更新 Size 的值并判断大小,若等于0,则直接赋值 EMPTY_ELEMENTDATA;若不为0,则执行 Arrays.copyOf()将 Collection内容拷贝到 elementData 中。
注:toArray()为浅拷贝(只复制对象传递引用,不复制实例),copyOf()为深拷贝(创建新实例并复制)

3. 添加方法

ArrayList 提供了两种类型的 Add() 方法,下面我们分开来说明

3.1 add(E e)方法

public boolean add(E e) {
        // size+1 为需要的最小数组大小,该方法确保数组有足够的空间可以添加新的元素(创建满足需要的容量大小的数组并拷进原数组元素)
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        // 把数组的最后以为设置为元素 e,然后改变 size 的值
        elementData[size++] = e;
        return true;
    }
private void ensureCapacityInternal(int minCapacity) {
        // 若数组为默认函数构造的默认初始数组,则设置数组的最小值为 default_capacity
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        // 进一步确保数组的容量
        ensureExplicitCapacity(minCapacity);
    }
private void ensureExplicitCapacity(int minCapacity) {
        // 数组修改次数加1
        modCount++;

        // overflow-conscious code
        // 若当前数组的容量小于要求的最小数组容量,则扩充数组大小为 minCapacity
        if (minCapacity - elementData.length > 0)
            // 扩充数组容量
            grow(minCapacity);
    }
private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        // 当前容量右移1位相当于除以2,即新的数组容量扩充为原来的1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
1)调用 ensureCapacityInternal(size+1)方法以确保有足够的空间
2)给数组最后以为赋值为新元素 e,并重置 size 大小

3.2 add(int index, E element)方法

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

        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }
private void rangeCheckForAdd(int index) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
1)检查索引 index 是否溢出
2)确保数组有足够的空间存放新增的元素
3)将数组 index 后的元素全部后移一位(通过拷贝到新数组的方式)
4)将 index 位置设置为新增的元素
从这里我们就可以看出,ArrayList 的增加操作是通过数组拷贝的方式实现,若数组元素很多的时候,性能相对很低,也进一步印证了文章前面我们提到的问题,结合注释,上面的代码很容易理解

4. 删除方法

ArrayList 提供了3个删除方法,下面一一介绍

4.1 带索引参数的删除

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;
}
通俗的说,删除该索引位置的值就是将该数组中指定索引位置后面所有的元素拷贝到前一个位置,再把最后一个位置的值置为null

4.2 带元素参数的删除

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
    }
此方法是直接遍历数组,找到该元素对应位置的索引值,再调用 fastRemove(index) 方法,这个方法同 4.1说过的,也是以数组拷贝的方式实现

4.3 带Collection 集合参数的删除

public boolean removeAll(Collection<?> c) {
        // 非空检查
        Objects.requireNonNull(c);
        return batchRemove(c, false);
    }
private boolean batchRemove(Collection<?> c, boolean complement) {
        final Object[] elementData = this.elementData;
        int r = 0, w = 0;
        boolean modified = false;
        try {
            for (; r < size; r++)
                if (c.contains(elementData[r]) == complement)
                    elementData[w++] = elementData[r];
        } finally {
            // Preserve behavioral compatibility with AbstractCollection,
            // even if c.contains() throws.
            if (r != size) {
                System.arraycopy(elementData, r,
                                 elementData, w,
                                 size - r);
                w += size - r;
            }
            if (w != size) {
                // clear to let GC do its work
                for (int i = w; i < size; i++)
                    elementData[i] = null;
                modCount += size - w;
                size = w;
                modified = true;
            }
        }
        return modified;
    }
首先确保Collection 集合非空,再调用 batchRemove() 方法,batchRemove()方法的实现也很简单,先对该数组遍历,把不包含在Collection集合中的元素重新设置到 elementData数组(包含在Collection中的是删除部分),此时的element Data的长度设置为数组的size,其余的置为null。一句话概括就是,删除 List 中包含 Collection的所有元素。比如:
list = [a,b,c,d,e]  collection = [c,d,k],list.removeAll(collection)=[a,b,e]

5. ArrayList 遍历

数组的遍历很简单,篇幅太长了,我就举两个例子:

5.1 for 循环遍历

// List<String>中的String可以填其他类型或对象类型
		List<String> list = new ArrayList<String>();
		list.add("");
		for(int i=0; i<list.size(); i++){
			String a = list.get(i);
		}
5.2 特殊遍历

// List<String>中的String可以填其他类型或对象类型
		List<String> list = new ArrayList<String>();
		list.add("");
		for(String str: list){
			System.out.println(str);
		}

5.3 迭代器遍历

List<String> list = new ArrayList<String>();
		list.add("do");
		Iterator ite = list.iterator();
		while(ite.hasNext()){
			System.out.println(ite.next());
		}

前面两种遍历方式有点类似,这几个例子应该很容易看懂

6. 其他方法
上面所说的这些方法都看明白的话,其他的查询等方法都是一个套路,篇幅太长,这里就不想赘述了,另外还有序列化和反序列化两个方法 writeObject() 和 readObject(),主要的作用就是把 List 对象写到输出流或从输入流读出

另外,文章开头还提到了为什么ArrayList 是线程不安全的呢?

线程不安全就是说在多线程访问的时候,没有对数据进行保护,可能导致与预期的结果不一致,使得数据混乱

我们从add() 方法来分析,首先add() 方法有两个步骤:

1)扩充数组的容量(是容量并非size值)

2)在数组的最后一位设置为添加的元素,并对size 执行 ++操作

假设线程1访问某个list,在索引0处添加元素,执行完上述第一步时,线程2同时访问该list并对其添加元素,这样最终就导致了两个线程都在索引0处添加了元素,size其实就只加了一次,数组添加了两个元素,可size却为1,这就是线程不安全的

示例:

public static void main(String[] args) throws InterruptedException {
		List list = new ArrayList();
		for(int i=0; i<1000; i++){
			new Thread(new Runnable() {
				@Override
				public void run() {
					list.add("a");
				}
			}).start();
		}
		Thread.sleep(2000);
		System.out.println("list:"+list);
		System.out.println("size:"+list.size());
	}
同时启动的线程数量足够多时,才能看到效果







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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值