欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Java ArrayList详解

程序员文章站 2022-06-23 10:44:24
简介ArrayList是List接口的典型实现,它是基于数组的,里面封装了一个动态的、允许再分配的Object[]数组。学过数据结构的应该都知道线性表的顺序存储结构,ArrayList就是线性表的顺序存储结构,所以它存、取和求表长的时间复杂度为O(1);插入和删除元素的时间复杂度为O(n).好多初学者没有搞明白存和插入的区别,这里稍作解释。存是指往某个位置放一个元素,不涉及其他位置元素的移动。以排队为例,队伍中第三个位置已经给你预留好了(包括空间),你直接走过去站在那就行,不管那里有没有人,有人你就替换...

简介

ArrayList是List接口的典型实现,它是基于数组的,里面封装了一个动态的、允许再分配的Object[]数组。
学过数据结构的应该都知道线性表的顺序存储结构,ArrayList就是线性表的顺序存储结构在Java中的实现,所以它存、取和求表长的时间复杂度为O(1);插入和删除元素的时间复杂度为O(n).
好多初学者没有搞明白“”和“插入”的区别,这里解释一下:“”是指往某个位置放一个元素,不涉及其他位置元素的移动。以排队为例,队伍中第三个位置已经给你预留好了(包括空间),你直接走过去站在那就行,不管那里有没有人,如果有人你就替换掉原来那个人,队伍其他位置的人不用动。“插入”是指没有预留给你空间,你要站在队伍第三个的位置,原来排第三个的人以及他后面的人都要整体往后移动。因此,ArrayList比较适合那种频繁的查找,很少插入和删除元素的场景。
另外,ArrayList不是线程安全的,所以建议在单线程中才使用ArrayList,多线程中选择Vector或CopyOnWriteArrayList.

构造器

  • ArrayList():空参构造器,默认容量10;
  • ArrayList(int initialCapacity):带参数的构造器,可以指定容量。
  • ArrayList(Collection<? extends E> c):构造一个包含指定Collection中元素的列表,列表中元素存储的顺序跟Collection的迭代器返回元素的顺序一样。

常用方法

  • boolean add(E e):将指定的元素追加到列表的结尾;
  • void add(int index, E element):向列表的指定位置插入一个元素。将当前位于该位置的元素(如果有的话)和它的后续元素向右移动,并且将这些向右移动的元素的索引+1;
  • E get(int index):返回列表中指定位置的元素;
  • E set(int index, E element):替换列表中指定位置的元素;
  • E remove(int index):移除列表中指定位置的元素,将该位置元素的后续元素整体向左移动,并将这些元素的索引-1;
  • ensureCapacity(int minCapacity):设置容量。虽然ArrayList可以动态扩容,但是扩容是比较耗费性能的,如果能知道放入ArrayList的元素有多少个,可以用此方法设置ArrayList的容量。
  • int size():返回列表中元素的个数。

核心源码

/**
 * 默认初始容量
 */
private static final int DEFAULT_CAPACITY = 10;
/**
 * 空数组,用于空的列表实例
 */
 private static final Object[] EMPTY_ELEMENTDATA = {};
 /**
  * 当我们用无参构造器初始化一个ArrayList对象时,容量是默认容量10,此时用于保存元素的数组还是
  * 空数组,直到真正使用时(比如第一次调用add方法添加元素时),才会初始化一个容量为默认容量的数组。
  * 这个空数组就是容量为默认容量时ArrayList的空数组。把它和EMPTY_ELEMENTDATA数组区分开来,以
  * 知道在添加第一个元素时容量需要增加多少。
  */
 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
 /**
  * ArrayList用来存储元素的数组。ArrayList的容量就是这个数组的长度。
  */
 transient Object[] elementData; // non-private to simplify nested class access
 /**
  * 列表中元素的个数。
  */
 private int size;
 /**
  * 不指定容量时,默认容量是10,但是初始化时并没有创建一个容量为10的数组,而是使用了默认的空数组。
  * 第一次调用add方法添加元素时才会创建一个容量为默认容量的数组
  */
 public ArrayList() {
 	this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
 }
 /**
  * initialCapacity大于0时直接初始化数组;
  * initialCapacity等于0时用的是EMPTY_ELEMENTDATA,之所以要跟DEFAULTCAPACITY_EMPTY_ELEMENTDATA
  * 区分开来,是为了分清楚主观上就是要让数组容量为0和数组长度会是默认容量10但是还没初始化这两种不
  * 同的情况。后面的很多代码都是根据使用的空数组对象的不同而走不同的业务逻辑
  */
 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);
     }
 }
 /**
  * 将ArrayList的容量修改成列表的当前大小。应用程序可以使用此操作来最小化ArrayList实例的
  * 存储空间。这里又涉及到数据结构的知识了,容量指的是数组的长度,也就是存放线性表的存储
  * 空间的长度;线性表的长度是线性表中数据元素的个数。线性表的长度总是小于等于数组的长度。
  */
 public void trimToSize() {
     modCount++;
     if (size < elementData.length) {
         elementData = (size == 0)
           ? EMPTY_ELEMENTDATA
           : Arrays.copyOf(elementData, size);
     }
 }
 /**
  * 1.minCapacity小于数组当前长度时啥也不干(总不能缩容吧)
  * 2.数组是空数组(数组容量本该是默认值,但是还没初始化),
  * 且minCapacity小于等于默认容量时啥也不干。
  * 其他情况下调用此方法都将扩容
  */
 public void ensureCapacity(int minCapacity) {
     if (minCapacity > elementData.length
         && !(elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
              && minCapacity <= DEFAULT_CAPACITY)) {
         modCount++;
         grow(minCapacity);
     }
 }
 /**
  * 数组长度大于0时要计算扩容多少容量,这里调用了jdk内部工具类ArraysSupport.newLength方法,
  * 这个方法的作用就是保证至少要扩容当前容量的一半,
  * 即在minCapacity - oldCapacity和oldCapacity >> 1之间求最大值,作为新增的容量。
  * 数组当前容量等于0时,在DEFAULT_CAPACITY和minCapacity之间求最大值,作为数组容量。
  */
 private Object[] grow(int minCapacity) {
     int oldCapacity = elementData.length;
     if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
         int newCapacity = ArraysSupport.newLength(oldCapacity,
                 minCapacity - oldCapacity, /* minimum growth */
                 oldCapacity >> 1           /* preferred growth */);
         return elementData = Arrays.copyOf(elementData, newCapacity);
     } else {
         return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
     }
 }
 /**
  * 往列表末尾追加元素。调用的是私有的add方法,s参数传的是元素个数size,
  * 如果size跟数组长度相等就扩容,因为size是列表长度,所以size - 1就是元素
  * 在数组中的index.上面提到过无参的构造器创建的ArrayList默认容量是10,但数组
  * 一开始不会初始化,所以在调用add方法时会初始化一个容量为10的数组,通过跟踪
  * grow方法的代码可以验证此观点
  */
 public boolean add(E e) {
     modCount++;
     add(e, elementData, size);
     return true;
 }
 private void add(E e, Object[] elementData, int s) {
     if (s == elementData.length)
         elementData = grow();
     elementData[s] = e;
     size = s + 1;
 }
 /**
  * 向列表指定位置插入元素。首先进行index的合法性检查。然后判断是否需要扩容,
  * 如果需要扩容则进行扩容相关操作。最后的插入元素的操作其实也比较简单,就是
  * 将从index开始的元素整体往后移动一个位置,通过数组复制实现,然后用新元素覆
  * 盖index处的元素
  */
 public void add(int index, E element) {
     rangeCheckForAdd(index);
     modCount++;
     final int s;
     Object[] elementData;
     if ((s = size) == (elementData = this.elementData).length)
         elementData = grow();
     System.arraycopy(elementData, index,
                      elementData, index + 1,
                      s - index);
     elementData[index] = element;
     size = s + 1;
 }
 /**
  * 获取元素,跟操作数组一样,比较简单,没啥好说的
  */
  public E get(int index) {
      Objects.checkIndex(index, size);
      return elementData(index);
  }
  /**
   * 替换列表中指定位置的元素。先记录下数组中index处的旧元素,覆盖完成后返回旧元素
   */
  public E set(int index, E element) {
      Objects.checkIndex(index, size);
      E oldValue = elementData(index);
      elementData[index] = element;
      return oldValue;
  }
  /**
   * 移除列表中指定位置的元素,并返回被移除的元素
   */
  public E remove(int index) {
      Objects.checkIndex(index, size);
      final Object[] es = elementData;

      @SuppressWarnings("unchecked") E oldValue = (E) es[index];
      fastRemove(es, index);

      return oldValue;
  }
  /**
   * 如果移除的是列表末尾的那个元素,就直接用null填补此位置。
   * 移除其他位置的元素都需要通过数组拷贝来实现元素位置移动
   */
  private void fastRemove(Object[] es, int i) {
      modCount++;
      final int newSize;
      if ((newSize = size - 1) > i)
          System.arraycopy(es, i + 1, es, i, newSize - i);
      es[size = newSize] = null;
  }
  /**
   * 通过遍历数组来找到需要移除的元素,匹配到第一个元素后就跳出循环。
   * 也就是说只会移除匹配到的第一个元素
   */
  public boolean remove(Object o) {
     final Object[] es = elementData;
     final int size = this.size;
     int i = 0;
     found: {
         if (o == null) {
             for (; i < size; i++)
                 if (es[i] == null)
                     break found;
         } else {
             for (; i < size; i++)
                 if (o.equals(es[i]))
                     break found;
         }
         return false;
     }
     fastRemove(es, i);
     return true;
  }

总结

源码就分析到这里了,当然还有好多没有涉及到的地方,有时间再慢慢更。通过阅读源码,对ArrayList又有了一些新的认识。主要有如下几点:

  1. 通过无参构造器创建的ArrayList对象,其内部用来存储元素的Object[]数组在没有调用add(或者ensureCapacity,不过需要满足特定条件)等方法前其实并没有真正的初始化;
  2. 扩容时,至少会增加当前容量一半的容量。

本文地址:https://blog.csdn.net/qq_35939417/article/details/112746911