浅析JDK8-ArrayList

基于jdk1.8源码

UML类关系图

​ 我们先来看一下ArrayList的类关系图。

ArrayList.png

​ 从图中可以看出,ArrayList继承了AbstractList抽象类,实现了4个接口。

1.实现了List接口:
2.实现了RandomAccess接口:
3.实现了Cloneable接口:
4.实现了Serializable接口:

构造函数

<u>ArrayList的构造方法就做一件事情,就是初始化一下储存数据的容器,其实本质上就是一个数组,在其中就叫elementData。</u>

1.ArrayList()

构造一个初始容量为 10 的空列表。如果我们创建ArrayList对象的时候不传入参数,则使用此无参构造方法创建ArrayList对象。

public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

初始化一个空的list,在add第一个元素的时候,设置初始容量为10

 private static int calculateCapacity(Object[] elementData, int minCapacity) {
          if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            //data一开始是空,则返回10的初始容量
              return Math.max(DEFAULT_CAPACITY, minCapacity);
          }
          return minCapacity;
      }

2.ArrayLIst(int)

构造一个指定容量为capacity的空ArrayList。这是一个带初始容量大小的有参构造函数。

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);
    }
}
  • 初始容量大于0,实例化数组,将自定义的容量大小当成初始化elementData的大小
  • 初始容量等于0,将空数组赋给elementData
  • 初始容量小于0,抛异常

3.ArrayList(Collection<? extends E>)

构造一个包含指定 collection 的元素的列表,这些元素是按照该 collection 的迭代器返回它们的顺序排列的。第二个有参构造方法构造时赋的值是它的父类Collection对象。

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)
          //每个集合的toarray()的实现方法不一样,所以需要判断一下,如果不是Object[].class类型,那么就需要使用ArrayList中的方法去改造一下。
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // replace with empty array.
        this.elementData = EMPTY_ELEMENTDATA;
    }
}
  • 将集合对象转成数据,地址赋值给elementData
  • 如果数组的实际大小等于0(c中没有元素),将空数组EMPTY_ELEMENTDATA赋值给elementData
  • 如果size的值大于0,则执行Arrays.copy方法,把collection对象的内容(可以理解为深拷贝)copy到elementData中()

重要方法

1.get方法

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

    return elementData(index);
}

private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

@SuppressWarnings("unchecked")
E elementData(int index) {
  return (E) elementData[index];
}

可以看到由于Arraylist底层就是个obj的数组,所以获取元素是比较简单的。直接先对index进行范围判断,然后返回数组下标对应的值。

2.set方法

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

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

set方法是替换数组某个位置的值。先检查下标是否越界,然后取出旧值,把新值替换进去,最后返回旧值给用户。

3.add方法

(1)一个参的方法

在list末尾加上一个元素


public boolean add(E e) {
  //确保内部容量,传的是size+1的最小容量值,避免浪费
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

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

private static int calculateCapacity(Object[] elementData, int minCapacity) {
  //实际数组大小如果为空的话,初始化max(10,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;
  //新容量=老容量+老容量/2
  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);
}

private static int hugeCapacity(int minCapacity) {
  if (minCapacity < 0) // overflow
    throw new OutOfMemoryError();
  return (minCapacity > MAX_ARRAY_SIZE) ?
    Integer.MAX_VALUE :
    MAX_ARRAY_SIZE;
}

(2)两个参的方法

在list指定的位置插入一个元素

public void add(int index, E element) {
  //越界检查
    rangeCheckForAdd(index);
//确保内部容量,不够会调用grow进行扩容
    ensureCapacityInternal(size + 1);  // Increments modCount!!
  //复制数组,arraycopy(原数组,源数组中的起始位置,目标数组,目标数据中的起始位置,要复制的数组元素的数量)
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    elementData[index] = element;
    size++;
}

4.remove方法

(1)删除指定位置的方法

删除指定位置元素,返回该元素

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);
  //将索引指向null,让GC回收多余末尾的旧值
    elementData[--size] = null; // clear to let GC do its work

    return oldValue;
}

(2)删除第一个匹配到的元素

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
    }

(3)删除一段区间的元素

fromIndex<=删除区间<toIndex

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

对比jdk11的写法

protected void removeRange(int fromIndex, int toIndex) {
  if (fromIndex > toIndex) {
    throw new IndexOutOfBoundsException(
      outOfBoundsMsg(fromIndex, toIndex));
  }
  modCount++;
  shiftTailOverGap(elementData, fromIndex, toIndex);
}

private void shiftTailOverGap(Object[] es, int lo, int hi) {
  System.arraycopy(es, hi, es, lo, size - hi);
  for (int to = size, i = (size -= hi - lo); i < to; i++)
    es[i] = null;
}

(4)removeAll和retainAll

//删除源数组中包含目标数组的元素
public boolean removeAll(Collection<?> c) {
    Objects.requireNonNull(c);
    return batchRemove(c, false);//false表示不保留相同的元素,即删除
}
//交集,保留源数组中包含目标数组的元素
public boolean retainAll(Collection<?> c) {
  Objects.requireNonNull(c);
  return batchRemove(c, true);//true表示保留相同的元素
}

private boolean batchRemove(Collection<?> c, boolean complement) {
  final Object[] elementData = this.elementData; //拿原集合出来
  int r = 0, w = 0;
  boolean modified = false; //有没有改变标志
  try {
    for (; r < size; r++)//循环每一个原集合的元素
      if (c.contains(elementData[r]) == complement)//判断每一个源元素是否存在于对比的list中。complement为true则把元素保存下来(retainAll);为false则不保留(removeAll)
        elementData[w++] = elementData[r];
  } finally {
    // Preserve behavioral compatibility with AbstractCollection,
    // even if c.contains() throws.
    if (r != size) {//如果contains过程出错,则把剩下的源数据的元素复制到目标数组里面去
      System.arraycopy(elementData, r,//源数组,源起始位置
                       elementData, w,//目标数组,目标起始位置
                       size - r);//复制的长度
      w += size - r;//更新目标数组的最新长度
    }
    if (w != size) {//目标数组剩下的元素为源数组的元素,可以==null,让gc回收掉
      // clear to let GC do its work
      for (int i = w; i < size; i++)
        elementData[i] = null;
      modCount += size - w;
      size = w;
      modified = true;//数组的大小有变化则返回true;
    }
  }
  return modified;
}

(5)indexOf和lastIndexOf

//返回源数组中第一个命中的下标,如没有则返回-1。
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;
}

(6)clear()

public void clear() {
    modCount++;

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

    size = 0;
}

问题

1.默认初始化大小

/**
 * Default initial capacity.
 */
private static final int DEFAULT_CAPACITY = 10;

2.怎么扩容

当判断需要的最小容量大于list中数组的长度时,则通过grow扩容,首先扩大为原数组的1.5倍,还不够就扩充为需求的容量大小。

3.最大容量

最大容量为MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

4.怎么找到下一个的

因为lsit的底层存储还是数组,所以直接通过下标定位就行了。

5.线程安全吗

不安全的,主要问题出现在获取size大小操作完后,size++这里不是原子性的。导致会出出现脏读的情况。举例:A和B两个线程同时调用add方法

  1. 初始列表size大小为0。此时两个线程同时拿到size为0。
  2. 线程A执行插入操作,将elementData[0]=A
  3. 此时线程B也执行插入操作,将elementData[0]=B
  4. 线程A将size++
  5. 线程B将size++

此时我们会发现ArrayList的size变成了2,[0]=B;[1]=null。这就出现线程不安全的问题了

6.和hashmap的区别

额外知识点

1.transient关键字

/**
 * The array buffer into which the elements of the ArrayList are stored.
 * The capacity of the ArrayList is the length of this array buffer. Any
 * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
 * will be expanded to DEFAULT_CAPACITY when the first element is added.
 */
transient Object[] elementData; // non-private to simplify nested class acces Object[] elementData; // non-private to simplify nested class access

transient的字面意思是短暂的,在java中被此关键字修饰的属性值将不会被序列化。

具体可查看:https://baijiahao.baidu.com/s?id=1636557218432721275&wfr=spider&for=pc

2.MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

 The maximum size of array to allocate.
 Some VMs reserve some header words in an array.
 Attempts to allocate larger arrays may result in
 OutOfMemoryError: Requested array size exceeds VM limit

有一些虚拟机在数组中保留了一些头信息,为了避免内存溢出而已。

3.modCount的作用

在 AbstractList 中,有一个全局变量 madCount,记录了结构性改变的次数。结构性改变指的是那些修改了列表大小的操作,在迭代过程中可能会造成错误的结果。

madCount 交由迭代器(Iterator)和列表迭代器(ListIterator)使用,当进行 next()、remove()、previous()、set()、add() 等操作时,如果 modCount 的值意外改变,那么迭代器或者列表迭代器就会抛出 ConcurrentModificationException 异常。

如果希望提供快速失败(fail-fast)机制,那么其子类就应该在 add(int, E)、remove(int) 等结构性改变的方法中将 madCount 变量自增,否则可以忽略该变量。

参考博文

  1. https://blog.csdn.net/u010250240/article/details/89762912
  2. https://baijiahao.baidu.com/s?id=1637926321175819771&wfr=spider&for=pc
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容