• 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;
    }
    
  • 相关阅读:
    Day 18
    Day 17
    Day 16
    Day 15
    Day 14
    Day 13
    Day 12
    Day 11
    Day 10
    《ES6标准入门》(阮一峰)--2.let 和 const 命令
  • 原文地址:https://www.cnblogs.com/yanzs/p/13788242.html
Copyright © 2020-2023  润新知