浅析 java ArrayList

简介

容器是java提供的一些列的数据结构,也可以叫语法糖。容器就是用来装在其他类型数据的数据结构。

ArrayList是数组列表所以他继承了数组的优缺点。同时他也是泛型容器可以自定义各种数据解构、对象容纳在其中。

结构浅析

  • 父类

    AbstractList

  • 接口

    List

    Collection

    RandomAccess

    Cloneable

    Serializable

基本用法

创建对象:

ArrayList<Integer> arrList = new ArrayList<Integer>();

表示创建一个支持整数类型的List集合。

添加元素

arrList.add(10);
arrList.add(5, 10);

第一个表示在arrList的尾部添加元素10;
第二个表示在arrList的index = 5的地方添加元素为10,并且会把5之后的数据全部右移

读取元素

arrList.get(int)

获取指定位置的值

删除元素

arrList.remove(int index)
arrList.remove(Object object)

第一种是删除指定位置的元素,删除后该位置之后的元素全部左移动
第二种是按指定对象删除,同样的删除后再其末尾的数据要左移动

如果ArrayList 的类型为Integer, 同时又想按对象删除元素是需要把其转换成对象: arrList.remove((Integer)10).

源码分析

构造函数

ArrayList.class:

 public ArrayList(int paramInt)
 {
   if (paramInt < 0) {
     throw new IllegalArgumentException("Illegal Capacity: " + paramInt);
   }
   elementData = new Object[paramInt];
 }
 
 public ArrayList()
 {
   this(10);
 }
 
  public ArrayList(Collection<? extends E> paramCollection)
 {
   elementData = paramCollection.toArray();
   size = elementData.length;
   if (elementData.getClass() != Object[].class) {
     elementData = Arrays.copyOf(elementData, size, Object[].class);
   }
 }
 

如上可以看出来,ArrayList是一个Object对象数组结构的,在没有被指定数组长度的情况下默认是为10。Object是所有对象的父类,所以ArrayList支持所有的对象类型。

添加元素

ArrayList.class


  public boolean add(E paramE)
  {
    ensureCapacityInternal(size + 1); // 确保原本的数组容量还能不能容纳新元素, 会被直接按当前大小的1/2 扩大。
    elementData[(size++)] = paramE; // 在尾部添加元素,同时让size + 1
    return true;
  }
  
  public void add(int paramInt, E paramE)
  {
    rangeCheckForAdd(paramInt); // 判断paramInt会不会越界,必须要小于list的长度才能被插入,否则会报异常:IndexOutOfBoundsException(outOfBoundsMsg(paramInt))
    ensureCapacityInternal(size + 1); // 确保长度是足够的。
    System.arraycopy(elementData, paramInt, elementData, paramInt + 1, size - paramInt);
    elementData[paramInt] = paramE;
    size += 1;
  }
  
  
  private void ensureCapacityInternal(int paramInt)
  {
    modCount += 1;
    // 新长度未超过最大list的size
    if (paramInt - elementData.length > 0) {
      grow(paramInt); // 对旧的list扩大size
    }
  }
  
  private void grow(int paramInt)
  {
    int i = elementData.length;
    int j = i + (i >> 1); // 等价于 (int)i+i/2, 用位运算效率更高。把长度扩充到原来的1.5倍
    if (j - paramInt < 0) { // 如果扩充的元素还是小于新的list的长度,则直接去新的list的size进程扩充
      j = paramInt;
    }
    if (j - 2147483639 > 0) { // (其实这里插入有可能失败了,因为如果当j = Integer.MAX_VALUE时是没有真正的扩充原本的list)
      j = hugeCapacity(paramInt);
    }
    // 跟系统重新分配一块数组空间,并完成拷贝工作
    elementData = Arrays.copyOf(elementData, j);
  }
  
  private static int hugeCapacity(int paramInt)
  {
    if (paramInt < 0) {
      throw new OutOfMemoryError();
    }
    // Integer.MAX_VALUE = 2147483647, 2^31 -1
    return paramInt > 2147483639 ? Integer.MAX_VALUE : 2147483639;
  }
  

Array.class

 public static <T> T[] copyOf(T[] paramArrayOfT, int paramInt)
  {
    return (Object[])copyOf(paramArrayOfT, paramInt, paramArrayOfT.getClass());
  }
  
  public static <T, U> T[] copyOf(U[] paramArrayOfU, int paramInt, Class<? extends T[]> paramClass)
  {
    Object[] arrayOfObject = paramClass == Object[].class ? (Object[])new Object[paramInt] : // 重新分配数组 (Object[])Array.newInstance(paramClass.getComponentType(), paramInt);
    System.arraycopy(paramArrayOfU, 0, arrayOfObject, 0, Math.min(paramArrayOfU.length, paramInt)); // 拷贝数据
    return arrayOfObject; // 新的数组地址
  }

从这块的代码分析过来,可以发现有如下两种行为:

  1. 频繁内存申请和数据拷贝:

    如果list频繁的插入数据会让这个list不断的重新分配数组空间,并重新拷贝数据。这个时候就考虑给一个不错的ArrayList 数组默认长度或者考虑其他更好的更适合场景的数据容器。

  2. 线程安全、并发问题

    观察add(Index, element) 发现ArrayList是非线程安全的,比如线程1 缸取Index的元素,然后做修改因为获取到的是元素的应用,所以修改Index也就修改了ArrayList的值了。在这个期间如果有线程2去把Index的元素给替换了,那么线程1的元素操作就被覆盖了。

读取元素

ArrayList.class

  public E get(int paramInt)
  {
    rangeCheck(paramInt); // 检查位置的合法性
    return (E)elementData(paramInt); // 取出对应值
  }
  
  private void rangeCheck(int paramInt)
  {
    if (paramInt >= size) {
      throw new IndexOutOfBoundsException(outOfBoundsMsg(paramInt));
    }
  }
  // 从这里也可以看出来,就是按数组的形式来操作的
  E elementData(int paramInt)
  {
    return (E)elementData[paramInt];
  }
  

这块代码浅显易懂就不做论述

删除元素

ArrayList.class

 // 根据指定位置删除元素
  public E remove(int paramInt)
  {
    rangeCheck(paramInt); // 检查删除的元素的位置释放是合法的
    modCount += 1;
    Object localObject = elementData(paramInt); // 取出对应位置的元素
    int i = size - paramInt - 1;
    if (i > 0) {
      System.arraycopy(elementData, paramInt + 1, elementData, paramInt, i);
    }
    elementData[(--size)] = null; // 对应位置设置为null值
    return (E)localObject;
  }
  // 根据对象主动删除元素
  public boolean remove(Object paramObject)
  {
    int i;
    if (paramObject == null) {
      for (i = 0; i < size; i++) {
        if (elementData[i] == null)
        {
          fastRemove(i);
          return true;
        }
      }
    } else {
      for (i = 0; i < size; i++) {
        if (paramObject.equals(elementData[i]))
        {
          fastRemove(i);
          return true;
        }
      }
    }
    return false;
  }
  // 具体的删除操作
  private void fastRemove(int paramInt)
  {
    modCount += 1;
    int i = size - paramInt - 1;
    if (i > 0) {
      System.arraycopy(elementData, paramInt + 1, elementData, paramInt, i);
    }
    elementData[(--size)] = null;
  }
  // 检查元素合法性
  private void rangeCheck(int paramInt)
  {
    if (paramInt >= size) {
      throw new IndexOutOfBoundsException(outOfBoundsMsg(paramInt));
    }
  }

从代码中可以得出如下结论:

  1. null值

    ArrayList中允许直接存储null值的

  2. 线程安全、并发问题

    因为他是直接对ArrayList对应位置置为null,如果有多个线程访问可能存在数据安全性问题。(跟上述说的问题是一样的)

  3. 内存问题

    上面说到他的内存随着数据不断插入会不断的去申请内存块,但是从这里的这块代码中可以发现,如果内存已经被分配的情况下是不会随着元素的递减(删除)而收缩内存的。

  4. 按对象删除元素

    如果是按对象从ArrayList中删除元素,会从开始位置找到第一个跟删除对象匹配的值并删除,如果ArrayList中存在多个相同的对象时需要考虑清楚是否是删除你想删除的对象哦。

优缺点

优点:读取速度快
缺点:插入慢,非线程安全

总结

在使用ArrayList的时候,脑海里必须清晰好自己的场景是否会涉及到并发问题。其次要清晰的了解到数组的优缺点。因为它就是数组的实现