Java集合--ArrayList

本文详细解析了ArrayList的内部结构、实现原理及特点,包括实例化过程、添加与删除元素、扩容机制等核心内容,并对比了ArrayList与Vector的区别。

一、ArrayList的架构

在深入了解ArrayList前,先来看一看ArrayList的继承关系
在这里插入图片描述
Java集合主要分为两类,一类是Map相关的集合类,一类是由Collection接口延伸出的集合接口(List、Set、Queue),ArrayList属于List接口的一种实现类,继承自AbstractList,并实现了List、RandomAccess、Cloneable、Serializable接口,其中RandomAccess、Cloneable、Serializable这三个接口里没有任何变量和方法,是Java的标记接口,这是一种约定,表明只要implements了这三个接口,就具有这三个接口的特性,即:可随机访问、可克隆、可序列化与反序列化

二、从源码深入探索ArrayList

1、实例化

ArrayList<String> list = new ArrayList<>();

以上为ArrayList的一种实例化方式,深入构造方法的源码,可以看到ArrayList的底层实际是用Object数组来实现的

transient Object[] elementData;
private int size;
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

仅仅一个构造方法,我们已经可以看到很多细节的地方了。

  • 用无参构造方法实例化时,elementData被赋值为一个空的数组,意味着ArrayList的第一次扩容发生在第一次添加元素时,而不是用无参构造方法实例化时(下面我们会谈到ArrayList的扩容机制)
  • size:可以看到java对于size的定义注释,The size of the ArrayList (the number of elements it contains),它表示的是集合内实际含有元素的个数,而不是elementData数组的大小
  • elementData:该属性使用transient修饰,岂不是说ArrayList中的元素不会被序列化?

Java序列化的方式:

  • 只是实现Serializable接口,那么在序列化时,就会自动调用java.io.ObjectOutputStream的defaultWriteObject()方法进行序列化,这时用transient或static修饰的成员变量是不会被序列化的
  • 实现Serializable接口,重写writeObject()方法,这时用transient修饰的成员变量(除静态变量)能不能被序列化,就取决于重写的writeObject()方法了
  • 实现Externalizable接口,Externalizable继承了Serializable接口,并增加了两个方法:writeExternal(ObjectOutput out) 和readExternal(ObjectInput in) 。在writeExternal方法里定义了哪些属性可以序列化,哪些不可以被序列化

ArrayList采用的是第二种方式,实现Serializable接口,并重写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 size as capacity for behavioural compatibility with clone()
        s.writeInt(size);

        // 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方法,此时用transient修饰的成员变量还没有被序列化,接下来根据size,把数组里的元素一个一个遍历并序列化,也就是说ArrayList中用transient修饰的elementData数组是会被序列化的。那么ArrayList花这么大工夫的目的是什么呢,为什么不直接将elementData的修饰符transient去掉呢?
我们在上面知道了size表示的是数组中实际含有的元素个数,如果了解了ArrayList的扩容机制,我们就会知道ArrayList每次空间不够,进行扩容时,elementData数组的空间都会比原空间大很多,它会预留一些空间,当空间都被用完不够时,才会再次进行扩容,正是这些预留的空间,表示数组中的内容不光会有实际的元素,还会有空元素,而ArrayList在序列化的做法是根据size来遍历,即只会将有元素的内容序列化,而不是整个数组,节省了空间和时间
在这里插入图片描述
(如果用idea调试时,看不到null元素,记得在调试面板将Enable alternative view for Collections classes和Hide null elements in arrays and collections选项,去除勾选)

其他两种构造方法:

  • 如果知道所构建的集合元素个数大致有多少的情况下,可以用有参构造方法实例化,给elementData数组设定初始的容量,这样就不用ArrayList再大费周章地去扩容,也可以避免扩容可能带来的空间浪费(实际存储的元素远小于扩容之后的容量)
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初始化时,把另一个集合的元素赋值给他
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、添加元素(扩容)

使用add方法给ArrayList添加元素

List<String> list = new ArrayList();
list.add("123");
list.add("321");
list.add("456");
list.add("789");

看下底层是怎么实现的

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

ensureCapacityInternal方法是ArrayList进行自动扩容的方法,这里我们具体来研究一下ArrayList的扩容机制

private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

private static final int DEFAULT_CAPACITY = 10;
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if(elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    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);
}

从ensureExplicitCapacity方法可以看到,ArrayList每次在添加元素时,都会判断当前数组所剩余的容量是否能允许元素添加进来(一次性添加的元素可能不止一个),若不能则需要扩容。而从calculateCapacity方法可以看到,若数组一开始是个空的数组(使用无参构造方法实例化),则扩容的容量为10,记得在jdk1.7中,若使用无参构造方法进行实例化,elementData的容量便会默认为10,而1.8则是在实际添加元素时,才对elementData数组开辟容量为10的存储空间,这种在需要用到时才开辟存储空间的方法明显要比一开始就开辟要好得多的,因为你可能new了ArrayList对象,但是不进行add,那开辟的空间就浪费了。

grow方法对ArrayList进行正式的扩容,首先是确定扩多少容(可能jdk的版本不同,扩容的量也不同),oldCapacity + (oldCapacity >> 1):oldCapacity表示的当前数组的容量(长度),右移一位,相当于乘以0.5,即扩容后的容量为原容量的1.5倍,对新容量的计算,jdk是用了右移来实现,这种二进制运算是要比直接乘0.5快的多的。

从hugeCapacity这段逻辑可以看出如果内存足够的情况下,ArrayList也不是可以无限add的,最大容量为Integer.MAX_VALUE(数组长度是整数类型,理论上最大就是Integer.MAX_VALUE,实际还是要看内存和jvm实现),即2^31-1,虽然看MAX_ARRAY_SIZE的注释,ArrayList定义的数组最大容量为Integer.MAX_VALUE-8,因为有些虚拟机需要在数组里存储一些header words,但是超过Integer.MAX_VALUE-8依旧可以扩容到Integer.MAX_VALUE,只是这么做可能会造成内存溢出

/**
 * The maximum size of array to allocate.
 * Some VMs reserve some header words in an array.
 * Attempts to allocate larger arrays may result in
 * OutOfMemoryError: Requested array size exceeds VM limit
 */
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
}

Arrays.copyOf进行数组扩容,具体方式为新建一个大容量的数组,然后将原数组的元素拷贝(引用指向)到新数组

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")
    T[] copy = ((Object)newType == (Object)Object[].class)
        ? (T[]) new Object[newLength]
        : (T[]) Array.newInstance(newType.getComponentType(), newLength);
    System.arraycopy(original, 0, copy, 0,
                     Math.min(original.length, newLength));
    return copy;
}

3、插入元素

//在下标为2的位置插入一个元素
List<String> list = new ArrayList(Arrays.asList("a","b","c","d"));
list.add(2, "e");
----------结果-----------
abecd
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++;
}
  • rangeCheckForAdd:判断插入的位置是否合法,是否在下标为0与下标为size之间
  • ensureCapacityInternal:判断是否需要扩容
  • System.arraycopy:数组拷贝,将下标为index位置的元素往后移动一个单位,即执行完这条语句后,数组内的元素变成了abccd
  • elementData[index] = element:替换下标为index上的元素,替换完为abecd

4、删除元素

List<String> list = new ArrayList(Arrays.asList("a","b","c","d","c","d"));
list.remove(1);
System.out.println(list);        //acdcd
list.remove("c");
System.out.println(list);        //adcd

ArrayList提供了按下标删除元素的方法和删除指定元素的方法,这两种方法的核心都是将删除元素后的所有元素都往前移动一个单位,并将最后一个位置的元素赋为null,方便gc回收

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的特点

在知道了ArrayList的底层实现后,便可以总结一下ArrayList的特点

元素是否允许为空允许(且允许多个元素为空)
元素是否允许重复允许
是否有序有序
是否线程安全非线程安全

四、其他

1、trimToSize

去除数组中未被分配的空间,将当前ArrayList容量的大小变更为数组实际存储元素的个数,可以用来减少空间的浪费

2、subList

List<String> list = new ArrayList(Arrays.asList("a","b","c","d","c","d"));
List<String> subList = list.subList(1, 3);
System.out.println(subList);        //b,c
subList.set(0, "11");
System.out.println(subList);        //11,c
System.out.println(list);           //a,11,c,d,c,d
ArrayList<String> arrayList = new ArrayList(subList);
arrayList.add("22");                
System.out.println(arrayList);      //11,c,22
System.out.println(subList);        //11,c
System.out.println(list);           //a,11,c,d,c,d

subList方法会返回一个SubList对象,它是ArrayList里的一个内部类,这个SubList并没有新创建一个List,使用的元素还是原集合的,即:对SubList进行添加、修改、删除元素,都会影响到原集合;同理,原集合修改元素也会影响到SubList(对原集合进行添加、删除元素后,再对SubList进行操作会触发fast-fail机制);如果想对SubList进行元素操作而不影响到原集合,可以重新建一个ArrayList

SubListpublic E set(int index, E e) {
    rangeCheck(index);
    checkForComodification();
    E oldValue = ArrayList.this.elementData(offset + index);
    ArrayList.this.elementData[offset + index] = e;
    return oldValue;
}

public E get(int index) {
    rangeCheck(index);
    checkForComodification();
    return ArrayList.this.elementData(offset + index);
}

3、Arrays.asList与new ArrayList

List<String> list = Arrays.asList("a","b","c","d","c","d");
list.add("111");      //java.lang.UnsupportedOperationException

Arrays.asList方法会返回一个ArrayList,但这个ArrayList和我们上面认识的ArrayList不是同一个物种,asList返回的是Arrays的一个内部类,继承AbstractList

public static <T> List<T> asList(T... a) {
    return new ArrayList<>(a);
}
private static class ArrayList<E> extends AbstractList<E>
    implements RandomAccess, java.io.Serializable{
    
    @Override
    public E get(int index) {
        return a[index];
    }

    @Override
    public E set(int index, E element) {
        E oldValue = a[index];
        a[index] = element;
        return oldValue;
    }
    ...
}

为什么调用add方法会报错呢,可以看到ArrayList内部类里除了get、set等常用方法外,并没有实现add、remove等方法,也就是说调用的add方法是AbstractList的

public void add(int index, E element) {
  throw new UnsupportedOperationException();
}

public E remove(int index) {
  throw new UnsupportedOperationException();
}

五、ArrayList与Vector

ArrayList的方法都没有加synchronized关键字修饰,即:非线程安全,如果要在多线程环境下使用,一种方法是用Collections.synchronizedList将ArrayList变成一个线程安全的List,另一种是使用Vector。Vector底层也是用数组来实现,大致原理和ArrayList相同,只是有以下细微区别

  • 线程安全:Vector线程安全(方法有synchronized修饰),ArrayList非线程安全
  • 扩容:ArrayList扩容后的容量为原容量的1.5倍;Vector可以在构建Vector指定增长系数,若增长系数大于0,则扩容后的容量为原容量+增长系数;若未指定或指定的系数不大于0,则扩容后的容量为原容量的2倍
  • 无参构造:Vector使用无参构造实例化后,便会默认为数组创建10个空间,而ArrayList创建10个空间是发生在add方法,扩容时

– jdk版本:1.8

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值