日期:2025/04/05 21:53来源:未知 人气:57
本篇文章主要跟大家分析一下ArrayList
的源代码。阅读本文你首先得对ArrayList
有一些基本的了解,至少使用过它。如果你对ArrayList
的一些基本使用还不太熟悉或者在阅读本文的时候感觉有点困难,你可以先阅读这篇文章ArrayList设计与实现,自己动手写ArrayList。
RandomAccess
,这个接口的含义表示可以随机访问ArrayList
当中的数据,拿什么是随机访问呢?随机访问就是表示我们可以在常量时间复杂度内访问数据,也就是时间复杂度是O(1)
。因为在ArrayList
当中我们使用的最基本的数据类型是数组
,而数组是可以随机访问的,比如像下面这样。
public static void main(String[] args) { int[] data = new int[10]; for (int i = 0; i < 10; i++) data[i] = i; System.out.println("data[5] = " + data[5]); }
而链表是不可以随机访问的,比如说我们想通过下标访问链表当中的某个数据,需要从头结点或者尾节点开始遍历,直到遍历到下标对应的数据,比如下图中的单链表找到第3个数据,需要从头开始遍历,而这个时间复杂度为O(n)
。
Serializable
,这个接口主要用于序列化,所谓序列化就是能将对象写入磁盘,反序列化就是能够将对象从磁盘当中读取出来,如果想序列化和反序列化ArrayList
的实例对象就必须实现这个接口,如果没有实现这个接口,在实例化的时候程序执行会报错,比如下面就是一个序列化的例子。
import java.io.*;import java.util.Objects;class TestPerson implements Serializable{ String name; Integer age; private static final long serialVersionUID = 9999L; @Override public String toString() { return "TestPerson{" + "name='" + name + '\'' + ", age=" + age + '}'; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; TestPerson that = (TestPerson) o; return that.age.equals(this.age) && that.name.equals(this.name); } @Override public int hashCode() { return Objects.hash(name, age); } public TestPerson(String name, Integer age) { this.name = name; this.age = age; }}public class SerialTest { public static void main(String[] args) throws IOException, ClassNotFoundException { TestPerson leHung = new TestPerson("LeHung", 18); FileOutputStream os = new FileOutputStream("objtest"); ObjectOutputStream outputStream = new ObjectOutputStream(os); // 序列化数据 outputStream.writeObject(leHung); FileInputStream is = new FileInputStream("objtest"); ObjectInputStream stream = new ObjectInputStream(is); // 反序列化数据 TestPerson object = (TestPerson) stream.readObject(); System.out.println(object); System.out.println(object == leHung); System.out.println(object.equals(leHung)); }}折叠
如果上面程序当中的TestPerson
没有implements Serializable
,则上述代码会报异常java.io.NotSerializableException:
。
Cloneable
,实现Cloneable
接口那么实现Cloneable
的类就能够调用clone
这个方法,如果没有实现Cloneable
接口就调用方法,则会抛出异常java.lang.CloneNotSupportedException
。
List
,这个接口主要定义了一些集合常用的方法让ArrayList
进行实现,比如add
,addAll
,contains
,remove
,set
,size
,indexOf
等等方法。
AbstractList
,这个抽象类也实现了List
接口里面的方法,并且为其提供了默认代码实现,比如说AbstractList
中对indexOf
的实现如下:
// 这个方法的作用就是返回对象 o 在容器当中的下标public int indexOf(Object o) { // 通过迭代器去遍历数据 ListIterator
集合的addAll
方法实现如下:
// 这个函数的作用就是在 index 的位置插入集合 c 当中所有的元素public boolean addAll(int index, Collection<? extends E> c) { rangeCheckForAdd(index); boolean modified = false; for (E e : c) { add(index++, e); modified = true; } return modified;}
在ArrayList
当中主要有以下这些字段:
// ArrayList 当中默认初始化容量,也就是初始化数组的大小private static final int DEFAULT_CAPACITY = 10;// 存放具体数据的数组 ArrayList 底层就是使用数组进行存储的transient Object[] elementData; // size 表示容器当中数据的个数 注意和容器的长度区分开来private int size;// 当容器当中没有元素的时候将 elementData 赋值为以下数据(不同情况不一样)private static final Object[] EMPTY_ELEMENTDATA = {};private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};// 下面两个函数是 ArrayList 的构造函数,从下面两个函数当中// 我们可以看出 EMPTY_ELEMENTDATA 和 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 使用区别// EMPTY_ELEMENTDATA 是容器当中没有元素时使用,DEFAULTCAPACITY_EMPTY_ELEMENTDATA// 是默认构造的时候使用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); }}public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;}
add
方法,这个方法用于往容器的末尾增加数据,也是ArrayList
当中最核心的方法。他的主要工作流程如下图所示:他首先调用函数ensureCapacityInternal
确保ArrayList
当中的数组长度能够满足需求,不然数组会报数组下标越界异常,add
函数调用过程当中所涉及到的函数如下。
public boolean add(E e) { // 这个函数的主要目的是确保 elementData 的容量有 size + 1 // 否则存储数据的时候数组就会越界 ensureCapacityInternal(size + 1); // size 表示容器当中数据的个数 注意和容器的长度区分开来 // 加入数据之后 容器当中数据的个数也要 + 1 elementData[size++] = e; return true;}// minCapacity 表示 ArrayList 中的数组最小的长度private void ensureCapacityInternal(int minCapacity) { ensureExplicitCapacity( // 这个函数计算数组的最小长度 calculateCapacity(elementData, minCapacity) );}private static int calculateCapacity(Object[] elementData, int minCapacity) { // 如果是无参构造的话,取默认长度和需求长度 minCapacity 中比较大的值 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { return Math.max(DEFAULT_CAPACITY, minCapacity); } return minCapacity;}private void ensureExplicitCapacity(int minCapacity) { // 这个表示容器发生改变的次数,我们在后续分析迭代器的时候进行分析 // 它跟容器扩容没关系,现在可以不用管他 modCount++; // 如果最小的需求容量 minCapacity 大于现在容器当中数组的长度,则需要进行扩容 if (minCapacity - elementData.length > 0) grow(minCapacity);}private void grow(int minCapacity) { int oldCapacity = elementData.length; // 新数组的长度为原数组的长度的1.5倍,右移一位相当于除以2 int newCapacity = oldCapacity + (oldCapacity >> 1); // 如果新数组的长度,小于需要的最小的容量,则更新数组的长度为 minCapacity 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);}private static int hugeCapacity(int minCapacity) { if (minCapacity < 0) // overflow throw new OutOfMemoryError(); return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;}折叠
上述代码的调用流程如下:
get
函数,获取对应下标的数据。
public E get(int index) { // 进行数组下标的检查,如果下标超过 ArrayList 中数据的个数,则抛出异常 // 注意这里是容器当中数据的个数 不是数组的长度 rangeCheck(index); return elementData(index);}private void rangeCheck(int index) { if (index >= size) throw new IndexOutOfBoundsException(outOfBoundsMsg(index));}E elementData(int index) { // 返回对应下标的数据 return (E) elementData[index];}
remove
函数,删除ArrayList
当中的数据。
// 通过下标删除数据,这个函数的意义是删除下标为 index 的数据public E remove(int index) { // 首先检查下标是否合法,如果不合法,抛出下标越界异常 rangeCheck(index); modCount++; E oldValue = elementData(index); // 因为删除某个数据,需要将该数据后面的数据往数组前面移动 // 这里需要计算需要移动的数据的个数 int numMoved = size - index - 1; if (numMoved > 0) // 通过拷贝移动数据 // 这个函数的意义是将 index + 1和其之后的数据整体移动到 index // 的位置 System.arraycopy(elementData, index+1, elementData, index, numMoved); // 因为最后一个数据已经拷贝到前一个位置了,所以可以设置为 null // 可以做垃圾回收了 elementData[--size] = null; return oldValue;}// 这个函数的意义是删除容器当中的第一个等于 o 的数据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;}// 这个方法和第一个 remove 方法原理一致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; // clear to let GC do its work}折叠
set
方法,这个方法主要是用于设置指定下标的数据,这个方法比较简单。
public E set(int index, E element) { rangeCheck(index); E oldValue = elementData(index); elementData[index] = element; return oldValue;}
ensureCapacity
方法public void ensureCapacity(int minCapacity) { int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) // any size if not default element table ? 0 // larger than default for default empty table. It's already // supposed to be at default size. : DEFAULT_CAPACITY; if (minCapacity > minExpand) { ensureExplicitCapacity(minCapacity); }}
这个方法我们在前面已经提到过了,不知道大家有没有观察到他的访问修饰符是public
,为什么要设置为public
呢?这个意思很明显,我们可以在使用ArrayList
的时候自己调用这个方法,防止当我们在往容器中加入数据的时候频繁因为数组长度不够重新申请内存,而原来的数组需要从新释放,这会给垃圾回收器造成压力。我们在ArrayList设计与实现,自己动手写ArrayList这篇文章当中写过一段测试程序去测试这个方法,感兴趣的同学可以去看看!!!
toString
方法我们首先来看一下下面代码的输出
public class CodeTest { public static void main(String[] args) { LinkedList
执行上面一段代码我们可以在控制台看见对应的输出,我们知道最终打印在屏幕上的是一个字符串,那这个字符串怎么来的呢,我们打印的是一个对象,它是怎么得到字符串的呢?我们可以查看System.out.println
的源代码:
public void println(Object x) { String s = String.valueOf(x); synchronized (this) { print(s); newLine(); }}
从上述代码当中我们可以看见通过String s = String.valueOf(x);
这行代码得到了一个字符串,然后进行打印,我们在进入String.valueOf
方法看看是如何得到字符串的:
public static String valueOf(Object obj) { return (obj == null) ? "null" : obj.toString();}
我们可以看到如果对象不为 null
最终是调用对象的toString
方法得到的字符串。因此当打印一个对象的时候,最终会打印这个对象的toString
方法返回的字符串。
toString
方法没有直接在ArrayList
当中实现,而是在它继承的类AbstractList
当中实现的,toString
的源代码如下所示:
public String toString() { // 得到 ArrayList 的迭代器 这个迭代器我们稍后细说 Iterator
上述代码的整个过程还是比较清晰的,大致过程如下:
如果容器当中没有数据,直接返回[]。
如果容器当中有数据的话,那么通过迭代每个数据,调用StringBuilder
的append
方法,将数据加入到输出的StringBuilder
对象当中,下面是append
的源代码。
// StringBuilder 的 append 方法@Overridepublic StringBuilder append(Object obj) { return append(String.valueOf(obj));}// StringBuilder 的 append 方法的重载方法@Overridepublic StringBuilder append(String str) { super.append(str); return this;}// String 类中的 valueOf方法public static String valueOf(Object obj) { return (obj == null) ? "null" : obj.toString();}
我们可以发现最终append
到StringBuilder
当中的字符串仍然是ArrayList
当中数据对象的toString
方法返回的数据。
equals
方法在ArrayList
当中的equals
方法和toString
方法一样,equlas
方法也是在类AbstractCollection
当中实现的,其源代码如下:
public boolean equals(Object o) { if (o == this) return true; if (!(o instanceof List)) return false; ListIterator
上面代码的主要流程:
首先判断o
和this
是否是同一个对象,如果是则返回true
,比如下面这种情况:
ArrayList