ArrayList源码阅读

前言

ArrayList是十分常用的集合类,很好用而且也不难用。这篇文章主要是对ArrayList的源码进行阅读,了解它常用方法的设计,做到不仅要会用,还要懂得为何要这样用。虽然ArrayList每一个Java程序员都会使用,但了解一下它的源码实现也是非常有必要的!其实主要还是因为面试要用到

ArrayList是什么

ArrayList是一个数组,底层使用Object[] elementData进行存储数据。普通的数组长度是固定的,而ArrayList数组的长度可以动态改变,因此使用起来更加方便。之所以可以改变,主要是在需要的时候对elementData进行修改。

与它类似的是LinkedList,二者的区别就像数组与链表的区别。ArrayList在查找和随机访问的情况下速度较快,而LinkedList在插入和删除的情况下速度较快。

特点:查询效率高,插入删除效率低,线程不安全。

Question:为什么线程不安全还要使用ArrayList?

Ans:因为我们正常使用的场景中,都是用来查询,不会涉及太频繁的增删。如果涉及频繁的增删,可以使用LinkedList。如果你需要线程安全就使用Vector,这就是三者的区别了,实际开发过程中还是ArrayList使用最多的。不存在一个集合工具是查询效率又高,增删效率也高的,还线程安全的,至于为啥大家看代码就知道了,因为数据结构的特性就是优劣共存的,想找个平衡点很难,牺牲了性能,那就安全,牺牲了安全那就快速。

ArrayList的方法

构造方法

一共有3个,其中包含1个空的构造方法,1个传入初始的容量,1个传入Collection对象作为初始化数据。

无参构造方法:给elementData赋值为空数据。

/**
  * Constructs an empty list with an initial capacity of ten.
  */
public ArrayList() {
  this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

传入初始容量的构造方法:如果int参数为0,相当于无参构造方法,如果为负数抛出异常。值得注意的是,ArrayList()和ArrayList(0)并不等价,elementData分别会赋值为DEFAULTCAPACITY_EMPTY_ELEMENTDATAEMPTY_ELEMENTDATA,后面扩容的时候会看到二者的区别。

/**
  * Constructs an empty list with the specified initial capacity.
  *
  * @param  initialCapacity  the initial capacity of the list
  * @throws IllegalArgumentException if the specified initial capacity
  * is negative
  */
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);
  }
}

传入Collection作为初始化数据:

/**
 * Constructs a list containing the elements of the specified
 * collection, in the order they are returned by the collection's
 * iterator.
 *
 * @param c the collection whose elements are to be placed into this list
 * @throws NullPointerException if the specified collection is null
 */
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;
  }
}

注意注释c.toArray might (incorrectly) not return Object[],c.toArray方法可能不会返回Object[]类型,这可能会导致类型错误。至于为什么Collection的toArray可能不会返回Object[]类型,这里先看ArrayList里的toArray方法:

public <T> T[] toArray(T[] a) {
  if (a.length < size)
    // Make a new array of a's runtime type, but my contents:
    return (T[]) Arrays.copyOf(elementData, size, a.getClass());
  System.arraycopy(elementData, 0, a, 0, size);
  if (a.length > size)
    a[size] = null;
  return a;
}

里面调用了Arrays.copyOf方法,最后返回的是elementData的类型。在前面我们已经知道,elementData的类型是Object[]类型,因此最后返回的就是Object[]类型。那么什么情况下不会返回Object[]类型?我们还记得Arrays类里有一个asList方法,它会返回一个Arrays的内部类对象,也是叫ArrayList,(Arrays.ArrayList),而这个同名的ArrayList也继承了AbstractList,因此也属于Collection,但它的toArray方法如下:

public static <T> List<T> asList(T... a) {		// Arrays的asList方法
  return new ArrayList<>(a);
}

// Arrays的内部类
private static class ArrayList<E> extends AbstractList<E>		
  implements RandomAccess, java.io.Serializable		
{
  private final E[] a;
  // ...

  @Override
  @SuppressWarnings("unchecked")
  public <T> T[] toArray(T[] a) {
    int size = size();
    if (a.length < size)
      return Arrays.copyOf(this.a, size,
                           (Class<? extends T[]>) a.getClass());
    System.arraycopy(this.a, 0, a, 0, size);
    if (a.length > size)
      a[size] = null;
    return a;
  }
  // ...
}

关键是看toArray方法,此处依然是调用Arrays.copfOf方法,但它传入的参数a,a的类型是E[],而不是Object[]。这就导致了Arrays.asList理应可以作为ArrayList构造方法的参数,但却会出现类型错误的情况,也就是如下例子:

public static void test1()
{
  List<String> list = Arrays.asList("abc");
  System.out.println(list.getClass());        // class java.util.Arrays$ArrayList
  Object[] objArray = list.toArray();
  System.out.println(objArray.getClass());    // class [Ljava.lang.String;
  objArray[0] = new Object();                 // cause ArrayStoreException
}

public static void test2()
{
  List<String> dataList = new ArrayList<>(Arrays.asList("abc"));		// good
  System.out.println(dataList.getClass());    // class java.util.ArrayList
  Object[] listToArray = dataList.toArray();
  // class [Ljava.lang.Object;返回的是Object数组
  System.out.println(listToArray.getClass()); // class [Ljava.lang.Object;
  listToArray[0] = "";
  listToArray[0] = 123;
  listToArray[0] = new Object();      // all are ok
}

test2是我们的常用用法,把Arrays.asList作为构造参数传给ArrayList,此时可以传入Object类型对象。但如果直接像test1那样做,会因为Arrays.toArray方法返回的类型是实际类型而不是Object类型,导致出错。在JDK修改此错误之前,test2会报test1的错误,因为类型不一致。但这个bug已经是很久之前的了,早就修复了,此处只是理解这一句注释的由来。

Question:为什么底层实现是elementData,但是数组的长度却可以动态改变?

Ans:ArrayList可以通过构造方法在初始化的时候指定底层数组的大小,而当元素的数量超出数组大小时,底层会修改elementData,实现扩容。数组默认的DEFAULT_CAPACITY是10,每一次扩容会增长为原来的1.5倍。

Question:调用构造方法ArrayList(int initialCapacity)会不会初始化数组大小?

Ans:不会。在构造方法里可以看到,它只是声明里缓冲数组elementData的长度,但实际上此时数组仍然是空的。所以如果我们进行get,add(int, E)等方法,都会报错。调用构造方法之后,可以看到size依然为0。

List<Integer> list = new ArrayList<>(100);
System.out.println(list.size());    // 0
list.add(1, 1);     // throws exception

add方法

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

public void add(int index, E element) {
  rangeCheckForAdd(index);

  ensureCapacityInternal(size + 1);  // Increments modCount!!
  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));
}

public boolean addAll(Collection<? extends E> c) {
  boolean modified = false;
  for (E e : c)
    if (add(e))
      modified = true;
  return modified;
}

对于第一个add方法,首先调用ensureCapacityInternal,确保数组可以容纳size + 1的数据。如果无法容纳,则对数组进行扩容。接着就是对elementData数组进行简单的赋值操作。而且也看得出来,add方法如果不指定index参数,那么就是在列表的末尾进行添加元素。

而第二个add方法,多了一个rangeCheckForAdd方法,确保index没有越界。add的过程是先把原数组index开始到结尾的数据移动到从index + 1开始的位置,这时候index位置就空出来,再执行elementData[index] = element就完成了元素的插入,最后再修改size。移动数组的算法使用的是System.arraycopy方法。如图演示:

①要在index 5的位置上插入元素

image-20200910091505731

②把从index 5开始的元素移动到index 5 + 1之后的位置(实际上这时候index 5依然是5,下图应该是1,2,3,4,5,5,6,7,8,9)

image-20200910091547457

③在index 5的位置上修改为要插入的元素

image-20200910091605563

显然,因为需要移动数组,复制数组,因此add操作的效率比较低。但是如果要往越靠近数组末尾的位置添加元素,这时候复制移动数组的大小就越小。所以如果是在数组的末尾进行add,那么效率还是比较高的。

而第三个addAll方法,就是通过for-each,也就是迭代器,逐个添加到列表的末尾,返回true/false。

remove方法

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; // clear to let GC do its work

  return oldValue;
}

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;
}

private void fastRemove(int index) {
  modCount++;
  int numMoved = size - index - 1;
  if (numMoved > 0)
    System.arraycopy(elementData, index+1, elementData, index,
                     numMoved);
  elementData[--size] = null; // clear to let GC do its work
}

remove有两个重载方法,一个的参数是int,表示传入index,删除指定index位置的元素,并且会返回被删除元素的值。另一个的参数是Object,表示要删除数组里的指定对象。

对于第一个传入int参数的remove方法,首先依然是判断index是否越界,如果没有,再计算numMoved,表示需要移动的元素个数。如果numMoved为0,说明删除的是末尾的元素,此时就无需复制和移动数组,直接把最后一个元素置为null即可。但如果大于0,就执行System.arraycopy(elementData, index+1, elementData, index, numMoved);,把从index + 1开始的元素复制到index开始的位置,因此原本index位置的元素就被覆盖掉了。无论是否进行arraycopy,最后都要把最后一个元素置为null,并且修改size的值:elementData[--size] = null;

最后一步是否可以换成size--?实际上是可行的,因为ArrayList里的操作都要受到size的限制,哪怕是再次调用remove,arraycopy即使多复制一个null值也不会出错,因为只要确保前面size个元素正确即可,边界判断方法确保不会访问到size之外的元素。所以这里要主动设置为null,果然就是跟注释里说的一样:提示GC这一个元素可以被回收了。

而第二个传入Object参数的remove方法,它会先判断Object是否为null,分别考虑参数是null和非null的两种情况,避免equals出错。如果找到了匹配的Object值,调用的是fastRemove方法,而这个fastRemove方法和remove方法区别不大,只是无需考虑index越界的问题,所以还省去了一个rangeCheck方法的调用,并且无需返回oldValue,仅此而已。

如图演示:

①删除index 5位置的元素

image-20200910093619863

②复制index 5 + 1之后的元素到index 5(此时index 9的位置实际上还是9)

image-20200910093714486

③把最后一个位置的元素设置为null,help GC。

可以看到remove也需要调用System.arraycopy,需要进行复制和移动数组,因此效率也不高。但如果remove的位置靠近结尾,那么效率尚可。

clear方法

public void clear() {
  modCount++;

  // clear to let GC do its work
  for (int i = 0; i < size; i++)
    elementData[i] = null;

  size = 0;
}

没什么特别,直接全部设置为null,size设置为0,唯一值得记录的估计就是modCount只会+1,与元素的数量无关~

removeRange方法

    protected void removeRange(int fromIndex, int toIndex) {
        modCount++;
        int numMoved = size - toIndex;
        System.arraycopy(elementData, toIndex, elementData, fromIndex,
                         numMoved);

        // clear to let GC do its work
        int newSize = size - (toIndex-fromIndex);
        for (int i = newSize; i < size; i++) {
            elementData[i] = null;
        }
        size = newSize;
    }

其实逻辑和remove是差不多的,但当时在源码里不小心点到了AbstractList,那里的removeRange方法看起来就比较奇怪:

protected void removeRange(int fromIndex, int toIndex) {
  ListIterator<E> it = listIterator(fromIndex);
  for (int i=0, n=toIndex-fromIndex; i<n; i++) {
    it.next();
    it.remove();
  }
}

里面使用了ListIterator,自定义了一个cursor变量,所以并不是想象中的移动多次数组。此处略。

clone方法

public Object clone() {
  try {
    ArrayList<?> v = (ArrayList<?>) super.clone();
    v.elementData = Arrays.copyOf(elementData, size);
    v.modCount = 0;
    return v;
  } catch (CloneNotSupportedException e) {
    // this shouldn't happen, since we are Cloneable
    throw new InternalError(e);
  }
}

先调用Object的clone方法,返回一个Object对象,再强制类型转换成ArrayList类型,接着通过Arrays.copyOf对elementData进行赋值,同时还把modCount设置为0,返回即可。

indexOf方法

public boolean contains(Object o) {
  return indexOf(o) >= 0;
}

public int indexOf(Object o) {
  if (o == null) {
    for (int i = 0; i < size; i++)
      if (elementData[i]==null)
        return i;
  } else {
    for (int i = 0; i < size; i++)
      if (o.equals(elementData[i]))
        return i;
  }
  return -1;
}

public int lastIndexOf(Object o) {
  if (o == null) {
    for (int i = size-1; i >= 0; i--)
      if (elementData[i]==null)
        return i;
  } else {
    for (int i = size-1; i >= 0; i--)
      if (o.equals(elementData[i]))
        return i;
  }
  return -1;
}

contains方法检测ArrayList是否包含某个Object,通过indexOf方法,根据返回值是否为-1判断。而indexOf返回的是指定Object对象在ArrayList中第一次出现的位置,lastIndexOf则是最后一次出现的位置。二者都是先判断一下Object参数为null的情况,对于null参数使用“==”运算符,否则用equals方法。

get和set方法

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

  return elementData(index);
}

E elementData(int index) {
  return (E) elementData[index];
}

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

  E oldValue = elementData(index);
  elementData[index] = element;
  return oldValue;
}

很简单,没什么好分析的。

ensureCapacityInternal和grow(扩容相关)

回到add方法,在执行add操作之前,会首先调用ensureCapacityInternal方法判断是否要扩容:

private void ensureCapacityInternal(int minCapacity) {
  ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

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

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

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

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);
}

calculateCapacity方法的意义是,如果elementData数组为DEFAULTCAPACITY_EMPTY_ELEMENTDATA,也就是创建ArrayList时调用的是无参构造方法,后面第一次add会进行扩容。此时不是从0开始一步一步地变成原本的1.5倍,而是直接创建为DEFAULT_CAPACITY(10)的大小。

这时候就可以解决构造方法时留下的疑问了,DEFAULTCAPACITY_EMPTY_ELEMENTDATAEMPTY_ELEMENTDATA的区别?但调用ArrayList()时,第一次add进行扩容会直接把大小设置为10,而调用ArrayList(0),第一次add会慢慢地扩容,从1开始。可以用反射来测试:

public static void main(String[] args) {
  printDefaultCapacityList();
  printEmptyCapacityList();
}

public static void printDefaultCapacityList() {
  ArrayList defaultCapacity = new ArrayList();
  System.out.println(
    "default 初始化长度:" + getCapacity(defaultCapacity));

  defaultCapacity.add(1);
  System.out.println(
    "default add 之后 长度:" + getCapacity(defaultCapacity));
}

public static void printEmptyCapacityList() {
  ArrayList emptyCapacity = new ArrayList(0);
  System.out.println(
    "empty 初始化长度:" + getCapacity(emptyCapacity));

  emptyCapacity.add(1);
  System.out.println(
    "empty add 之后 长度:" + getCapacity(emptyCapacity));
}

public static int getCapacity(ArrayList<?> arrayList) {
  Class<ArrayList> arrayListClass = ArrayList.class;
  try {
    // 获取 elementData 字段
    Field field = arrayListClass.getDeclaredField("elementData");
    // 开启访问权限
    field.setAccessible(true);
    // 把示例传入get,获取实例字段elementData的值
    Object[] objects = (Object[]) field.get(arrayList);
    //返回当前ArrayList实例的容量值
    return objects.length;
  } catch (Exception e) {
    e.printStackTrace();
    return -1;
  }
}

运行结果:

default 初始化长度:0
default add 之后 长度:10
empty 初始化长度:0
empty add 之后 长度:1

那么讨论完DEFAULTCAPACITY_EMPTY_ELEMENTDATA的情况后,如果elementData的长度确实不足以存储minCapacity数量的元素,此时会调用grow方法进行扩容。通过int newCapacity = oldCapacity + (oldCapacity >> 1);使得容量会是旧容量的1.5倍。如果1.5倍之后仍然没有达到minCapacity,那么直接将新容量设置为minCapacity:newCapacity = minCapacity;实际上这一行代码只有在EMPTY_ELEMENTDATA的时候才会执行。过去总以为如果一次性往ArrayList里添加多个元素(比如addAll)会只执行1次扩容,但看了addAll方法就会发现,addAll只是通过迭代器,按顺序地对每一个元素执行add方法。此处也可以用反射进行测试,略。

序列化和反序列化

序列化writeObject和反序列化readObject:

transient Object[] elementData; // non-private to simplify nested class access

// ...

/**
 * 将ArrayLisy实例的状态保存到一个流里面
 */
private void writeObject(java.io.ObjectOutputStream s)
  throws java.io.IOException{
  // Write out element count, and any hidden stuff
  int expectedModCount = modCount;
  s.defaultWriteObject();

  // Write out size as capacity for behavioural compatibility with clone()
  s.writeInt(size);

  // 按照顺序写入所有的元素
  for (int i=0; i<size; i++) {
    s.writeObject(elementData[i]);
  }

  if (modCount != expectedModCount) {
    throw new ConcurrentModificationException();
  }
}

/**
 * 根据一个流(参数)重新生成一个ArrayList
 */
private void readObject(java.io.ObjectInputStream s)
  throws java.io.IOException, ClassNotFoundException {
  elementData = EMPTY_ELEMENTDATA;

  // Read in size, and any hidden stuff
  s.defaultReadObject();

  // Read in capacity
  s.readInt();

  if (size > 0) {
    // be like clone(), allocate array based upon size not capacity
    ensureCapacityInternal(size);

    Object[] a = elementData;
    // Read in all elements in the proper order.
    for (int i=0; i<size; i++) {
      a[i] = s.readObject();
    }
  }
}

ArrayList里的elementData对象声明为transient,原因是JDK不想让整个elementData都序列化或反序列化,而是只将size和实际存储的元素进行序列化或反序列化,从而节省空间和时间。

迭代器

ArrayList因为实现了Collection,自然也实现了Iterable接口,因此可以调用iterator方法生存一个迭代器:

public Iterator<E> iterator() {
  return new Itr();
}

/**
 * An optimized version of AbstractList.Itr
 */
private class Itr implements Iterator<E> {
  int cursor;       // index of next element to return
  int lastRet = -1; // index of last element returned; -1 if no such
  int expectedModCount = modCount;

  Itr() {}

  public boolean hasNext() {
    return cursor != size;
  }

  @SuppressWarnings("unchecked")
  public E next() {
    checkForComodification();
    int i = cursor;
    if (i >= size)
      throw new NoSuchElementException();
    Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length)
      throw new ConcurrentModificationException();
    cursor = i + 1;
    return (E) elementData[lastRet = i];
  }

  public void remove() {
    if (lastRet < 0)
      throw new IllegalStateException();
    checkForComodification();

    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) {
    Objects.requireNonNull(consumer);
    final int size = ArrayList.this.size;
    int i = cursor;
    if (i >= size) {
      return;
    }
    final Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length) {
      throw new ConcurrentModificationException();
    }
    while (i != size && modCount == expectedModCount) {
      consumer.accept((E) elementData[i++]);
    }
    // update once at end of iteration to reduce heap write traffic
    cursor = i;
    lastRet = i - 1;
    checkForComodification();
  }

  final void checkForComodification() {
    if (modCount != expectedModCount)
      throw new ConcurrentModificationException();
  }
}

代码有点长,先看三个变量:

cursor:记录迭代器当前指向的位置,游标。

lastRet:记录上一次返回的元素位置。

exceptedModCount:期望的modCount值

对于next方法,主要的逻辑是先判断各种异常情况,然后把cursor指向下一个位置,返回cursor先前指向的位置,所以实际上next方法只用看这两行:

// ...
cursor = i + 1;			// 游标指向i + 1,即下一个位置
return (E) elementData[lastRet = i];	// 返回i位置的元素,即游标先前指向的位置

而remove方法,此时会删除lastRet位置上的元素,然后把cursor指向lastRet,因为数据量减1,cursor也需要减1。并且会把lastRet设置为-1,所以不能连续两次进行remove方法。这是代码角度的限制,而实际含义则是迭代器就不应该有连续删除的功能,而是逐步遍历,找到合适的才会考虑删除。

而且next和remove方法的开头都会调用checkForComodification方法,判断modCount是否与exceptedModCount相等。因为ArrayList的迭代器是fast-fail的,因此在使用的过程中不允许对数据进行修改。此时如果修改了数据,那么modCount也会发生改变,导致modCount与exceptedModCount不相等,抛出ConcurrentModificationException。但有的时候modCount不一定会变化,这时候就不会抛出异常。所以这个异常只能用于测试,但不能因为没有抛出异常就断定一定没有在中途修改数据,关键是要清晰为什么不能修改。

通过上面的代码,我们会发现迭代器里的remove还会把exceptedModCount设置为modCount,因此这时候就不会抛出异常。所以在ArrayList上进行remove/add/set等操作会抛出异常,但在迭代器上调用remove并不会:

public static void main(String[] args) {
  ArrayList arrayList = new ArrayList();
  for (int i = 0; i < 10; i++) {
    arrayList.add(i);
  }
  remove(arrayList);
  System.out.println(arrayList);
}

public static void remove(ArrayList<Integer> list) {
  Iterator<Integer> iterator = list.iterator();	
  while (iterator.hasNext()) {
    Integer number = iterator.next();
    if (number % 2 == 0) {
      // 抛出ConcurrentModificationException异常
      list.remove(number);		// 如果换成iterator.remove()则不会抛出异常
    }
  }
}

最后关于迭代器这一块,还有一个forEachRemaining的方法,暂时没用到,先不管了。

其他

Question:ArrayList适用的场景?

Ans:ArrayList适用于需要频繁地随机查询,值的修改,在尾部添加删除元素的情况,或者元素的个数是确定的。所以ArrayList可以用于实现栈,只对最后一个元素进行操作(但LinkedList的方法名更直观)。ArrayList不适合用于实现队列,因为队列需要在头跟尾进行操作,这平均下来会需要进行大量的数组复制和移动操作,效率很低。

Question:ArrayList不适合实现队列,那数组是否适合实现队列?

Ans:实际上ArrayList跟数组基本是同一个东西,但有一种特殊的数组:环形数组。也就是数组的头尾是连接在一起的,根据两个index指向数组的头和尾(实际上一个是读的地点,一个是写的地点),并且确保不会超出范围。因此环形数组可以用于实现环形队列,比如ArrayBlockingQueue,它是一个定长的队列,底层使用一个定长的数组来实现。

Question:ArrayList和LinkedList的遍历性能差异?

Ans:论遍历ArrayList要比LinkedList快得多,ArrayList遍历最大的优势在于内存的连续性,CPU的内部缓存结构会缓存连续的内存片段,可以大幅降低读取内存的性能开销。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: ArrayListJava语言中的一种数据结构,它是基于动态数组实现的,可以根据需要自动扩容。 下面是ArrayList源码: ```java public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { private static final long serialVersionUID = 8683452581122892189L; private transient Object[] elementData; private int size; public ArrayList(int initialCapacity) { super(); if (initialCapacity < 0) throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); this.elementData = new Object[initialCapacity]; } public ArrayList() { this(10); } public boolean add(E e) { ensureCapacityInternal(size + 1); elementData[size++] = e; return true; } public E get(int index) { rangeCheck(index); return elementData(index); } public E set(int index, E element) { rangeCheck(index); E oldValue = elementData(index); elementData[index] = element; return oldValue; } public int size() { return size; } private void rangeCheck(int index) { if (index >= size) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); } private String outOfBoundsMsg(int index) { return "Index: "+index+", Size: "+size; } } ``` 其中,ArrayList的实现是通过一个Object类型的数组来存储数据,当数组空间不足时会自动进行扩容。同时,ArrayList实现了List和RandomAccess接口,因此可以像数组一样进行随机访问,并且可以使用List的各种方法来操作数据。除此之外,ArrayList还支持序列化和克隆。 ### 回答2: ArrayListJava中常见的集合类之一,它是基于数组的动态数组实现。其源码位于`java.util`包下。 在ArrayList源码中,我们可以看到它的核心组成部分是一个Object类型的数组elementData来存储元素。当我们向ArrayList中添加元素时,它会根据需要动态调整数组的大小,并将元素添加到数组的末尾。 除了存储元素的数组外,ArrayList还包含一个整型的size用于记录实际存储的元素个数。通过size,ArrayList可以方便地管理当前存储的元素数量。 ArrayList源码中还包含了一系列常用的方法,用于对集合进行增删改查等操作。例如,add方法用于向ArrayList末尾添加元素,remove方法用于删除指定位置的元素,get方法用于获取指定位置的元素,等等。 在ArrayList源码中,还有一些涉及到数组扩容和复制的实现细节,以保证ArrayList的存储和访问效率。当ArrayList需要扩容时,会调用Arrays.copyOf方法来创建一个新的更大的数组,并将原数组中的元素复制到新数组中。 总的来说,ArrayList源码主要涉及到动态数组的实现细节,以及一系列常用的方法。它的设计和实现使得我们能够方便地使用和管理多个元素,提高了集合的灵活性和效率。 ### 回答3: ArrayListJava中的一种动态数组,是List接口的可调整大小的数组实现。它可以根据需要自动增长和缩减,提供了更灵活的数据存储方式。 ArrayList源码主要包括以下几个关键的部分: 1. 实现了List接口:ArrayList类实现了List接口,包括了List接口中定义的常规方法,如添加、删除、查找、修改元素等操作。 2. 内部数组:ArrayList使用一个内部数组data[]来存储元素,这个数组是动态的,可以根据需要自动扩容和缩容。 3. 扩容机制:当需要添加元素时,如果当前数组已满,ArrayList会创建一个更大的新数组,并将原数组的内容复制到新数组中。这种机制保证了数组的容量始终能够满足需求,并避免了频繁的元素搬迁。 4. 索引、增删改查方法:ArrayList提供了一系列方法来对元素进行操作,比如get(index)获取指定位置的元素,add(element)在末尾添加元素,remove(index)删除指定位置的元素,set(index, element)修改指定位置的元素等。 5. 其他方法:ArrayList还提供了其他一些方法,如size()返回元素个数,isEmpty()判断是否为空,contains()判断是否包含某个元素等。 6. 实现了序列化接口:ArrayList实现了Serializable接口,可以进行序列化和反序列化,可以在不同应用之间传递。 总之,ArrayList源码实现了动态可调整大小的数组,提供了一系列方便的方法来操作元素,增加了灵活性和便捷性。通过扩容机制,可以充分利用内存,避免频繁地进行数组复制。ArrayListJava集合中常用的数据结构之一,适合于需要频繁读取和修改元素的场景。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值