关于ArrayList的底层实现

ArrayList继承与实现关系

先看看这个类的继承与实现:

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

AbstractList

继承了一个AbstractList类,是一个List接口的抽象实现类。AbstractList做了什么事?

  1. 实现了List接口:AbstractList实现了List接口,因此它具备了List接口的所有方法,包括添加、删除、获取元素等操作。这使得具体的List实现类可以直接继承AbstractList,而无需从零开始实现List接口的方法。
  2. 提供了基本的抽象方法:AbstractList提供了一些基用本的抽象方法,例如get(int index)set(int index, E element)等。这些方法的具体实现由子类来完成,以满足不同List实现类的需求。
  3. 支持修改操作:AbstractList还提供了一些辅助方法,用于支持修改操作。例如,add(int index, E element)remove(int index)方法可以在指定位置添加和删除元素。这些方法的具体实现会利用其他方法来实现。

List

实现List接口,为什么已经继承了实现List接口的AbstractList抽象类,为什么还要实现List接口?

  • ArrayList类再次实现List接口是为了提供get(int index)set(int index, E element)等这些具体的抽象方法的实现,确保ArrayList类满足List接口的所有合约要求。通过实现List接口,ArrayList类可以保证它所提供的方法与List接口的要求一致,并且与其他实现了List接口的集合类可以无缝替换和使用。

RandomAccess

RandomAccess是一个标记接口,它没有任何方法定义。在Java集合框架中,实现了RandomAccess接口的类表示它们支持快速、随机访问元素的能力。

  1. ArrayList,使用索引访问元素的效率要比使用迭代器进行遍历的效率高。这是因为RandomAccess接口的存在可以让算法在处理集合时,根据其是否实现了RandomAccess接口来选择不同的遍历方式。具体来说,当一个集合实现了RandomAccess接口时,算法会优先选择通过索引来访问元素,因为这种方式可以在常数时间内进行,即O(1)的时间复杂度。而如果一个集合没有实现RandomAccess接口,算法会选择使用迭代器进行遍历,因为使用索引来访问元素可能需要较高的时间复杂度,例如链表结构
  2. 需要注意的是,并非所有实现了List接口的集合类都实现了RandomAccess接口。仅当集合具备快速随机访问元素的能力时,才应该实现RandomAccess接口。

Cloneable

Cloneable是一个标记接口,在Java中被用来指示一个类可以被克隆。通过实现Cloneable接口,类可以表明它支持克隆(复制)自身的能力。

  • ArrayList实现了Cloneable接口,这意味着可以通过clone()方法来创建一个ArrayList的副本。这个副本将包含与原始ArrayList相同的元素,但是是完全独立的对象。需要注意的是这种复制方式是浅拷贝的方式,这意味着两个ArrayList将共享相同的元素,如果修改其中一个ArrayList的元素,另一个ArrayList也会受到影响。

Serializable

Serializable是一个标记接口,在Java中被用来指示一个类可以被序列化操作的。通过实现Cloneable接口,类可以表明它支持将自身进行序列化操作的能力。

  • 在Java中如果要实现对象的IO读写操作,都必须实现Serializable接口。

ArrayList成员变量

/**
* 序列号ID,用于序列化的版本控制
*/
private static final long serialVersionUID = 8683452581122892189L;
/** 
* 这是一个常量,表示ArrayList的默认容量大小。当我们创建一个新的ArrayList时,如果没有指定* 初始容量,它的初始容量将被设置为这个值。
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* 空对象数组,EMPTY_ELEMENTDATA是为了优化创建ArrayList空实例时产生不必要的空数组,使得 * 所有ArrayList空实例都指向同一个空数组。
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* 空对象数组,DEFAULTCAPACITY_EMPTY_ELEMENTDATA是为了确保无参构成函数创建的实例在添
* 加第一个元素时,最小的容量是默认大小10。
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* 这是一个对象数组,用于存储ArrayList中的元素。当我们向ArrayList中添加元素时,它们会被放* 入这个数组中。这个数组的大小会根据需要进行动态调整,以适应ArrayList中的元素数量。
* transient修饰表示elementData序列化的时候会被忽略;并使用ArrayList提供了两个用于序列化* 和反序列化的方法readObject和writeObject把保存的数据写入流或从流读出。
*/
transient Object[] elementData; 
/**
* 这是一个整数变量,用于记录ArrayList中实际包含的元素数量。当我们添加、删除元素时,size的* 值会相应地增加或减少。
*/
private int size;
/**
* 最大容量
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

ArrayList构造函数

无参构造函数

public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;//对象数组赋值成空对象数组
}

有参构造函数

  1. 通过传入int类型的长度创建ArrayList
//传入初始化容量initialCapacity 【new ArrayList(容量)】
public ArrayList(int initialCapacity) {
        //判断传入的容量大小
    if (initialCapacity > 0) {
        //创建一个容量为initialCapacity的对象数组
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        //将elementData 指向空对象数组EMPTY_ELEMENTDATA
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        //非法传参,容量不可以为负数
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    }
}

        2.通过传入Collection子类集合创建ArrayList

public ArrayList(Collection<? extends E> c) {
     //将传入的集合转成对象数组并赋值给elementData 
    elementData = c.toArray();
     //判断elementData的长度是否为0并把长度赋值给size 
    if ((size = elementData.length) != 0) {
        // 判断elementData 的类型,由于泛型中类型擦除的原因,他有可能返回的不是一个          // Object[]
        if (elementData.getClass() != Object[].class)
              /**如果类型不是对象数组,则通过Arrays.copyOf()将elementData复制成一个新的类型为Object[]的数组*/
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // 如果传入的集合长度为0,直接将elementData 指向EMPTY_ELEMENTDATA
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

ArrayList部分成员方法

对ArrayList本身进行操作

  • void trimToSize(),用于减小ArrayList的容量,将其容量调整为当前元素数量所需的最小容量。
public void trimToSize() {
      // 记录ArrayList的结构性修改次数,以便在迭代器中检测到这个修改。
    modCount++;
      /** 判断当前size(ArrayList中的元素数量)是否小于elementData数组的长度(这里会有扩容多出来的长度),如果小于,说明当前元素数量小于数组长度,可以将数组的长度调整为当前元素数量,以节省内存空间。*/
    if (size < elementData.length) {
        /** 三目表达式得出的结构会赋值给elementData, 使用Arrays.copyOf()方法来创建一个新的、具有指定长度的数组。如果当前元素数量为0(即ArrayList为空),则新数组将使用EMPTY_ELEMENTDATA(一个空数组)来初始化;如果当前元素数量不为0,则新数组将是在elementData数组的基础上复制出指定长度的新数组。*/
        elementData = (size == 0)
          ? EMPTY_ELEMENTDATA
          : Arrays.copyOf(elementData, size);
    }
}
  • int size(),用于获取当前ArrayList中的元素个数。(并非容量)

public int size() { return size; }

  • boolean isEmpty(),用于判断当前ArrayList是否为空(没有元素)。
    
public boolean isEmpty() {
    return size == 0;
}
  • void clear(),用于清空ArrayList
public void clear() {
    modCount++;
        /** 通过一个循环遍历集合中的每个元素,将其设置为null。这是为了释放对每个元素的引用,让垃圾回收器可以回收这些对象占用的内存空间。JVM使用可达性分析算法判断一个内存中哪些对象是垃圾,通过设置对元素的引用设置为null,断开原本栈中指向堆内存的引用,使得堆中存储对象的内存块不可达。*/
    for (int i = 0; i < size; i++)
        elementData[i] = null;
        //将size设置为0,表示集合中不再包含任何元素
    size = 0;
}

对ArrayList数据进行操作

  • int indexOf(Object o),用于获取o在ArrayList中第一次出现的下标。
public int indexOf(Object o) {
        /**这里要区分一下,如果o为空就不能调用equals()方法进行比较,所有判断null的时候用 == */
    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;
    }
// 没有匹配的元素时,返回-1
    return -1;
}
  • int lastIndexOf(Object o),用于获取o在ArrayList中最后一次出现的下标。
/**与indexOf(Object o)区别是lastIndexOf(Object o)是倒叙遍历的手法从ArrayList后往前寻找匹配元素,实现获取o在ArrayList中最后一次出现的下标*/
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;
}
  • boolean contains(Object o),用于判断数组是否包含某个元素
public boolean contains(Object o) {
        /**直接调用indexOf(o),如果匹配到了元素就会返回元素的下标,反之返回-1,通过判断大于或小于0实现判断是否包含某个元素的功能。*/
    return indexOf(o) >= 0;
}
  • E get(int index),用于传入下标,获取ArrayList中的元素
public E get(int index) {
/**这里调用rangeCheck()方法传入下标,作用是检查传入的下标是否在ArrayList的下标范围中,如果不在会抛出异常IndexOutOfBoundsException。*/
    rangeCheck(index);
       //返回elementData中保存的元素
    return elementData(index);
}
  • E set(int index, E element),用于传入下标和值,修改某个下标中存储的元素值
public E set(int index, E element) {
/**这里调用rangeCheck()方法传入下标,作用是检查传入的下标是否在ArrayList的下标范围中,如果不在会抛出异常IndexOutOfBoundsException。*/
    rangeCheck(index);
/**获取当前下标存储的旧值*/
    E oldValue = elementData(index);
/**赋新值*/
    elementData[index] = element;
    return oldValue;
}
  • boolean add(E e)和void add(int index, E element),用于向ArrayList中添加元素,后者可以自定义插入的位置;扩容机制就发生在这里
public boolean add(E e) {
        /**调用ensureCapacityInternal,作用是让调用了默认构造函数创建的ArrayList能够在第一次add元素的时候,扩容默认的容量10。*/
    ensureCapacityInternal(size + 1); 
    elementData[size++] = e;
    return true;
}

public void add(int index, E element) {
    rangeCheckForAdd(index);
        // ensureCapacityInternal() 里调用了方法进行扩容
    ensureCapacityInternal(size + 1); 
        /**System.arraycopy(源数组,源下标,目标数组,目标下标,复制源数组多少个值),其作用是将插入的指定下标的值以及后面的值进行向后移动,目的是腾出index这个下标下的元素
 例如 有一个elementData= [1,2,3,null,null],容量是5,size=3 在index=1的下标位置插入元素element=4,调用 System.arraycopy()后,就会将2,3向后移动,变成[1,2,2,3,null],在elementData[index] = element,最终[1,4,2,3,null]
*/
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    elementData[index] = element;
    size++;
}
  • void ensureCapacityInternal(int minCapacity),确保默认构造函数创建的空对象数组,在第一次扩容时是从默认容量开始扩容的
private void ensureCapacityInternal(int minCapacity) {
//判断elementData 是否是默认构造函数创建的空对象数组
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//如果是就直接将默认的容量DEFAULT_CAPACITY = 10赋值给minCapacity(元素个数+1的结果)
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
        //在这个ensureExplicitCapacity判断是否需要扩容
    ensureExplicitCapacity(minCapacity);
}
  • void ensureExplicitCapacity(int minCapacity),判断是否需要对elementData 扩容
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

        /**判断minCapacity(元素个数+1的结果),意思是添加了元素之后元素的总和,容量最小应该满足的值。minCapacity - elementData.length > 0 如果大于表示超出了当前容量,反之*/
    if (minCapacity - elementData.length > 0)
// 真正做扩容的方法
        grow(minCapacity);
}
  • void grow(int minCapacity),对elementData扩容
private void grow(int minCapacity) {
    //先保存当前elementData中的容量
    int oldCapacity = elementData.length;
    /**扩容的倍数,这里用到了位运算 其实就是除以2 例如 10 >> 1 = 5 ,这个newCapacity是扩容之后的容量,旧值+旧值的1/2,得出newCapacity是oldCapacity 的1.5倍*/
    int newCapacity = oldCapacity + (oldCapacity >> 1);
 // 判断新值是否满足了minCapacity(添加元素后应该有的总容量)
    if (newCapacity - minCapacity < 0)
        //如果新值还是不满足,就直接扩容到刚好满足容量的minCapacity,这个值
        newCapacity = minCapacity;
       /**这里判断是否超出了最大扩容量(最大扩容量是Integer.MAX -8,因为有些虚拟机超出这个数会OOM,也就是内存溢出)*/
    if (newCapacity - MAX_ARRAY_SIZE > 0)
       /**这里是,如果真的超过了Integer.MAX_VALUE-8 此时还可以调用hugeCapacity()将容量扩大到Integer.MAX_VALUE*/
        newCapacity = hugeCapacity(minCapacity);
    elementData = Arrays.copyOf(elementData, newCapacity);
}

结尾

以上就是我对ArrayList底层实现的拙见。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值