java集合-ArrayList源码详解

文章目录

  • 前言
  • 一、ArrayList的继承和接口实现
  • 二、ArrayList底层实现
    • 1.底层结构
    • 2.增删改查
  • 三、ArrayList要知道的设计
      • 扩容机制
      • Fail-Fast机制 modCount
  • 四、另言

导言:
在实际应用开发的过程中,对于数据的操作我们常常考虑这样的问题:需要快速搜索成千上万个有序序列吗?需要快速插入删除有序序列吗?需要建立键值之间的关联吗?当非常关注性能时,选择不同的数据结构会带来很大的差异。



前言

ArrayList在java集合中的位置
java集合-ArrayList源码详解_第1张图片
ArrayList是List(有序)接口的动态数组的实现。实现了所有可选列表操作,并允许包括 null 在内的所有元素。可存储重复元素。
ArrayList的动态性体现在于随着向ArrayList中不断添加元素,其容量也自动增长。自动增长会带来数据向新数组的重新拷贝,因此,如果可预知数据量的多少,可在构造ArrayList时指定其容量。
此外注意,ArrayList此实现不是同步的(Vector同步)。ArrayList采用了快速失败的机制,通过记录modCount参数来实现。在面对并发的修改时,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。

一、ArrayList的继承和接口实现

先看源码(jdk 1.8)

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
  • AbstractList
    其实在java集合中以Abstract开头的抽象类实现了对应接口的大部分函数,像AbstractCollection实现了Collection的大部分函数,我们继承这些抽象函数要比实现接口轻松许多。本例中AbstractList实现了List接口,为List接口方法添加了默认实现和拓展。
  • List接口
    拓展自Collection接口,list接口属于有序序列,此接口的用户可以对列表中每个元素的插入位置进行精确地控制。用户可以根据元素的整数索引(在列表中的位置)访问元素,并搜索列表中的元素。
  • RandomAccess接口
    这个接口比较有意思,是一种标记接口,字面意思理解为“随机访问”,即数组的访问形式。list接口的另一个实现类LinkedList是基于链表实现的,只有迭代器访问形式。两者只有ArrayList实现了RandomAccess,那么我们就可以通过判断是否是RandomAccess的实例来决定集合是ArrayList或者LinkedList,从而能够更好选择更优的遍历方式(for遍历/迭代器遍历),从而提高性能!
  • Cloneable接口
    Cloneable其实也是一个标记接口(同RandomAccess),只有实现这个接口后,然后在类中重写Object中的clone方法,然后通过类调用clone方法才能克隆成功,如果不实现这个接口,则会抛出CloneNotSupportedException异常。
  • java.io.Serializable接口
    当我们需要将一个类对象的状态信息在网络中传输时,就需要将把对象转换为字节序列,这个过程就是序列化(即转化成可进行IO传输的形式)我们的类只有实现这个接口,该类对象才可以被序列化进行传输。

二、ArrayList底层实现

1.底层结构

源码

   transient Object[] elementData; 
//内部是用到了一个数组来进行元素的存储(transient 可避免序列化此字段)
    private int size;
    //size表示实际元素的个数
    protected transient int modCount = 0;
    //表示数组操作数,监测并发修改

java集合-ArrayList源码详解_第2张图片

另外可以使用size来和数组的大小进行比较进行释放数组多余的空间,节约资源,接口函数为trimToSize

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

2.增删改查

本部分个人觉得只需说一下ArrayList增 查(遍历)

增 add /addAll

add和addAll各有两个重载,add可选择直接在数组尾部增加元素,也可选择在指定的位置增加元素。

  public boolean add(E e) {
  /*确定当前数组容量在增加n个元素是否充足,
  充足继续执行,否则最终通过grow函数扩充容量
  */
       ensureCapacityInternal(size + 1); 
        elementData[size++] = e;
        //存入数组
        return true;
    }
   
   public void add(int index, E element) {
        rangeCheckForAdd(index);
  //判断index是否是越界访问。
        ensureCapacityInternal(size + 1);  
        // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }

addAll

public boolean addAll(int index, Collection<? extends E> c) {
        rangeCheckForAdd(index);
//判断index是否是越界访问。
        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount
        int numMoved = size - index;
        if (numMoved > 0)
            System.arraycopy(elementData, index, elementData, index + numNew,
                             numMoved);

        System.arraycopy(a, 0, elementData, index, numNew);
        size += numNew;
        return numNew != 0;
    }

说明:addAll函数是增加参数集合中的全部元素,index参数指明具体在调用者哪一个位置开始加入元素。

  • System.arraycopy(a,b,c,d,e)
    将a数组中从下标b开始的元素移动到c数组从下标d的位置,共移动e个元素。

遍历

在ArrayList中含有实现了Iterator和listIterator两个迭代器接口。
Iterator实现类的三个属性

    int cursor;     //当前元素位置
    int lastRet = -1; //访问的上一个元素位置
    int expectedModCount = modCount;
    //用于处理并行访问迭代器时的快速失败策略

Iterator接口中含有下面四个方法

boolean hasNext();
//如果迭代器对象还有多个可以访问的对象,则返回true
E next();
//通过反复调用next()可以访问集合中的元素
default void remove();
//删除上一个访问的元素
default void forEachRemaining(Consumer<? super E> action) ;
//可以使用该方法结合一个lambda表达式进行对每一个元素进行操作

使用例子:

    ArrayList<Integer> arrayList=new ArrayList<>();
      for(int i=0;i<10;i++)
      arrayList.add(i);
        Iterator<Integer> iterator=arrayList.iterator();
        //随机访问
        for(Integer i:arrayList)
        {
            System.out.println(i);
        }
        //迭代器遍历
        while(iterator.hasNext())
        {
            System.out.println(iterator.next());
        }
       //forEachRemaining方法
        iterator.forEachRemaining(e-> System.out.println(e));

说明:Iterator和listIterator两个迭代器接口有什么区别呢?(说一下前者的优点)
listIterator可以说是Iterator的加强版,前者可以定位当前索引的位置,有nextIndex()和previousIndex(),有hasPrevious()和previous()方法,可以实现向前遍历,有add(e)方法,可以向List中添加对象,有set(e)方法,可以对List中的对象进行修改。


三、ArrayList要知道的设计

扩容机制

每当向数组中添加元素时,都要去检查添加后元素的个数是否会超出当前数组的长度,如果超出,数组将会进行扩容,以满足添加数据的需求。数组扩容通过一个公开的方法ensureCapacity(int minCapacity)来实现。在实际添加大量元素前,我也可以使用ensureCapacity来手动增加ArrayList实例的容量,以减少递增式再分配的数量。 数组进行扩容时,会将老数组中的元素重新拷贝一份到新的数组中,每次数组容量的增长大约是其原容量的1.5倍。这种操作的代价是很高的,因此在实际使用时,我们应该尽量避免数组容量的扩张。当我们可预知要保存的元素的多少时,要在构造ArrayList实例时,就指定其容量,以避免数组扩容的发生。或者根据实际需求,通过调用ensureCapacity方法来手动增加ArrayList实例的容量。
源码这里介绍扩容函数grow()

 private void grow(int minCapacity) {
        // 获取数组的长度
        int oldCapacity = elementData.length;
        //扩大至1.5倍的容量  >>1相当于除以2
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // 更换成新数组的引用
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

java集合-ArrayList源码详解_第3张图片

Fail-Fast机制 modCount

ArrayList也采用了快速失败的机制,通过记录modCount参数(操作数)来实现。在面对并发的修改时,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。

四、另言

`

  • 因为数组是一个有界集合,如果程序中收集的数据没有上限就需要我们使用链表来实现。

  • ArrayList对于频繁数据增删的场景不适用

  • 为追求效率,ArrayList没有实现同步。(synchronized),如果需要多个线程并发访问,用户可以手动同步,也可使用Vector替代。

    欢迎朋友讨论指正哈~ ~

你可能感兴趣的:(java技术,java,数据结构)