ArrayList,基于数组实现的有序列表

Posted 毛奇志

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ArrayList,基于数组实现的有序列表相关的知识,希望对你有一定的参考价值。

一、前言

二、ArrayList基础

2.1 底层数据结构

ArrayList实现了List接口,ArrayList是一个可调整大小的数组(数组插入的时候扩容,删除的时候减少容量,使用复制的方式,效率低,但是查询效率高)可以用来存放各种形式的数据,并提供了包括CRUD在内的多种方法可以对数据进行操作,但是它不是线程安全的,另外ArrayList是有序的,按照插入的顺序来存放数据。

所以,ArrayList底层是数组,查询效率高,插入删除效率低,其扩容也是底层数组的容量增大。

2.2 ArrayList的五个成员变量

private static final int DEFAULT_CAPACITY = 10;//数组默认初始容量
 
private static final Object[] EMPTY_ELEMENTDATA = {};//定义一个空的数组实例,以供其他需要用到空数组的地方调用,在ArrayList的两个带参构造函数中使用
 
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};//定义一个空数组,以供其他需要用到空数组的地方调用,在ArrayList的无参构造函数/默认构造函数中使用,另外,这个空数组是用来判断ArrayList第一添加数据的时候要扩容多少
 
transient Object[] elementData;//数据存的地方它的容量就是这个数组的长度,同时只要是使用默认构造器(DEFAULTCAPACITY_EMPTY_ELEMENTDATA )第一次添加数据的时候容量扩容为DEFAULT_CAPACITY = 10 
 
private int size;//当前数组的长度

小结:ArrayList类中的五个参数(三个初始化时,两个运行时):

第一,三个初始化时:
private static final int DEFAULT_CAPACITY = 10; static类型,定死了的,表示数组默认初始容量
private static final Object[] EMPTY_ELEMENTDATA = {}; static类型,定死了的
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; staic类型,定死了的

第二,两个运行时:
elementData和size区别,一个是Object数组,实际容量,一个是int,数组容量(数组容量>=实际容量,size>=elementData.length)

2.3 ArrayList三种构造方法

ArrayList的构造方法有三种,如下:

// 无参构造函数
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;   // 该常量表示空数组
}
// 指定集合框架的构造函数 
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;   // 该常量表示空数组
    }
}
// 指定初始化大小的构造函数 
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);
    }   
}

金手指:三个构造函数的elementData实际数组

第一个,无参构造函数,elementData 为 DEFAULTCAPACITY_EMPTY_ELEMENTDATA,可以看到默认的构造器就是用了参数DEFAULTCAPACITY_EMPTY_ELEMENTDATA返回了一个空的数组,所以这边我们可以了解到ArrayList在创建的时候如果没有指定初始容量的话就会返回一个长度为0的空数组。

第二个,参数为集合的构造函数,分两种情况:
集合为空,elementData 为 EMPTY_ELEMENTDATA;
集合不为空,elementData为传入的集合。

第三个,参数为int类型指定容量的构造函数,分两种情况:
int为0,elementData为EMPTY_ELEMENTDATA;
int不为0, this.elementData为int个数的Object对象,成为数组Object数组。

注意:初始化的过程就是设置elementData这个Object数组,这个elementData是类变量,是核心存储,后面扩容 add remove都会用到的,要引起注意。

下面从ArrayList的扩容机制开始解析,因为在所有添加数据的操作上面都要需要判断当前数组容量是否足以容纳新的数据,如果不够的话就需要进行扩容,且看本文第三部分。

三、ArrayList数组扩容机制

ArrayList扩容的核心从ensureCapacityInternal()方法说起。可以看到前面介绍ArrayList类的五个成员变量的时候,提到的ArrayList有两个默认的空数组:

1、DEFAULTCAPACITY_EMPTY_ELEMENTDATA:在第一个构造函数中出现,是用来使用默认构造方法时候返回的空数组。如果第一次添加数据的话那么数组扩容长度为DEFAULT_CAPACITY=10。

2、EMPTY_ELEMENTDATA:在第二个和第三个构造函数中出现,出现在需要用到空数组的地方,即第二个构造函数指定的集合为空数组或第三个构造函数指定的容量为0。

从三个构造函数中可以看出,如果是第二种情况,使用了空数组EMPTY_ELEMENTDATA话,那么不会返回默认的初始容量。

3.1 计算容量

使用calculateCapacity()来计算容量,如下:

//判断当前数组是否是默认构造方法生成的空数组,如果是的话minCapacity=10,如果不是则根据原来的值传入下一个方法去完成下一步的扩容判断
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    // if条件直接比较引用是否相等 
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {  
        return Math.max(DEFAULT_CAPACITY, minCapacity);  // 这里是DEFAULT_CAPACITY这个定死的static变量的唯一使用,就是用来这里计算容量
    }
    return minCapacity;
}

解释一下上面这个方法:
(1) calculateCapacity()方法用来计算容量,计算完成后,其返回值作为调用ensureExplicitCapacity()方法的实参来扩容;
(2) minCapacity表示修改后的数组容量,即 minCapacity = size + 1,下面的add,就是传入size+1。

3.2 ensureCapacityInternal() 与 ensureExplicitCapacity()

ensureExplicitCapacity()方法(modCount涉及到Java的快速报错机制后面会谈到),可以看到如果修改后的数组容量大于当前的数组长度那么就需要调用grow进行扩容,反之则不需要。

// minCapacitt表示修改后的数组容量,minCapacity = size + 1,下面的add,就是传入size+1
private void ensureCapacityInternal(int minCapacity) {
    //判断看看是否需要扩容
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
//判断当前ArrayList是否需要进行扩容
private void ensureExplicitCapacity(int minCapacity) {
    //快速报错机制
    modCount++;
    // 传入的参数大于当前实际存储的时候,要调用grow()扩容,1.5倍扩容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

minCapacity - elementData.length >0 表示 minCapacity > elementData.length 传入的参数大于当前实际存储的时候,要调用grow()扩容。

ensureCapacityInternal()方法根据传入的实参minCapacity来判断ArrayList是否扩容,在添加元素的时候调用,如下:

3.3 扩容操作

ArrayList扩容的核心方法grow(),源码解析如下:

// ArrayList扩容的核心方法,此方法用来决定扩容量
private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
        //  newCapacity = Max (newCapcity,minCapacity);   // 扩容
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
        // newCapacity = Min(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;
}
 
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

如果看不懂的话,我们将grow()的代码稍作修改,更方便理解

//ArrayList扩容的核心方法,此方法用来决定扩容量
private void grow(int minCapacity) {
    int oldCapacity = elementData.length;  // 实际元素个数记录到oldCapacity 
    int newCapacity = oldCapacity + (oldCapacity >> 1);  //  oldCapacity数字上扩大1.5倍 得到  newCapacity 
    newCapacity = Max(newCapcity, minCapacity);   // 扩容  1.5*oldCapacity与传入参数比较,取出较大值
    newCapacity = Min(newCapacity, hugeCapacity(minCapacity));  // 限制newCapacity 上限
    // newCapacity 是局部变量,没什么用,唯一作用是使用newCapacity来设置elementData,elementData才是类变量,实际存储数组
    elementData = Arrays.copyOf(elementData, newCapacity);   // 复制数组  good 核心一句
}

下面将针对三种情况对该方法进行解析:

情况1:当前数组是由默认构造方法生成的空数组并且第一次添加数据:此时minCapacity等于默认的容量(10)那么根据下面逻辑可以看到最后数组的容量会从0扩容成10,而后的数组扩容才是按照当前容量的1.5倍进行扩容;

情况2:当前数组是由自定义初始容量构造方法创建并且指定初始容量为0:此时minCapacity等于1那么根据下面逻辑可以看到最后数组的容量会从0变成1,这里可以看到一个严重的问题,一旦我们执行了初始容量为0,那么根据下面的算法前四次扩容每次都 +1,在第5次添加数据进行扩容的时候才是按照当前容量的1.5倍进行扩容,即:

int oldCapacity = elementData.length;     oldCapacity 为当前容量
newCapcity  =   0+0>>1 =0 
newCapacity = Max (newCapcity = 0 ,minCapacity = 1);   =1 第一次   0-->1

1 + 1>>1 =2 第二次 1–>2
10 + 10>>1 = 2+ 1 = 3 第三次 2–>3
11 + 11>1 = 3+1 =4 第四次 3–>4
100 +100>1=4+2=6 第五次,开始1.5扩容 4–>6

情况3:当扩容量(newCapacity)大于ArrayList数组定义的最大值后会调用hugeCapacity来进行判断:如果minCapacity已经大于Integer的最大值(溢出为负数)那么抛出OutOfMemoryError(内存溢出),否则的话根据与MAX_ARRAY_SIZE的比较情况确定是返回Integer最大值还是MAX_ARRAY_SIZE。这边也可以看到ArrayList允许的最大容量就是Integer的最大值(-2的31次方~2的31次方减1)。

问题1:grow()方法的输入参数哪里来的?
回答1:本质上是从ensureCapacityInternal()方法中来的,add就是 size +1 ,addAll 就是size + 具体长度。

问题2:ArrayList完成扩容的?
回答2:ArrayList底层数据结构是数组,是通过数组复制来完成扩容的,数组复制逻辑如下:

elementData = Arrays.copyOf(elementData, newCapacity);  
System.arraycopy(original, 0, copy, 0,  Math.min(original.length, newLength));

对System.arraycopy()五个参数的解释:
original,0:原数组从0开始;
copy,0:目标数组从0开始;
Math.min(original.length, newLength):原数组复制 Math.min(original.length, newLength)个元素到目标数组中。

四、快速报错机制

4.1 ArrayList快速报错的理论解释

Java容器有一种保护机制,能够防止多个进程同时修改同一个容器的内容。如果你在迭代遍历某个容器的过程中,另一个进程介入其中,并且插入,删除或修改此容器的某个对象,就会立刻抛出ConcurrentModificationException。

前文提到的迭代遍历包括两种情况:使用迭代器Iterator(ListIterator)或者forEach循环,实际上一个类要使用forEach就必须实现Iterable接口并且重写它的Iterator方法所以forEach本质上还是使用Iterator。所以,我们直接看Iterator的源码解析就好了,如下:

public Iterator<E> iterator() {
    return new Itr();   // 调用iterator()方法就是新建一个迭代器的实现类
}
 
private class Itr implements Iterator<E> {    
    int cursor;       // 下一个要返回的索引
    int lastRet = -1; // 返回最后一个元素的索引
    int expectedModCount = modCount;   // modCount是arrayList的变量,expectModCount是内部类iterator的变量,当你对这个集合进行遍历的时候就把modCount传到expectedModCount这个变量里
 
    public boolean hasNext() {      // 查询是否为最后一个elementData数组原则
          return cursor != size;
    }
 
    @SuppressWarnings("unchecked")
    public E next() {         //  返回下一个elementData数组元素
        checkForComodification();
        //防止篇幅过长省去了其中代码
        return (E) elementData[lastRet = i];
    }
 
    public void remove() {    // 实用remove,删除只要,要expectedModCount = modCount;,这样check判断才是正确的
        if (lastRet < 0)
            throw new IllegalStateException();
        checkForComodification();   // iterator remove之前检查
 
        try {
            ArrayList.this.remove(lastRet);
            cursor = lastRet;
            lastRet = -1;
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }
 
    @Override
    @SuppressWarnings("unchecked")
    public void forEachRemaining(Consumer<? super E> consumer) {
        //防止篇幅过长省去其中代码
        checkForComodification();    // 调用check判断
    }
 
    final void checkForComodification() {    
        //检查,check判断的依据是modCount == expectedModCount,不相等表示不同步,报错 
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}

从上面代码中可以看到在迭代遍历的过程中都调用了方法checkForComodification来判断当前ArrayList是否是同步的。

举个例子,假设你往一个Integer类型的ArrayList插入了10条数据,那么每操作一次modCount(继承自父类AbstractList)就加1,所以就变成10,而当你对这个集合进行遍历的时候就把modCount传到expectedModCount这个变量里(int expectedModCount = modCount;),然后ArrayList在checkForComodification中通过判断两个变量是否相等来确认当前集合是否是同步的,如果不同步就抛出ConcurrentModificationException。

所谓的不同步指的就是,如果你在遍历的过程中对ArrayList集合本身进行add,remove等操作时候就会发生。当然如果你用的是Iterator那么使用它的remove是允许的因为此时你直接操作的不是ArrayList集合而是它的Iterator对象。

对于一个Integer泛型的ArrayList,删除指定位置的元素是remove(int i),删除指定值的元素的是remove(Integer integer),两个删除方法重载,只是参数类型不同,一个是值类型,一个Object引用类型。

4.2 三个Demo验证快速报错机制

我们用三个Demo来演示:

第一种情况使用Iterator:失败原因:不能在while iterator迭代中,直接对arrayList add remove数据

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class ArrayListTest {
    public static void main(String[] args) {
        List<Integer> list=new ArrayList<>();
        for (int i=0;i<10;i++)
            list.add(i);
        Iterator iterator=list.iterator();
        while (iterator.hasNext()){
            int i=(int)iterator.next();
            list.remove(i);
        }
        System.out.println("result: "+list);
    }
}

运行结果:

Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
	at java.util.ArrayList$Itr.next(ArrayList.java:851)
	at mypackage1.ArrayListTest.main(ArrayListTest.java:14)

第二种情况使用forEach:错误原因,不能在foreach循环中,直接对arrayList add remove元素

import java.util.ArrayList;
import java.util.List;

public class ArrayListTest2 {
    public static void main(String[] args) {
        List<Integer> list=new ArrayList<>();
        for (int i=0;i<10;i++)
            list.add(i);
        for (Integer i:list)
            list.remove(i);
        System.out.println("result: "+list);
    }
}

运行结果:

Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
	at java.util.ArrayList$Itr.next(ArrayList.java:851)
	at mypackage1.ArrayListTest2.main(ArrayListTest2.java:12)

foreach底层就是Iterator迭代器。

第三种情况使用Iterator自身删除数据:正确,在while iterator或foreach中,实用iterator add remove arraylist中的数据

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class ArrayListTest3 {
    public static void main(String[] args) {
        List<Integer> list=new ArrayList<>();
        for (int i=0;i<10;i++)
            list.add(i);
        Iterator iterator=list.iterator();
        while (iterator.hasNext()){
            int i=(int)iterator.next();
            iterator.remove();
        }
        System.out.println("result: "+list);
    }
}

运行结果:

result: []

此外在多线程也会存在这种情况,但是如果你在多线程中使用CopyOnWriteArrayList就可以避免了。

五、ArrayList的基本操作

这里介绍ArrayList的基本操作,在插入和删除中理解扩容和缩容。

5.1 添加数据

ArrayList中添加数据方法一共四个,如下:
1、boolean add(E e):将指定元素追加到此列表的末尾。
2、void add(int index,E element):在此列表中的指定位置插入指定的元素。
3、boolean addAll(Collection<? extends E> e):按指定集合的Iterator返回的顺序将指定集合中的所有元素追加到此列表的末尾。
4、boolean addAll(int index,Collection<? extends E> c):将指定集合中的所有元素插入到此列表中,从指定的位置开始。

5.1.1 add(E e)

从add(E e)方法可以看到每次添加数据ArrayList都会先调用ensureCapacityInternal来判断是否需要扩容,接着再插入数据并且每次末尾插入,所以ArrayList是按插入的顺序排序。

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

末尾插入两步骤:
步骤1:底层grow多设置一个elementData元素,elementData = Arrays.copyOf(elementData, newCapacity);
步骤2:扩容多出来的元素正好存放新值

ensureCapacityInternal(size + 1); //  这里是size+1,底层grow多设置一个elementData元素,elementData = Arrays.copyOf(elementData, newCapacity);
elementData[size++] = e;  // 末尾插入,扩容多出来的元素正好存放新值,所以对外表现为末尾插入

5.1.2.add(int index,E element)

add(int index,E element)与前面的add只多了一个参数index,index表示你要插入的位置。此时会先判断是否会出现数组越界,然后再调用ensureCapacityInternal方法紧接着可以看到调用了System.arraycopy方法来进行操作因为该方法为本地方法(native)所以并不是用Java来实现的。根据这个方法的参数解释我们可以了解到ArrayList每次指定位置添加数据的时候都会进行数组的复制,复制的过程为把相对于当前插入位置(index)后面的数据都向后移动一位(如下图所示)。因此我们说ArrayList在对数据的插入上效率比较差,随着数据量的增大花费的时间越多。这也是我们常说的ArrayList在随机插入数据的效率上比不上LinkedList。

//index表示element要插入的位置
public void add(int index, E element) {
    //判断插入的位置是否当前数组长度或是小于0,是的话会抛出IndexOutOfBoundsException
    rangeCheckForAdd(index);
 
    ensureCapacityInternal(size + 1);  // 判断是否需要扩容,保证新进来元素有位置可以插入
 
    //每一次插入数据都要把相对于当前index后面的数据向后移动一位
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);   
                     // 原数组elementData从index开始,目标数组elementData从index+1开始,将原数组中从index开始的 size-index 个元素复制到目标数组中去
    elementData[index] = element;   // 设置中间空出来的这个index
    size++;   // 变量值修改
}
 
/*src - 源数组
  srcPos - 源数组中的起始位置 
  dest - 目标数组
  destPos - 目的地数据中的起始位置
  length - 要复制的数组元素的数量*/
public static native void arraycopy(Object src,  int  srcPos,
                                    Object dest, int destPos,  int length);

搞懂System.arraycopy,是查看下面的源码的基础:

//我们有一个数组数据
byte[]  srcBytes = new byte[]{2,4,0,0,0,0,0,10,15,50};  // 源数组
byte[] destBytes = new byte[5]; // 目标数组
// 我们使用System.arraycopy进行转换(copy)
System.arrayCopy(srcBytes,0,destBytes ,0,5)  // 源数组从0开始,目标数组从0开始,原数组copy 5个到目标数组
那么这行代码的运行效果应该是 2,4,0,0,0

5.1.3.addAll(Collection<? extends E> c)

下面看看addAll的两个方法。首先可以看到接受的参数对象为一个集合类型。如果你试图把跟当前类型不同的集合添加进来的话有两种情况会发生:

第一种:如果你用了泛型,那么你在试图把Integer的类型的集合addAll到String类型的集合中就会在编译器抛出错误信息。

第二种:如果你不使用泛型(Object),那么你可以在一个String类型的集合中,放入Integer,类对象等等。但是当你遍历ArrayList集合要取出当中的数据进行操作的时候,除非你每次强制转换都正确,不然就会抛出ClassCastException。所以这也是使用泛型的一个好处之一吧(可以让错误在编译的时候就暴露出来,而不用等到运行的时候)。

public boolean addAll(Collection<? extends E> c) {
    Object[] a = c.toArray();
    int numNew = a.length;
    ensureCapacityInternal(size + numNew);  // elementData数组扩容为size+numNew,先扩容,后插入
    System.arraycopy(a, 0, elementData, size, numNew);
    // 原数组a从0开始,目标数组elementData从size开始,元素组中numNew个元素复制到目标数组elementData中去
    size += numNew;   // 修改size记录
    return numNew != 0;
}

5.1.4.addAll(int index, Collection<? extends E> c)

接下来可以看到addAll方法也提供了一个随机插入的方法。这跟前文提到的add大相径庭这边就不再赘述

public boolean addAll(int index, Collection<? extends E> c) {
    rangeCheckForAdd(index);
 
    Object[] a = c.toArray();
    int numNew = a.length;
    ensureCapacityInternal(size + numNew);  // elementData扩容到size + numNew
 
    int numMoved = size - index;
    if (numMoved > 0)
        System.arraycopy(elementData, index, elementData, index + numNew,
                         numMoved);
       // 原数组elementData从index开始,目标数组elementData从index + numNew开始,元素复制numMoved个元素到目标数组中
    // 对于中间的,新元素a从0开始,目标数组从index开始,原数组numNew个元素复制到目标数组
    System.arraycopy(a, 0, elementData, index, numNew);
    // 末尾插入,新元素a从0开始,目标数组从index开始,原数组numNew个元素复制到目标数组
    size += numNew;
    return numNew != 0;
}

5.2 删除数据

JDK1.8新增了一个方法removeIf (实现Collection接口),下面将按照顺序来依次解析。

ArrayList中添加数据方法一共五个,如下:
1、E remove(int index):删除该列表中指定位置的元素。
2、boolean remove(Object o):从列表中删除指定元素的第一个出现(如果存在的话)。
3、boolean removeAll(Collection<?> c):从此列表中删除指定集合中包含的所有元素。
4、boolean removeIf(Predicate<? super E> filter):删除满足给定谓词的此集合的所有元素。
5、protected void removeRange(int fromIndex,int toIndex):从这个列表中删除所有索引在fromIndex(含)和toIndex之间的函数。

5.2.1.remove(int index)

很明显remove在删除的时候也用了System的本地方法arraycopy,跟前文add不同的是它把相对于插入位置的后几位数据全部向前移动一位并且,此外该方法会返回被删掉的数据。

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);
    // 向前移动,原数组从index+1开始,目标数组从index开始,原数组中numMoved个元素复制到目标数组中
    elementData[--size] = null; // clear to let GC do its work 消除过期对象的引用
 
    return oldValue;
}

5.2.2.remove(Object o)

接着看看ArrayLsit对Object对象的remove,从方法中可以看到ArrayList在对Object对象删除操作上区分开了Null,重点要注意的是在对非空对象进行删除的时候ArrayList是调用了equals来匹配数组中的数据。也就是说如果你的集合(不局限于ArrayList)是对类进行操作,而你的类没有重写hashCode以及equals,那么你通过该方法来删除数据都是无法成功的,总之如果你要在集合中对类对象进行操作就需要重写上述的两个方法。此外就算你ArrayList中存有多个相同的Object对象,执行该方法也只会删除一次。

问题:既然使用equals那直接重写equals不就好了何必跟着重写hashCode呢?
回答:如果你只重写equals是可以完成删除操作,但是你重写equals没有重写hashCode那么你在使用散列数据结构HashMap,HashSet对该类进行操作的话会出错(JDK1.8 HashMap工作原理(Get,Put)和扩容机制)。而在Object规范中提到的第二点要求就是如果两个对象经过equals比较后相同,那么他们的hashCode一定相同。所以这就是为什么要hashCode跟euqals两者同时重写。

金手指:
(1) 如果使用remove(object)就要重写hashcode和equals,但是remove(index)不需要这样做,所以尽量采用remove(int index);
(2) 无论是remove(object)还是remove(index)源码里面都没有for/while循环,都只删除一个元素。
(3) remove(object):ArrayList在对Object对象删除操作上区分开了Null,
如果Object==null 用 == 比较;
如果object != 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 {
        集合家族——List集合汇总

List集合

在Java中,ArrayList与数组如何相互转换,写出例子

在 Arraylist 的 listview 的 listitem 上显示值

java集合List集合之ArrayList详解

基于时间复杂度的这些片段真的很困惑