Java 源码分析——Arraylist(个人笔记)

    科技2022-07-11  99

    Java 源码分析——Arraylist(个人笔记)

    最近复习数据结构,于是想看看Java的源码具体实现。通过博客简单的记录个人的学习历程吧

    一、

    本篇介绍的是Java的动态数组——Arraylist,这是平时非常常用的数据结构。由于个人水平有限,只分析其中的部分源码,分析有误的地方,还请各位大佬多多指出,咱们共同学习。另外,我使用的是jdk1.8版本。

    二、成员变量分析

    从命名中可知,用于集合的序列化操作。这个部分没有研究过,不做分析

    private static final long serialVersionUID = 8683452581122892189L;

    用于设置初始数组的容量

    private static final int DEFAULT_CAPACITY = 10;

    两个空数组,不过这里我没想明白为什么要有两个,暂时留着这个问题吧

    private static final Object[] EMPTY_ELEMENTDATA = {}; private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    用于实现Arraylist的底层数组

    transient Object[] elementData;

    三、常用函数分析

    1.构造函数

    Arraylist中有三个构造函数,其中一个是无参构造函数,另两个有参数。

    public ArrayList()函数:这是无参构造函数,将数组初始化为空,很好理解 public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } public ArrayList(int initialCapacity)函数:这是第二个构造函数,传入的参数代表数组的初始容量。函数中对参数initialCapacity进行了检查,防止非法参数的传入,很好理解 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(Collection<? extends E> c)函数:这是第三个构造函数,传入的参数是一个集合,函数使用集合来直接初始化数组。这里同样存在一个检查的环节,后面我再补充这个检查的含义 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.常用的一些操作函数

    在介绍某一个函数的时候,很可能这个函数内部调用了其他的函数,我这里就把那些被调用的函数中一些关键的函数,也进行了介绍。

    1.add函数系列

    public boolean add(E e)函数:这里的逻辑很简单,第一步是保证数组的容量够用;第二步是将元素加入到数组末尾,同时更新数组中元素个数。这里第一步中的ensureCapacityInternal函数。 public boolean add(E e) { ensureCapacityInternal(size + 1); // 保证数组容量够用 elementData[size++] = e; return true; } public void add(int index, E element)函数:这个函数的目的是将元素加入到指定的索引位置,需要注意的是,将元素插入到index位置上,那么原来index位置上的元素要往后移动,index后面的所有元素都要做类似的移动。具体流程在代码注释中解释了,不多赘述。 public void add(int index, E element) { rangeCheckForAdd(index); //检测index是否合法 ensureCapacityInternal(size + 1); // 保证数组容量够用 System.arraycopy(elementData, index, elementData, index + 1, size - index); //使用native静态方法进行数组拷贝,完成index以及后面索引位置元素的移动 elementData[index] = element; //在index位置插入新的元素 size++; } public boolean addAll(Collection<? extends E> c)函数:这个函数传入参数是一个集合,目的是将集合中的元素加入到Arraylist的末尾。 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); //将a数组中的元素拷贝到elementData数组的末尾 size += numNew; //对数组元素个数进行更新 return numNew != 0; } public boolean addAll(int index, Collection<? extends E> c)函数:这个函数目的是将集合插入到Arraylist的index索引位置。这里一方面需要对插入位置index进行检测,防止非法参数。另一方面,需要比较数组元素size与index的大小,如果size==index,说明插入位置是数组的末尾,那么直接进行插入就可以了,不需要移动原先的数组元素;如果size>index,说明插入位置不是末尾,那么就需要先在数组中腾出位置(把index以及后面索引位置元素往后挪动),才能进行后面的插入。 public boolean addAll(int index, Collection<? extends E> c) { rangeCheckForAdd(index); //检测index是否合法 Object[] a = c.toArray();//转换成数组 int numNew = a.length; ensureCapacityInternal(size + numNew); // 保证数组容量够用 int numMoved = size - index; //判断size与index的大小 if (numMoved > 0) System.arraycopy(elementData, index, elementData, index + numNew, numMoved); //将elementData数组中index以及后面索引位置元素进行移动,为前面预留位置 System.arraycopy(a, 0, elementData, index, numNew);//将a数组中元素拷贝到elementData中指定位置 size += numNew;//更新数组元素个数 return numNew != 0; }

    2.remove函数系列

    public E remove(int index)函数:从数组上删除指定位置的元素。首先需要进行索引检查,用index后面的元素向前逐个覆盖,把最后元素的值改成null,方便后面的垃圾回收,最后返回要删除的元素,完成删除。 public E remove(int index) { rangeCheck(index); //检测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; // 将最后位置元素清空,利于垃圾回收 return oldValue; //返回要删除的元素值 } public boolean remove(Object o)函数:从数组中删除某个元素,这里是删除第一次出现的相同元素。需要对null进行特殊处理,对于非null元素,调用equals方法进行对比 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; } public boolean removeAll(Collection<?> c)函数:从数组中删除所有出现在集合c中的元素。首先对集合c进行检测,要求c不能为null。然后,进行批量删除。这里如果采用常规的多次删除方法,每次删除一个元素,那么每次都要完成元素的移动,时间开销会很大。 public boolean removeAll(Collection<?> c) { Objects.requireNonNull(c); //要求集合c不能为null return batchRemove(c, false); //执行批量删除 }

    批量删除的核心原理是:遍历数组中的元素,将不在集合c中的元素往前放,逐个覆盖,同时进行统计,统计不在c中的元素个数,最后将后面的元素进行统一删除,避免了元素的频繁移动,也保证了数组的连续性。具体代码为

    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 { // Preserve behavioral compatibility with AbstractCollection, // even if c.contains() throws. if (r != size) { System.arraycopy(elementData, r, elementData, w, size - r); w += size - r; } if (w != size) { // clear to let GC do its work for (int i = w; i < size; i++) elementData[i] = null; modCount += size - w; size = w; modified = true; } } return modified; } public boolean removeIf(Predicate<? super E> filter)函数:这个函数目前我还没用过 public boolean removeIf(Predicate<? super E> filter){ //这个函数还没具体学习 } protected void removeRange(int fromIndex, int toIndex)函数:删除指定范围的元素。通过元素的移动,来覆盖掉需要被删掉的部分元素,然后将后面的元素赋为null,方便垃圾回收 protected void removeRange(int fromIndex, int toIndex) { modCount++; int numMoved = size - toIndex; System.arraycopy(elementData, toIndex, elementData, fromIndex, numMoved); // clear to let GC do its work int newSize = size - (toIndex-fromIndex); for (int i = newSize; i < size; i++) { elementData[i] = null; } size = newSize; }

    3.其他常用函数

    public boolean contains(Object o):这个方法用于判断数组中是否函数目标元素 public boolean contains(Object o) { return indexOf(o) >= 0; }

    这里函数调用了indexOf(o)函数,这个函数用来查找某个元素在数组中的索引位置,通过索引位置,可以判断该元素是否在数组中

    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; } public E get(int index)函数:该函数用于访问具体某个索引值对应的元素,本质上就是数组的随机访问 public E get(int index) { rangeCheck(index);//对index进行合法性检查 return elementData(index);//返回数组中的该元素 } public boolean isEmpty()函数:用于判断数组是否为空 public boolean isEmpty() { return size == 0; } public E set(int index, E element)函数:用指定元素特换指定索引位置的元素 public E set(int index, E element) { rangeCheck(index); //检查索引是否合法 E oldValue = elementData(index); elementData[index] = element; return oldValue; } public void clear()函数:用于清空整个数组,这里的做法是将数组中的全部元素置为null,方便后续的垃圾回收 public void clear() { modCount++; // clear to let GC do its work for (int i = 0; i < size; i++) elementData[i] = null; size = 0; }

    3.扩容函数

    看Arraylist的源码,如果不看扩容函数,那相当于白看呀。可以注意到,前面介绍的四个add函数中,都调用了下面这个函数:

    ensureCapacityInternal(size + 1);

    下面就来介绍这个函数。

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

    里面调用了另一个函数ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)),

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

    从代码中看到,如果minCapacity小于数组的长度,说明不需要扩容;如果大于,那么就调用grow(minCapacity)函数进行扩容,下面分析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); }

    在一般情况下,Java采用的扩容方式是,将容量增大为原来容量的1.5倍。然后将新的容量与所需要的最小容量进行比较,根据比较结果再进行一次调整。

    四、总结

    Arraylist的源码分析就到此结束,后面可能会再补充一些,不过常用的知识点应该都有了。有不对的地方,希望大佬们多多指出!

    Processed: 0.049, SQL: 8