リテラシープログラム-ArrayList一般的なメソッド原則リテラシー

ArrayListスイープ

一緒に書く習慣をつけましょう!「ナゲッツデイリーニュープラン・4月アップデートチャレンジ」に参加した初日です。クリックしてイベントの詳細をご覧ください

序章

ArrayListは、Listの一般的に使用される実装クラスであり、その名前から、その最下層が配列を介して実装されていることがわかります。そのため、要素間に隙間ができず、途中で要素を挿入・削除する場合、配列の移動やコピーなどの操作が必要になり、時間がかかるというデメリットがあります。ただし、配列を介して実装されているため、クエリの効率は高く、高速ランダムアクセスをサポートしています。リスト内のリストへの変更が少なく、クエリの数が多い場合は、ArrayListをお勧めします。

ArrayListの重要なプロパティ

//默认容量大小
private static final int DEFAULT_CAPACITY = 10;
//空数组
private static final Object[] EMPTY_ELEMENTDATA = {};

//这个也是一个空数组,不过我没有理解为什么会有两个空的数组,不过在看源码时发现DEFAULTCAPACITY_EMPTY_ELEMENTDATA
//是用在无参构造里面的,EMPTY_ELEMENTDATA是用在有参构造中,如果参数不规范就会赋值这个空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

//存储元素的数组,在第一次添加元素的时候被初始化
transient Object[] elementData;

//ArrayList的数组大小(ArrayList的元素数量)
private int size;
复制代码

ArrayListのコンストラクタ

ArrayListには3つのコンストラクターがあります。2つはパラメーターあり、もう1つはパラメーターなしです。

  • 配列リスト()
  • ArrayList(Collection <?extends E> c)
  • ArrayList(int initialCapacity)

ヌルパラメータの構築

public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
复制代码

nullパラメータの構成については何も言うことはありません。要素を格納する配列に、空の配列を割り当てることです。

パラメータを使用して構築: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);
    }
}
复制代码

このロジックは非常に単純です。

  1. 作成される現在の配列のサイズが0より大きいかどうかを判別します。0より大きい場合、配列は通常どおりに作成され、elementDataに割り当てられます。
  2. 作成する現在の配列のサイズが0に等しいかどうかを判別し、等しい場合は、EMPTY_ELEMENTDATAをelementDataに割り当てます。
  3. 上記の2つの分岐が完了すると、渡される数は0未満である必要があると判断できます。配列の容量サイズは0未満にすることはできないため、ここで不正なパラメーター例外がスローされます。

パラメータを使用して構築:コレクションインターフェイスの実装クラスを渡します

画像-20220403165636455

コレクションのサブクラスである限り、渡すことができます。ここでは、SetとList、およびその他のいくつかのみをリストします。

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable{}
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;
    }
}
复制代码

这个构造方法可以传一个实现了Collection的类。在这个构造里面会将传入类的数组赋值给elementData.但是传入的这个它的泛形是一是E的,如果这里传入的参数长度是0那么就会赋值一个空的数组。

还有一点要说的:这里有一个条件判断elementData是不是Object[]数组(这里一般都会是Object数组),如果不是的话会将其传成Object数组,通过Arrays.copyOf重新赋值。Arrays工具在java.util包里面有兴趣可以看下

ArrayList中的添加元素的方法

在使用ArrayList的时候用的最多的方法就是添加和查询了吧。那就先来看下添加是如何实现的。

插入的方法分为两种:

  • 直接插入
  • 指定位置插入

直接插入元素的方法

public boolean add(E e) {
    ensureCapacityInternal(size + 1); 
    elementData[size++] = e;
    return true;
}
复制代码

插入方法总结后大至分为两步:

  • 确保数组的容量是否能够插入一个元素
  • 将当前需要插入的元素放到数组指定位置

如何检查数组容量

在添加元素的时候会调用这么一个方法,用来在添加数据前确保数组的容量,它在这里需要传入一个最小的空间数值(这个数值计算的方式是在原数组大小的基础上加1,因为会插入一个元素嘛所以要加1),这个ensureCapacityInternal方法主要是用来检查当前的数组是否需要扩容的方法。还会有一个计算容量的方法calculateCapacity

private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
复制代码

计算插入数组所需的容量

检查数组是否需要扩容前,首先需要一个数值用来判断当前是否需要扩容。这个数值就是通过calculateCapacity方法计算出来了。

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}
复制代码

这个方法没有太难的逻辑,步骤大概如下:

  • 先判断当前数组是否是一个空的数组,如果是的话,它会将默认的容量和传入来的minCapacity做对比,取出一个最大值,做为扩容后的数组大小。(这里就就是在创建ArrayList如果没有传入大小时,为啥ArrayList的容量是10的原因)
  • 如果不是一个空的数组,就会将当前传入的minCapacity直接返回

检查是否需要扩容

当确定完插入数据后所需要的容量,就要开始判断当前数组的大小是否可以将这个元素插入了。经过判断后如果容量不足就会触发扩容。

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}
复制代码

这个方法判断是否需要扩容的条件是:

使用minCapacity减去当前数组的大小如果大于0就说明当前数组不足以插入一个元素,就需要进行扩容操作

扩容方法

这个扩容的方法需要一个参数,这个参数就是插入数据后的数组所需要的最小容量

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
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);
}
复制代码

这个扩容的方法大至分为5步:

  • 第一步:首先会将未扩容之前的数组长度记录下来
  • 第二步:会生成一个新的数组长度,这个长度就是数组扩容之后的长度。新的长度是(旧容量 + 旧容量 / 2),因为 左移操作就等于除2嘛
  • 第三步:检查新生成的数组容量减去最小容量是否大于0,否就说明当前生成新的容量并不能容下新插入的数据
  • 第四步:检查新生成的长度减最大数组容量是否大于0。为真就会通过调用hugeCapacity方法生成新的数组长度
    • 在个别的JVM里面,如果将Integer.MAX_VALUE结果作为数组的长度有可能会发生OOM,所以这里将MAX_ARRAY_SIZE设置成了Integer.MAX_VALUE - 8
    • 当newCapacity比MAX_ARRAY_SIZE大时,调用hugeCapacity方法。这个方法原理是判断minCapacity 是否大于MAX_ARRAY_SIZE,如果小于就会将MAX_ARRAY_SIZE返回作为新的数组长度。否则就会将Integer.MAX_VALUE 返回。
    • 这里有些伙伴可能存在疑惑,为啥这里又把Integer.MAX_VALUE 返回了,不是要避免OOM吗?原因是minCapacity 都比MAX_ARRAY_SIZE大了,那就直接把Integer.MAX_VALUE返回,也不管什么OOM,爱咋咋滴吧。
  • 第五步:到了这一步,就是将通过Arrays.copyOf方法,把老的数组和新的容量传入,就会生成一个新的数组并赋值给elementData,完成扩容。

插入数据

上面的过程完成后就可以调用add方法里面赋值的过程。

public boolean add(E e) {
    //.... 
    elementData[size++] = e;
    //....
}
复制代码

将当前的元素赋值给elementData中的某一个位置。并且将数组元素的数量加1(size++)

就完成了插入操作。

指定位置插入元素

先过一遍源码,这个里面源码好说一些,因为ensureCapacityInternal在上一章已经说过了,这里就不重复了。

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++;
}
复制代码

这个指定位置插入数据大致可以分成四步:

  • 检查索引是否存在错误
  • 是否需要扩容
  • 数组的拷贝
  • 在数组的指定位置赋值

检查索引是否存在错误

在指定位置插入之前首先要判断要插入的位置是否越界。

private void rangeCheckForAdd(int index) {
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
复制代码

原理就是判断了index是否大于数组的最大长度,或者是index小于0,如果这个条件成立,就说明不能将该元素插入这个位置。

拷贝数组

public void add(int index, E element) {
    //....
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    //....
}
复制代码

拷贝数组的时候主要是调用了System.arraycopy之个方法,这个方法是一个native方法。只来看一下它要传的参数就好:

public static native void arraycopy(Object src,  int  srcPos,
                                    Object dest, int destPos,
                                    int length);
复制代码
  • src: 源数组
  • srcPos:从源数组的那个位置开始拷贝
  • dest:目标数组
  • destPos: 目标数组的起始位置
  • length: 需要拷贝的长度

这样拷贝完成后,要插入元素的index位置就空了出来,然后将这个要插入的元素赋值到数组的index位置就好了。然后将数组元素的数量(size)加1。

查询元素

ArrayList除了添加放法,常用到的还有一个查询元素的方法get()

public E get(int index) {
    rangeCheck(index);

    return elementData(index);
}
复制代码

这个方法就比较简单了,首先检查下传入index是否会存在越界,如果越界就会抛出 IndexOutOfBoundsException异常。不存在越界问题就直接将数组该位置的元素返回。

移除元素

移除元素也有多个方法,下面一一介绍下:

  • 移除指定位置元素
  • 移除指定数据

移除指定位置元素

移除指定位置的元素只需要将元素在数组中的下标传入即可。源码如下:

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; 

    return oldValue;
}
复制代码

移除指定位置的元素大至可以分为以下几步:

  • 检查当前传入的索引位置是否越界,通过rangeCheck方法进行检查

  • 保存当前所要删除的值,在return的时候将其返回

  • 计算出所要移动数组的长度(size - index - 1) ,你会发现这个公式的结果就是在index元素之后的元素个数。

  • numMovedが0より大きいかどうかを確認します。0より大きい場合は、この時点で配列に要素が1つあるか、まったくないことを意味します。0より大きい場合は、次のように配列コピーメソッドが実行されます。上の図。

  • 最後に、配列内の要素の数を1つ減らし、最後の要素を空にして、GCの回復を容易にします。

指定されたデータを削除する

ArrayListの要素を削除するメソッドもあり、要素の値を渡して削除することができます

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;
}
复制代码

要素の値を渡すと、配列をループして要素の値を削除します。

ループする前に、着信値が空かどうかをチェックします。

Q:空かどうかを区別する必要があるのはなぜですか?

回答:nullが渡された場合、equalsメソッドを呼び出すときにnullポインター例外が発生します。

次に、ifのブランチに入ると、配列のトラバースを開始します。入力値が配列がトラバースした値と等しいことがわかった場合、fastRemoveメソッドが呼び出されて削除されます。

fastRemoveメソッドは、このメソッドのコードが上記の指定された位置要素を削除するためのコードと同じであるため、ここではあまり説明しません。

指定された位置で要素を交換します

ArrayListを使用する場合は、次のsetメソッドを使用する必要があるため、このメソッドがどのように実装されているかを見てみましょう。

 public static void main(String[] args) {
     List<String> l = new ArrayList<>(10);
     l.add("2");
     l.set(0,"4");
     System.out.println("替换后的元素值:" + l.get(0));
 }

替换后的元素值:4
复制代码

ArrayListの多くのメソッドと同様に、最初に着信インデックスが範囲外かどうかを判断します。

public E set(int index, E element) {
    rangeCheck(index);

    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}
复制代码

インデックスが範囲外でない場合は、古い値が保存されます。次に、それを新しい着信値に置き換えれば、問題ありません。

次に、古い値を返します。

ここでは、私たちが一般的に使用するいくつかの方法の実装原理のみを説明します。他の方法に興味がある場合は、自分で確認できます。

おすすめ

転載: juejin.im/post/7082708521313107981