简单阅读一下ArrayList的部分常用方法源码

简单阅读一下ArrayList的部分常用方法源码

ArrayList 的优缺点

ArrayList的优点如下:

  • ArrayList 底层以数组实现,其内存空间在物理上是连续的,是一种随机访问模式。ArrayList 实现了 RandomAccess 接口,按位置读取元素的平均时间复杂度为 O(1)。
  • ArrayList 在顺序添加一个元素的时候非常方便。

ArrayList 的缺点如下:

  • 删除元素的时候,需要做一次元素复制操作。如果要复制的元素很多,那么就会比较耗费性能。
  • 插入元素的时候,也需要做一次元素复制操作。

总结起来ArrayList 新增元素和根据下标获取元素很快,但是插入元素和删除元素很慢,比较适合顺序添加、随机访问的场景。

ArrayList源码解析

	private static final long serialVersionUID = 8683452581122892189L;
	
    private static final int DEFAULT_CAPACITY = 10;//默认容量

	/*
	*下面这两个空数组是用来区分不同情况下ArrayList为空时应该怎么操作,
	*具体在代码中会解释
	*/
    private static final Object[] EMPTY_ELEMENTDATA = {};//用于空实例的共享空数组实例。

    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};//用于默认大小的空实例的共享空数组实例。我们将其与EMPTY_ELEMENTDATA区分开来,以了解在添加第一个元素时应该膨胀多少。
    
    transient Object[] elementData; // 这是用于存储元素的核心数组,非私有以简化嵌套类访问
    
    private int size;

从上面可以看到,ArrayList使用一个Object[]数组elementData来存储元素,并且有一个final的默认容量大小DEFAULT_CAPACITY = 10,说明如果使用无参构造器去创建ArrayList,默认大小就是10。但是这里有个需要注意的点,Java1.7以后,在一开始创建的时候elementData并不会直接被初始化为10容量,而是在第一次调用add()方法时才会初始化,这一点我们在后面会详细讲解。

构造器

我们看一下ArrayList的三个构造器:

	//指定初始化容量的构造器
	public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];//直接根据指定容量初始化数组
        } else if (initialCapacity == 0) {
        //如果需要初始化一个容量为0的ArrayList,就让elementData 指向EMPTY_ELEMENTDATA
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

    public ArrayList() {
    //调用无参构造器会让elementData 指向DEFAULTCAPACITY_EMPTY_ELEMENTDATA
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

可以看到,上面的构造器中使用到了之前说的两个空数组。在调用无参构造器时,因为没有声明容量,所以让elementData 指向DEFAULTCAPACITY_EMPTY_ELEMENTDATA,而当声明容量为0或者传入的Collection的大小为0时,就让elementData 指向EMPTY_ELEMENTDATA,这样后面就能根据elementData 指向谁来判断是哪种情况,然后进行扩容的处理

常用方法

add(E e):
	//顺序新增元素
	public boolean add(E e) {
        ensureCapacityInternal(size + 1); //保证容量允许新增元素需不需要扩容
        elementData[size++] = e;
        return true;
    }

可以看到,顺序新增元素的add()方法极其简单,通过ensureCapacityInternal()方法先对数组进行容量保证,如果发现容量不够就会启动扩容机制。

下面我们研究一下ArrayList的扩容机制:

	private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }  
    
	//用于计算需要的容量大小    
	private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        //如果elementData 指向DEFAULTCAPACITY_EMPTY_ELEMENTDATA,说明elementData 的容量尚未初始化
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }

	private void ensureExplicitCapacity(int minCapacity) {
        modCount++;//这说明扩容会影响ArrayList的结构
        if (minCapacity - elementData.length > 0)//判断是否需要扩容
            grow(minCapacity);
    }      

//真正的扩容操作
	private void grow(int minCapacity) {
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);//扩容50%
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        //通过复制操作将元素复制到一个新数组中去
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

代码比较简单,看注释应该就明白了,所以这里不进行解析了。看客可以结合具体的情况进行理解,比如第一次调用add()、不需要扩容、需要扩容这三种情况。

add(int index,E element):
	//插入
    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++;
    }

可以看到,ArrayList的插入操作是先将插入点之后的元素往后移一位,然后更新插入点的元素。所以插入操作的快慢主要是由插入点到数组尾部的距离决定的,时间复杂度为O(n)。

set():
    public E set(int index, E element) {
        rangeCheck(index);//判断下标是否正常

        E oldValue = elementData(index);//获取旧元素
        elementData[index] = element;//更新
        return oldValue;//返回旧元素
    }
    
	private void rangeCheck(int index) {
	//如果下标越界就抛出异常
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

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

        return elementData(index);
    }
remove():
	//根据下标删除
	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; // 方便GC

        return oldValue;
    }

    //指定元素删除
    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; // 方便GC
    }

逻辑都比较简单,删除元素同样需要复制数组,时间复杂度为O(n)。

关于ArrayList的一些常见问题

ArrayList如何遍历

常见的遍历方法有三种,for、foreach和iterator,那么这三种有什么区别呢?

  • for 循环遍历,基于计数器。在集合外部维护一个计数器,然后依次读取每一个位置的元素,当读取到最后一个元素后停止。
  • 迭代器遍历,Iterator。Iterator 是面向对象的一个设计模式,目的是屏蔽不同数据集合的特点,统一遍历集合的接口。Java 在 Collections 中支持了 Iterator 模式。
  • foreach 循环遍历。foreach 内部也是采用了 Iterator 的方式实现,使用时不需要显式声明 Iterator 或计数器。优点是代码简洁,不易出错;缺点是只能做简单的遍历,不能在遍历过程中操作数据集合,例如删除、替换。

因为ArrayList实现了 RandomAccess 接口,最佳实践是使用普通的for去遍历

ArrayList如何边遍历边删除

边遍历边修改 ArrayList (或者说所有集合) 的唯一正确方式是使用 Iterator.remove() 方法,如下:

Iterator<Integer> it = list.iterator();
while(it.hasNext()){
   *// do something*
   it.remove();
}

一种最常见的错误代码如下:

for(Integer i : list){
   list.remove(i)
}

运行以上错误代码会报 ConcurrentModificationException 异常。集合的iterator在实现时都会根据modCount属性是否为expectedmodCount值去检查集合是否被修改。我们从之前ArrayList的源代码可以看到,add()、remove()都会有一句:modCount++。当使用 foreach语句时,会自动生成一个iterator 来遍历该 ArrayList,但同时该 ArrayList执行了 remove() 导致modCount的数值变化,那么就会抛出异常。

下面是ArrayList的iterator模式实现的部分源码,可以对照着理解一下:

	public E next() {
            checkForComodification();//检查ArrayList结构是否被修改
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }
	//可以安全地再遍历中删除元素
	public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();//检查ArrayList结构是否被修改

            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }
        
	final void checkForComodification() {
            if (modCount != expectedModCount)//如果modCount不符合预期就抛出错误
                throw new ConcurrentModificationException();
        }

为什么 ArrayList 的 elementData 加上 transient 修饰?

ArrayList 中的数组定义如下:

	transient Object[] elementData;

再看一下 ArrayList 的定义:

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

可以看到 ArrayList 实现了 Serializable 接口,这意味着 ArrayList 支持序列化。transient 的作用是说不希望 elementData 数组被序列化,并且重写了 writeObject 实现:

    private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException{
        int expectedModCount = modCount;
        s.defaultWriteObject();
        s.writeInt(size);

        for (int i=0; i<size; i++) {
            s.writeObject(elementData[i]);
        }

        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
    }

每次序列化时,先调用 defaultWriteObject() 方法序列化 ArrayList 中的非 transient 元素,然后遍历 elementData,只序列化已存入的元素,这样既加快了序列化的速度,又减小了序列化之后的文件大小。

多线程场景下如何使用 ArrayList?

ArrayList 不是线程安全的,如果遇到多线程场景,可以通过 Collections 的 synchronizedList 方法将其转换成线程安全的容器后再使用。例如像下面这样:

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,两者内部都是通过synchronized来保证线程安全的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ArrayList是一个可以自动扩容的动态数组类。它的底层实现是基于数组,可以通过索引访问其中的元素,可以根据需求动态调整数组大小。 ArrayList码主要包含以下部分: 1. 定义:ArrayList类是一个实现了List接口的类,它有一个默认初始容量为10的数组elementData,代表ArrayList中存储的元素。除此之外,ArrayList还定义了一些变量和常量,如DEFAULT_CAPACITY(默认容量)、MAX_ARRAY_SIZE(最大容量)等。 2. 构造方法ArrayList有多个构造方法,主要区别在于初始化时是否需要指定容量和元素集合。其中,无参的构造方法默认创建一个初始大小为10的空集合。 3. 添加操作:ArrayList中的添加操作主要有两个add方法,一个是指定插入位置插入元素,一个是在末尾添加元素。添加元素时,若当前元素数组已经满了,则需要进行扩容操作。扩容的方式是创建一个更大的数组,并将当前数组中的元素全部拷贝到新数组中。 4. 删除操作:ArrayList的删除操作包括remove(int index)和remove(Object obj)两个方法。删除元素时,会将被删除元素之后的所有元素向前移动一个位置,并将最后一个元素置为null。如果删除后的元素数量小于当前数组大小的50%,则会进行缩容操作。缩容的方式是创建一个比当前数组小的新数组,并将元素全部拷贝到新数组中。 5. 查询操作:ArrayList有多个查询操作,如get(int index)、indexOf(Object obj)、lastIndexOf(Object obj)等。其中,get是根据索引来访问元素,而indexOf和lastIndexOf是根据元素值来查找元素。查询操作比较简单,主要是对数组元素进行遍历。 6. 数组容量操作:数组容量操作包括ensureCapacity(int minCapacity)和trimToSize()两个方法。ensureCapacity用于确保ArrayList的容量至少为指定的minCapacity大小,而trimToSize用于缩小ArrayList内部数组的大小,使其与当前元素数量相同。如果当前元素数量大于数组容量,则不执行操作。 7. 迭代器:ArrayList实现了Iterable接口,因此可以使用foreach遍历。同时,它还实现了ListIterator接口,可以使用ListIterator来遍历和修改集合中的元素。 以上就是ArrayList的主要码结构。其实现方式相对简单,在处理元素的添加和删除时需要注意数组扩容和缩容问题。由于ArrayList是动态数组,因此可以有效地避免数组大小限制的问题。但需要注意的是,频繁插入或删除元素会导致性能下降,因此应视情况选择合适的数据结构。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值