ArrayList从入门到入土

ArratList从入门到入土

基本介绍

继承图

​ 要想了解一个类,首先先看一下其的继承关系:

image

概述

ArrayList实现了List接口,是顺序容器,即元素存放的的顺序与放进去的顺序相同;允许放入null元素和元素 重复。

底层通过数组实现,未实现同步。

  • 随机访问速度快,插入和移除性能较差
  • 数组大小灵活调整
  • ArrayList是基于动态数组实现的,在增删时候,需要数组的拷贝复制
  • ArrayList的默认初始化容量是10.每次扩容时候增加原先容量的一半,也就是变为原来的1.5倍
  • 删除元素时不会减少容量,若希望减少容量则调用trimToSize()
  • 使用iterator遍历可能会引发多线程异常
ArrayList<String> alist = new ArrayList<String>();

由于ArrayList实现了List接口,所以alist变量的类型可以是list类型;new关键字声明后的尖括号中也可以不用再指定元素的类型。

由于实现了RandomoAccess接口,即提供了随机访问的功能。我们可以通过元素的需要快速获得元素对象。

由于实现了Cloneable接口,即覆盖了函数clone()能被克隆

由于实现了Serializable接口,意味着支持序列化,能通过序列化去传输数据

基本属性

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, Serializable {  
    // 序列化id  
    private static final long serialVersionUID = 8683452581122892189L;  
    // 默认初始的容量  
    private static final int DEFAULT_CAPACITY = 10;  
    // 一个空对象(为什么是new Object[0]呢?)
    //用Object[0]来代替null 很多时候我们需要传递参数的类型,而不是传null,所以用Object[0]
    private static final Object[] EMPTY_ELEMENTDATA = new Object[0];  
    // 一个空对象,如果使用默认构造函数创建,则默认对象内容默认是该值  
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = new Object[0];  
    // 当前数据对象存放地方,当前对象不参与序列化(主要是关键字transient起作用的)  
    transient Object[] elementData;  
    // 当前数组长度  
    private int size;  
    // 数组最大长度  
    private static final int MAX_ARRAY_SIZE = 2147483639;  
}

方法

构造方法
无参构造方法
 public ArrayList() {
        // 创建一个空对象
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
传入容量的构造

​ 当传入的是0的时候调用EMPTY_ELEMENTDATA

​ 当传入的大于0就新建一个数组存储元素

 public ArrayList(int initialCapacity) {
       // 如果传入的初始容量大于0,就新建一个数组存储元素
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            // 如果传入的初始容量等于0,使用空数组EMPTY_ELEMENTDATA
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }
还支持从其他集合构造
    public ArrayList(Collection<? extends E> c) {
       //把集合转为数组
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            //  检查c.toArray()返回的是不是Object[]类型,如果不是,重新拷贝成Object[].class类型
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // 如果是空集合,则初始化为空数组EMPTY_ELEMENTDATA
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }
自动扩容

每次向数组中添加元素时们都要去检查添加后的元素的个数是否会超出当前数组的长度。

如果超过,将会调用ensureCapacity(int minCapacity)方法来进行扩容。

另外在实际添加大量元素前,也可以调用ensureCapacity来手动增加容量。

增加方法

add方法可说是ArrayList比较重要的方法,首先总览一下:

// 直接添加元素
public boolean add(E e) {        
    ensureCapacityInternal(size + 1);         
    elementData[size++] = e;        
    return true;    
}
// 插入到特定的位置上
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++;   
}
add(E,e)

添加元素到末尾,平均时间复杂度O(1)

步骤:

  • 检查是否需要扩容
  • 插入元素

首先,我们看这个方法:

public boolean add(E e) {
    // 检查是否需要扩容
    ensureCapacityInternal(size + 1);   
    // 把元素 插入到末尾
    elementData[size++] = e;        
    return true;    
}
  • 先确认list容量,尝试容量加一,看看有无必要
  • 添加元素

接下来看这个小容量(+1)是否能满足我们的需求

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    // 想要得到最小的容量(不浪费资源)
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

private void ensureCapacityInternal(int minCapacity) { 
    // 明确容量大小
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

private void ensureExplicitCapacity(int minCapacity) {        
    modCount++;      
    // 如果要的最小容量比数组的长度要打,那么就调用grow()来进行扩容。
    if (minCapacity - elementData.length > 0)          
        grow(minCapacity);   
}

然后调用grow扩容方法。

总结一下:

  • 首先去检查一下数组的容量是否足够

    • 扩容到原来的1.5倍
    • 第一次扩容后,如果容量还是小于minCapacty,就将容量扩容为minCapacity。
    • 足够:直接添加
    • 不足够:扩容
add(int index,E element)

在指定位置插入元素,时间复杂度O(n)

步骤:

  • 检查角标
  • 空间检查,如果有需要就进行扩容
  • 插入元素
public void add(int index, E element) { 
    // 检查角标是否越界
    rangeCheckForAdd(index);  
    // 检查是否需要扩容
    ensureCapacityInternal(size + 1);  
    // 调用arraycopy()来进行插入C++编写的
    System.arraycopy(elementData, index, elementData, index + 1,size - index);       
    //插入到指定位置并扩容
    elementData[index] = element;        
    size++;    
}
addALL

用于批量添加

  public boolean addAll(Collection<? extends E> c) {
       // 集合转化成数组
        Object[] a = c.toArray();
        
        int numNew = a.length;
        //检查是否需要扩容
        ensureCapacityInternal(size + numNew);  // Increments modCount
        //将集合内的元素复制到 elementData 中,覆盖 [size, size+numNew) 的元素
        System.arraycopy(a, 0, elementData, size, numNew);
        //数组大小增加numNew
        size += numNew;
        return numNew != 0;
    }


    public boolean addAll(int index, Collection<? extends E> c) {
       //检查下标是否越界
        rangeCheckForAdd(index);
        //转换为数组
        Object[] a = c.toArray();
        int numNew = a.length;
        //检查是否需要扩容
        ensureCapacityInternal(size + numNew);  // Increments modCount
        int numMoved = size - index;
        if (numMoved > 0)
           // 将 elementData 中位置为 index 及其以后的元素都向后移动 numNew 个位置
            System.arraycopy(elementData, index, elementData, index + numNew,
                             numMoved);
        // 将集合内的元素复制到 elementData 中,覆盖 [index, index+numNew) 的元素                      
        System.arraycopy(a, 0, elementData, index, numNew);
        size += numNew;
        return numNew != 0;
    }
扩容方法

数组进行扩容时,会将老数组中的元素重新拷贝一份到新数组中,每次数组容量的增长大约是其原容量的1.5倍。但是这种操作代价很高,在实际的生产中可以通过制定初始值以及手动增加来尽量避免。

grow()方法
private void grow(int minCapacity) {               
    int oldCapacity = elementData.length;   
    // 相当于扩容1.5倍
    int newCapacity = oldCapacity + (oldCapacity >> 1);        
    if (newCapacity - minCapacity < 0)            
        newCapacity = minCapacity;        
    if (newCapacity - MAX_ARRAY_SIZE > 0)            
        newCapacity = hugeCapacity(minCapacity);  
    // 扩容完以后,调用的是copyOf()方法
    elementData = Arrays.copyOf(elementData, newCapacity);    
}
copyOf()方法
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {       
    @SuppressWarnings("unchecked")        
    T[] copy = ((Object)newType == (Object)Object[].class)           
        ? (T[]) new Object[newLength]            
        : (T[]) Array.newInstance(newType.getComponentType(), newLength);        
    System.arraycopy(original, 0, copy, 0,                         
                     Math.min(original.length, newLength));        
    return copy;    
}
get方法

步骤

  • 检查角标
  • 返回元素
 public E get(int index) {  
     // 检查角标
     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];    
}
set方法

步骤:

  • 检查角标
  • 代替元素
  • 返回旧值
public E set(int index, E element) {   
    // 检查角标
    rangeCheck(index);   
    // 将值进行替换,返回旧值
    E oldValue = elementData(index);        
    elementData[index] = element;        
    return oldValue;    
}
remove方法

步骤

  • 检查角标
  • 删除元素
  • 计算出需要移动的个数,并移动
  • 设置为null,让GC回收
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;    
}
其他方法
trimToSize()

将底层数组的容量调整为当前列表保存的实际元素的大小功能。

public void trimToSize() {
        modCount++;
        if (size < elementData.length) {
            elementData = (size == 0)
              ? EMPTY_ELEMENTDATA
              : Arrays.copyOf(elementData, size);
        }
    }
indexOf(),lastindexOf()

分别是获取元素第一次和最后一次出现的位置

序列化

​ 需要注意的是ArrayList中有两个属性是被transient关键字修饰的。其作用就是让修饰的成员属性变量不被序列化

transient Object[] elementData;
protected transient int modCount = 0;

​ 由于ArrayList没有用Java序列化机制的默认处理来序列化elementData数组,而是通过readObject、writeObject方法自定义序列化和反序列化策略。

​ 之所以要用自定义的序列化和反序列化策略,是因为效率问题。防止序列化一些没有存数的字段

线程安全

​ ArrayList源码中,有很多快速失败的机制(通过记录modCount参数来实现),比如判断扩容的方法中:

private void ensureExplicitCapacity(int minCapacity){
    // 修改次数+1,用于 fail-fast 处理
    modCount++;
}

这种快读失败的机制一定程度上避免了线程安全问题

“快速失败”即fail-fast,它是java集合的一种错误检测机制。当多线程对集合进行结构上的改变或者集合在迭代元素时直接调用自身方法改变集合结构而没有通知迭代器时,有可能会触发fast-fail机制并抛出异常

快速失败举例

ArrayList的equals方法就使用到了fail-fast

将modCount赋值给一个expectedModCount变量,在对两个容器内的元素一一进行完比较判断后得出两个对象是否相等的判断,但在返回判断之前要问一个问题,在对比判断的过程中当前这个ArrayList(this)有没有被其他人(线程)动过?所以加了一个checkForComodification方法进行判断,如果modCount与原先不同则代表该ArrayList经过改动,则equals的判断结果并不可信,抛出throw new ConcurrentModificationException()异常

当然fail-fast机制只是可能触发,实际上,ArrayList的线程安全还是没有保证的,要想保证ArrayList的线程安全,可以使用一下几个方案

  • 使用Collections.SynchronizedList包装ArrayList,然后操作包装后的list即可
  • 使用CopyOnWriteArrayList代替ArrayList
  • 在使用ArrayList时,应用程序通过同步机制去控制ArrayList的读写(不推荐)
  • 使用Vector代替ArrayList(不推荐因为已经过时)

最后

  • 如果觉得看完有收获,希望能给我点个赞,这将会是我更新的最大动力,感谢各位的支持
  • 欢迎各位关注我的公众号【java冢狐】,专注于java和计算机基础知识,保证让你看完有所收获,不信你打我
  • 如果看完有不同的意见或者建议,欢迎多多评论一起交流。感谢各位的支持以及厚爱。

image

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
`ArrayList` 是一个常用的动态数组类,它提供了一种便捷的方法来管理对象集合。它是 Java 中的一个类,可以存储一组对象,并且可以随时动态调整其大小。`ArrayList` 实现了 `List` 接口,因此它具有 `List` 的所有功能,包括添加、删除、获取元素等。 以下是使用 `ArrayList` 的一些基本操作: ### 创建一个 ArrayList 使用 `ArrayList` 类创建一个新的 ArrayList 对象,可以指定初始容量大小(可选): ```java ArrayList<String> arrayList = new ArrayList<String>(); // 创建一个空的 ArrayList 对象 ArrayList<Integer> arrayList = new ArrayList<Integer>(10); // 创建一个容量为 10 的 ArrayList 对象 ``` ### 添加元素到 ArrayList 使用 `add()` 方法向 ArrayList 中添加一个元素: ```java ArrayList<String> arrayList = new ArrayList<String>(); arrayList.add("apple"); // 添加一个字符串 "apple" arrayList.add("banana"); // 添加一个字符串 "banana" ``` ### 获取 ArrayList 中的元素 可以使用 `get()` 方法从 ArrayList 中获取一个元素: ```java ArrayList<String> arrayList = new ArrayList<String>(); arrayList.add("apple"); arrayList.add("banana"); String fruit = arrayList.get(0); // 获取 ArrayList 中第一个元素 "apple" ``` ### 修改 ArrayList 中的元素 可以使用 `set()` 方法来修改 ArrayList 中的元素: ```java ArrayList<String> arrayList = new ArrayList<String>(); arrayList.add("apple"); arrayList.add("banana"); arrayList.set(1, "orange"); // 将 ArrayList 中的第二个元素修改为 "orange" ``` ### 删除 ArrayList 中的元素 使用 `remove()` 方法来删除 ArrayList 中的元素: ```java ArrayList<String> arrayList = new ArrayList<String>(); arrayList.add("apple"); arrayList.add("banana"); arrayList.remove(0); // 删除 ArrayList 中的第一个元素 "apple" ``` ### 遍历 ArrayList 使用 `for` 循环来遍历 ArrayList: ```java ArrayList<String> arrayList = new ArrayList<String>(); arrayList.add("apple"); arrayList.add("banana"); for (String fruit : arrayList) { System.out.println(fruit); } ``` 这将输出: ``` apple banana ``` 以上是 `ArrayList` 的一些基本操作,可以使用它来管理各种类型的对象集合。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值