资讯专栏INFORMATION COLUMN

深入了解Java集合中的ArrayList

zeyu / 2098人阅读

摘要:实现了接口,所以包含的基本方法新增,删除,插入等都实现了。线程安全问题在多线程情况下有线程进行修改时,是线程不安全的。线程安全性问题,取决于如何应用。反映的是当前数组中存了多少数据,而则反映的是内部数组的容量。

什么是ArrayList

ArrayList 是一个可扩容数组Resizable-array,它实现了List接口的所有方法。

从对ArrayList的简单描述中我们可以得出几点

ArrayList 是数组,但不同于一般数组,可扩容,而一般数组容量固定。

ArrayList 实现了List接口,所以List包含的基本方法(新增,删除,插入等)ArrayList都实现了。

ArrayList 需要知道的10点内容 1.ArrayList 内部是通过数组实现
ArrayList 内部实现是通过一个对象数组
transient Object[] elementData
2.ArrayList 添加操作(add) 的执行时间是固定的
ArrayList add() 方法实质上是实现了在数组的末尾添加一个元素 elementData[size++] = el 所以执行的时间复杂度是固定的O(1),添加n个元素为O(n)
3.除了 add 方法外其他操作如: 插入、删除 操作时间是线性的。
因为本质上是对数组进行操作,当在数组中插入、删除数据时,需要先对数组进行移位操作,插入时需要将插入点以后的数据全部后移,而删除操作则需要将删除节点后的数据全部前移,操作时间复杂度为O(n)
4.线程安全问题
ArrayList 在多线程情况下有线程进行修改时,是线程不安全的。线程安全性问题,取决于如何应用。List list = Collections.synchronizedList(new ArrayList(...))可以获取线程安全的ArrayList
5.关于扩容

ArrayList 有其自动扩容机制也可以在预知要处理数据大小时手动扩容

1.自动扩容

ArrayList 在新增元素时(add ,addAll操作)会先检测当前内部数组elementData[]的容量是否足够添加新元素,如果不足则扩容,ArrayList最大容量为Integer.MAX_VALUE,自动扩容调用的流程如下

public boolean add(E e) {
    // 自动扩容,并记录元素修改数量 ,元素修改数量主要是用于并发修改错误
    ensureCapacityInternal(size + 1); 
    elementData[size++] = e;
    return true;
}

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

// 返回一个数组大小的值 minCapacity 和默认的 DEFAULT_CAPACITY 中较大的那个
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    // 如果是空数组 通过new ArrayList()创建
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
      // 默认大小是DEFAULT_CAPACITY =  10
      return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}


实际扩充容量的片段

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // 将上面方法返回的大小和当前 elementData 大小进行比较,判断是否需要扩容
    if (minCapacity - elementData.length > 0)
      grow(minCapacity);
}
private void grow(int minCapacity) { // 具体扩容代码
    int oldCapacity = elementData.length;
    // 扩容后容量总是扩容前的大约1.5倍左右增量0.5
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
          newCapacity = minCapacity;
    // 如果扩容后超出了最大个数限制 则由hugeCapacity()来处理
    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.手动扩容或者指定初始容量

ArrayList 手动扩容主要是调用ensureCapacity(int minCapacity)方法,除此之外还能在创建ArrayList时指定初始容量

创建时直接指定容量new ArrayList<>(initialCapacity)及其实现

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

通过ensureCapacity(int minCapacity)方法在处理过程中扩容,这种情况一般发生在处理过程中发现初始容量不能满足当前数据需求,而又为了避免自动扩容时的资源浪费,因为每次自动扩容时都会进行数组复制。

public void ensureCapacity(int minCapacity) {
    int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) ? 0: DEFAULT_CAPACITY;
    if (minCapacity > minExpand) {
        ensureExplicitCapacity(minCapacity);
    }
}

3.最后看看扩容时的数组复制,数组复制是通过Arrays.copyOf(origin,newLenght)方法来实现的

public static  T[] copyOf(T[] original, int newLength) {
    return (T[]) copyOf(original, newLength, original.getClass());
}
// 具体复制代码
public static  T[] copyOf(U[] original, int newLength, Class newType) {
    @SuppressWarnings("unchecked")
    T[] copy = ((Object)newType == (Object)Object[].class)
        ? (T[]) new Object[newLength]
        : (T[]) Array.newInstance(newType.getComponentType(), newLength);
    // 将旧数组中的数据复制到新数组
    System.arraycopy(original, 0, copy, 0,
                     Math.min(original.length, newLength));
    return copy;
}
6.关于ArrayList在方法中传递的问题。
ArrayList在方法中传递时,传递的是对象的引用,当某一个方法修改了ArrayList时,该修改会反映到引用该ArrayList的所有地方,不管传递多少次,都引用的时同一个内存区域的数据,所以如果要实现在传递时确保内容不变,应该克隆(用ArrayList的clone())方法一份进行传递(需要注意深浅克隆问题深克隆可以使用Collections.copy(dest,src)方法),具体该怎么弄,还是要视需求而定。
7.ArrayList 的capacity和size的关系
如果要找他们之间的关系可能就是size=元素数量 <= capacity = 容量大小吧。size 反映的是当前数组中存了多少数据,而capacity则反映的是ArrayList内部数组的容量。不能习惯性认为capacity 既是 size
8.ArrayList 和 LinkedList的选择性问题
ArrayList和LinkedList为什么会存在选择性问题,因为他们在get、add、remove时性能是不一样的。

ArrayList是内部实现是数组,在随机存取时时间复杂度是O(1)知道索引就能立马取值,而其插入和删除操作就相对比较麻烦,需要进行移位操作线性的时间复杂度。

LinkedList 是双向连表Doubly-linked ,在插入和删除时时间复杂度都是O(1),但索引(取索引位上的值)时需要从表头或者表尾进行遍历操作。

所以选用哪一个,完全取决于你要进行的操作是以随机存取为主还是增删元素较多为主。

9.为什么elementData使用transient修饰
transient关键字的作用是阻止对象序列化,那么为什么要防止elementData序列化呢?那是因为elementData是一个数组,并且并不是数组中每一个位置上都保存有值,容量10000的数组中可能只保存了10个对象。所以不能对其进行序列化,在ArrayList中重写了writeObject 、readObject 方法来对ArrayList进行序列化控制。
10.ArrayList实现RandomAccess接口有什么用
RandomAccess接口是一个标记接口并没有定义任何方法,ArrayList 实现它是标记ArrayList支持快速随机访问这一特性!
11.并发修改异常ConcurrentModificationException

并发修改异常ConcurrentModificationException通常出现在对一个List进行遍历时,对正在遍历的数组进行了修改性操作(修改性操作:改变大小(size=数量)的操作,而不是指具体值)(add,remove,clear等)时便会抛出这个异常,在ArrayList内部有一个protected transient int modCount = 0的变量用于记录对ArrayList的修改,比如add方法代码

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

可以看到在调用add方法时会执行modCount++操作以此标记最新修改的数量modCount

而在遍历时比如forEach先看代码

public void forEach(Consumer action) {
    Objects.requireNonNull(action);
    final int expectedModCount = modCount;
    @SuppressWarnings("unchecked")
    final E[] elementData = (E[]) this.elementData;
    final int size = this.size;
    for (int i=0; modCount == expectedModCount && i < size; i++) {
        action.accept(elementData[i]);
    }
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}

可以看到for循环中一个明确的条件是modCount == expectedModCount 每次遍历都会检测该条件是否成立,而在进入该段代码之前先用final int expectedModCount = modCount;来保存代码执行之前的修改数量,当进入遍历后,有了更改操作,就会使得expectedModCount 和modCount不相等此时便会抛出ConcurrentModificationException异常,对于其他的修改操作,原理都是类似的。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/74311.html

相关文章

  • java源码

    摘要:集合源码解析回归基础,集合源码解析系列,持续更新和源码分析与是两个常用的操作字符串的类。这里我们从源码看下不同状态都是怎么处理的。 Java 集合深入理解:ArrayList 回归基础,Java 集合深入理解系列,持续更新~ JVM 源码分析之 System.currentTimeMillis 及 nanoTime 原理详解 JVM 源码分析之 System.currentTimeMi...

    Freeman 评论0 收藏0
  • ArrayList源码解析之fail-fast机制深入理解

    摘要:当多个线程对同一个集合的内容进行操作时,就可能会产生事件。当某一个线程遍历的过程中,的内容被另外一个线程所改变了就会抛出异常,产生事件。在线程在遍历过程中的某一时刻,线程执行了,并且线程删除了中的节点。 概要 前面,我们已经学习了ArrayList。接下来,我们以ArrayList为例,对Iterator的fail-fast机制进行了解。 1 fail-fast简介 fail-fast...

    NikoManiac 评论0 收藏0
  • Java深入 - 深入理解Java集合

    摘要:集合集合类存放于包中。迭代器,可以通过迭代器遍历集合中的数据是映射表的基础接口有序集合的是非常常用的数据类型。按照指定的迭代器所返回的元素顺序,将该中的所有元素添加到此列表的尾部。 集合集合类存放于Java.util包中。集合类型主要有3种:set(集)、list(列表包含Queue)和map(映射)。 Collection:Collection是集合的基本接口,List、Set、Qu...

    hlcfan 评论0 收藏0
  • Java相关

    摘要:本文是作者自己对中线程的状态线程间协作相关使用的理解与总结,不对之处,望指出,共勉。当中的的数目而不是已占用的位置数大于集合番一文通版集合番一文通版垃圾回收机制讲得很透彻,深入浅出。 一小时搞明白自定义注解 Annotation(注解)就是 Java 提供了一种元程序中的元素关联任何信息和着任何元数据(metadata)的途径和方法。Annotion(注解) 是一个接口,程序可以通过...

    wangtdgoodluck 评论0 收藏0
  • 记一次惨痛的面试经历

    摘要:把内存分成两种,一种叫做栈内存,一种叫做堆内存在函数中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配。堆内存用于存放由创建的对象和数组。 一次惨痛的阿里技术面 就在昨天,有幸接到了阿里的面试通知,本来我以为自己的简历应该不会的到面试的机会了,然而机会却这么来了,我却没有做好准备,被面试官大大一通血虐。因此,我想写点东西纪念一下这次的经历,也当一次教训了。其实面试官大大...

    CoorChice 评论0 收藏0

发表评论

0条评论

zeyu

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<