Java源码阅读------ArrayList(1)

简介

java中的线性表容器,是一种顺序表的结构,核心是用一个数组来对数据进行存储,同时也是一个容器,可以实现数据的动态存储。ArrayList中使用了泛型的定义,所以在实际的存储中存储的对象不能是基本类型(int,double等)。

基本操作

先从一些基本设计来看:

    transient Object[] elementData;//实际用于存储的数组
	private static final int DEFAULT_CAPACITY = 10;//默认的容器容量
	private int size;//实际的数量

这里有一个区分一个是数据的数量,表述已经装入了这么多数量的数据,一个是容器的容量表示目前开辟的内存空间可以装这么多的数据。既然是一个顺序表的结构我们再看看其基本的操作。

初始化(构造函数)

这个类提供了三个构造函数。
默认生成空的线性表:

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

这里的空的表是初始化时将表置空,还有一个空表是在之后操作时表中没有数据了,为了区分这两个逻辑使用了两个引用加以区分。

private static final Object[] EMPTY_ELEMENTDATA = {};//空的表

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//初始化时的空表,这时的容器容量为默认容量。

自定义容器大小的初始化,在初始化的时候不使用默认的容量初始化数组:

public ArrayList(int initialCapacity) {
	if (initialCapacity > 0) {//判断传入的大小是否合法
    	this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {//如果是0就是建立空表
        this.elementData = EMPTY_ELEMENTDATA;
    } else {//异常处理
        throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
	}
}

最后一个构造函数是以一个原有容器中的数据为基础创建顺序表:

public ArrayList(Collection<? extends E> c) {
	elementData = c.toArray();//类型转换
    if ((size = elementData.length) != 0) {
        if (elementData.getClass() != Object[].class)
        	//转换之后的类型可能不是Object数组,再使用Arrays.copyOf()方法转化。
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {//如果传入的是个空的容器就设置一个空表
        this.elementData = EMPTY_ELEMENTDATA;
	}
}

Collection这个接口为所有容器提供了一些共有的操作定义

ArrayList实现了List接口,而Collection是所有相关容器接口的父接口。
所以使用Collection可以代指所有与容器相关的类。
toArray方法是Collection接口中定义的,可以将相关的数据转换为数组。当然ArrayList也实现了这个方法。但是实际的实现对象是Arrays类。

public Object[] toArray() {
	return Arrays.copyOf(elementData, size);
}

再到Arrays中看看:

//保持默认类型的转换
public static <T> T[] copyOf(T[] original, int newLength) {
	return (T[]) copyOf(original, newLength, original.getClass());
}

//指定类型的转换
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
	@SuppressWarnings("unchecked")
	//根据出传入的类型和大小size来初始化一个数组
	//如果不是Object类型就获取其默认类型(反射),并完成数组的空间开辟
    T[] copy = ((Object)newType == (Object)Object[].class)
            ? (T[]) new Object[newLength]
            : (T[]) Array.newInstance(newType.getComponentType(), newLength);
    //复制原先数组指定数量的项到新的数组中。Math.min()求出了具体复制的次数
    System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength));
    return copy;
}

arraycopy是System的native方法实现了数组的复制,参数是原数组的引用和起始位置,目的数组的引用和起始位置,还有要复制的项数。

按索引的取值与赋值

//检查索引范围
private void rangeCheck(int index) {
	if (index >= size)
		throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
//处理索引越界的异常信息,size为表长
private String outOfBoundsMsg(int index) {
	return "Index: "+index+", Size: "+size;
}

//按索引取值
E elementData(int index) {
    return (E) elementData[index];
}

public E get(int index) {
	rangeCheck(index);
    return elementData(index);
}

//按索引赋值
public E set(int index, E element) {
	rangeCheck(index);
	E oldValue = elementData(index);
	elementData[index] = element;
	return oldValue;//返回旧值
}

就是一般的对数组的操作。

按值的查找

从头查找

public int indexOf(Object o) {
	if (o == null) {
        for (int i = 0; i < size; i++)
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = 0; i < size; i++)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;
}

就是一个简单的判断根据是否为null来进行遍历查找,同理从后面查找有

public int lastIndexOf(Object o) {
	if (o == null) {
        for (int i = size-1; i >= 0; i--)
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = size-1; i >= 0; i--)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;
}

如果找到就返回相应的索引,如果没有找到就返回-1,同理可以判断是否在数组中含有某个元素。

public boolean contains(Object o) {
	return indexOf(o) >= 0;
}

容量扩充

插入操作会改变数据的数量大小。除此之外还要考虑容器的容量大小。还有容器的容量扩充。

//当容器容量过大时的处理
private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0)//容量小于0,int越界
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
    //容量在int的范围内则返回最大值MAX_ARRAY_SIZE
}

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;//容器容量的最大值

//容器扩充操作
private void grow(int minCapacity) {
	//传入的是所需容量的最小值,也就是插入后数据的个数
    int oldCapacity = elementData.length;//原先数据的个数,容器容量占满了
    int newCapacity = oldCapacity + (oldCapacity >> 1);//先扩充至1.5倍
    if (newCapacity - minCapacity < 0)//如果扩充不够就按最小的容量需求扩充
    	newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)//如果大于设置的最大值就进行大容量处理
    	newCapacity = hugeCapacity(minCapacity);
    //确定好容量后进行数组的扩充
    elementData = Arrays.copyOf(elementData, newCapacity);
}

//判断容量是否够用,如果不够就进行扩容
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;//记录数据变化的次数
	if (minCapacity - elementData.length > 0)
    	grow(minCapacity);
}

//判断容量是否够用,先将需要的大小与默认大小进行比较,保证容量最小保持在默认限度内
private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    ensureExplicitCapacity(minCapacity);
}

这样就实现了容量的判读机制与自动扩容机制,当插入后的容量不够后就可以自动的申请内存了,但是线性表的最大数据容量在一个int能表示的最大范围内。

容量裁剪

有容量的扩充也有容量的裁剪,trimToSize函数可以将当前的容器容量减到与数据容量相同。

public void trimToSize() {
	modCount++;//数据修改计数
    if (size < elementData.length) {//数据数量小于容量
	    elementData = (size == 0) ? EMPTY_ELEMENTDATA : Arrays.copyOf(elementData, size);
	    //根据实际存储的数据大小来修改数组的容量大小
    }
}

这里Arrays.copyOf()函数是将elementData数组的大小控制到size的大小。

插入数据

基本的插入操作分为在对应的位置插入和默认的插入表尾。

//默认的插入表尾
public boolean add(E e) {
	//先查看容器的大小是否够用,不够就自动扩容
    ensureCapacityInternal(size + 1);  
    elementData[size++] = e;//将数据插到表尾,表的长度加一
	return true;
}

//对应的位置插入
public void add(int index, E element) {
    rangeCheckForAdd(index);//越界判断
    ensureCapacityInternal(size + 1);//查看容器的大小是否够用,不够就自动扩容
    //将对应位置之后的元素向后移动一位
    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));
}

除了插入单个的数据之外,还有一次性插入多个数据:

//全部加到表尾
public boolean addAll(Collection<? extends E> c) {
    Object[] a = c.toArray();//先转换为数组
    int numNew = a.length;
    ensureCapacityInternal(size + numNew);  // 计算并判断容量是否足够
    System.arraycopy(a, 0, elementData, size, numNew);//移位并插入表尾
    size += numNew;//修改计数
    return numNew != 0;//插入的数据数量要大于0
}

//指定位置插入
public boolean addAll(int index, Collection<? extends E> c) {
    rangeCheckForAdd(index);//检查位置是否越界
    Object[] a = c.toArray();//容器数据转换为数组
    int numNew = a.length;
    ensureCapacityInternal(size + numNew); // 计算并判断容量是否足够
    int numMoved = size - index;// 计算插入的位置后面有多少数据要移位
    if (numMoved > 0)//进行移位操作为插入的数据腾出空间
        System.arraycopy(elementData, index, elementData, index + numNew, numMoved);
    System.arraycopy(a, 0, elementData, index, numNew);//将数据插入
    size += numNew;//修改计数
    return numNew != 0;//插入的数据数量要大于0
}

移除数据

移除数据的操作也分为按索引移除和按值移除:

//按索引移除并返回移除的值
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; //将移除的表尾置null,由gc回收
	return oldValue;//返回旧值
}

//按索引移除不返回移除的值
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; //将移除的表尾置null,由gc回收
}

//按数值移除
public boolean remove(Object o) {
	if (o == null) {//如果是移除null
    	for (int index = 0; index < size; index++)
	        if (elementData[index] == null) {//找到在表中的第一个null值
            	fastRemove(index);//移除
                return true;
            }
    } else {//移除的值不为null
        for (int index = 0; index < size; index++)
            if (o.equals(elementData[index])) {//找到在表中的第一个相关数值
                fastRemove(index);//移除
                return true;
            }
    }
    return false;
}

和插入一样还有按范围删除的:

protected void removeRange(int fromIndex, int toIndex) {
	modCount++;//数据修改计数
    int numMoved = size - toIndex;//计算在移除的位置后面有多少数据需要移动
    System.arraycopy(elementData, toIndex, elementData, fromIndex, numMoved);//数据移动
	int newSize = size - (toIndex-fromIndex);//统计计数
    for (int i = newSize; i < size; i++) {
        elementData[i] = null;//批量置null,由gc回收
    }
    size = newSize;//修改计数
}

还有批量删除:

//complement用于表示是保留数据(true),删除数据(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 {
        if (r != size) {//保持与AbstractCollection的兼容性,contains可能会抛出异常
            System.arraycopy(elementData, r, elementData, w, size - r);//将中断后的数据加到表头
            w += size - r;//最终数据的大小
        }
        if (w != size) {//如果判断后的数据数量比之前小(需要移除)
        	for (int i = w; i < size; i++)
                elementData[i] = null;//将之后的数据置null
            modCount += size - w;//数据修改计数
            size = w;//修改后的数据数量
            modified = true;
        }
    }
    return modified;
}

//保留容器含有中的数据,其余的数据清空
public boolean retainAll(Collection<?> c) {
    Objects.requireNonNull(c);//检查空指针
	return batchRemove(c, true);
}

//删除容器中含有的数据
public boolean removeAll(Collection<?> c) {
	Objects.requireNonNull(c);//检查空指针
	return batchRemove(c, false);
}

还有比较极端的清除操作,清除全部:

public void clear() {
    modCount++;//数据修改计数
    for (int i = 0; i < size; i++)
        elementData[i] = null;//清除所有数据,置null由gc回收
	size = 0;
}

表长

//空表判断
public boolean isEmpty() {
	return size == 0;
}

//返回表长
public int size() {
    return size;
}
发布了62 篇原创文章 · 获赞 36 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/sinat_36945592/article/details/97542168