写在前面:java
小伙伴儿们,你们好!今天来学习ArrayList相关内容,做为面试必问的知识点,来深刻了解一波!程序员
思惟导图:面试
ArrayList就是动态数组,是List接口的可调整大小的数组实现;除了实现List接口以外,该类还提供了一些方法来操纵内部使用的存储列表的数组大小。它的主要底层实现是数组Object[] elementData。算法
数组的特色你们都知道,遍历查询速度快——数组在内存是连续空间,能够根据地址+索引的方式快速获取对应位置上的元素。可是它的增删速度慢——每次删除元素,都须要更改数组长度、拷贝以及移动元素位置。数组
ArrayList 是 java 集合框架中比较经常使用的数据结构了。继承自 AbstractList,实现了 List 接口。底层基于数组实现容量大小动态变化。容许 null 的存在。同时还实现了 RandomAccess、Cloneable、Serializable 接口,因此ArrayList 是支持快速访问、复制、序列化的。缓存
与ArrayList相似的是LinkedList,可是LinkedList底层是链表,它的数组遍历速度慢,但增删速度很快。安全
小结:数据结构
ArrayList底层是数组实现的存储,查询效率高,增删效率低。多线程
下面是查看API中构造方法架构
咱们看源码中的无参构造方法:
无参构造,使用默认的size为10的空数组,在构造方法中没有对数组长度进行设置,会在后续调用add方法的时候进行扩容。
里面是一个赋值操做,右边是一个空容量数组,左边则是存储数据的容器,如下是参照源码分析;
//默认空容量数组,长度为0 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; //集合真正存储数据的容器 transient Object[] elementData;
来看看源码中的int型构造方法:
参数大于0,elementData初始化为initialCapacity大小的数组;
参数等于0,elementData初始化为空数组;
参数小于0,抛出异常;
来看看构造方法的源码:
将一个参数为Collection的集合转变为ArrayList(实际上就是将集合中的元素换为了数组的形式);若是传入的集合为null会抛出空指针异常。
c.toArray()可能不会正确地返回一个 Object[]数组,那么使用Arrays.copyof()方法。
若是集合转换成数组以后数组长度为0,那就直接使用本身的空成员变量初始化elementData。
总结:
上面的构造方法理解起来比较简单,无参构造和初始化容量构造的目的都是初始化底层数组elementData(this.elementData=XXX);
无参构造方法会将elementData初始化一个空数组,插入元素时,扩容将会按默认值从新初始化数组;
有参构造方法会将elementData初始化为参数值大小(>=0)的数组;
若是在构造 ArrayList 实例时,指定初始化值(初始化容量或者集合),那么就会建立指定大小的 Object 数组,并把该数组对象的引用赋值给 elementData;若是不指定初始化值,在第一次添加元素值时会使用默认的容量大小 10 做为 elementData 数组的初始容量,使用 Arrays.conpyOf() 方法建立一个 Object[10] 数组。
通常状况下,咱们用默认的构造方法便可。上面说到使用无参构造时会调用add方法并进行扩容,下面来看看add方法以及扩容的细节。
看看ArrayList的add()添加方法:
public boolean add(E e)
先来看看源码分析
咱们先来看第一个添加方法add(E e),具体流程以下:
//将添加的数据传入给e public boolean add(E e) { //调用方法对内部容量进行校验 ensureCapacityInternal(size + 1); elementData[size++] = e; return true; }
咱们看到add方法中在添加元素以前,会先判断size的大小,因此咱们来看看ensureCapacityInternal方法的细节之处
private void ensureCapacityInternal(int minCapacity) { //判断集合存数据的数组是否等于空容量的数组 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { //经过最小容量和默认容量 出较大值 (用于第一次扩容) minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } //将if中计算出来的容量传递给下一个方法,继续校验 ensureExplicitCapacity(minCapacity); }
当 要 add 进第1个元素时,minCapacity为(size+1=0+1=)1,在Math.max()方法比较后,minCapacity 为10。而后紧接着调用ensureExplicitCapacity更新modCount的值,并判断是否须要扩容。接下来看ensureExplicitCapacity方法
private void ensureExplicitCapacity(int minCapacity) { //实际修改集合次数++ (在扩容的过程当中没用,主要是用于迭代器中) modCount++; //判断最小容量 - 数组长度是否大于 0 if (minCapacity - elementData.length > 0) //将第一次计算出来的容量传递给 核心扩容方法 grow(minCapacity); }
而后是扩容的核心**grow()**方法:
private void grow(int minCapacity) { //记录数组的实际长度,此时因为木有存储元素,长度为0 int oldCapacity = elementData.length; //核心扩容算法 原容量的1.5倍 int newCapacity = oldCapacity + (oldCapacity >> 1); //判断新容量-最小容量是否小于0, 若是是第一次调用add方法必然小于 if (newCapacity - minCapacity < 0) //仍是将最小容量赋值给新容量 newCapacity = minCapacity; //判断新容量-最大数组大小是否>0,若是条件知足就计算出一个超大容量 if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // 调用数组工具类方法,建立一个新数组,将新数组的地址赋值给 elementData elementData = Arrays.copyOf(elementData, newCapacity); }
执行流程:
public void add(int index, E element)
先来看看源码分析
//在元素序列index位置处插入 public void add(int index, E element) { rangeCheckForAdd(index); //1,检测是否须要扩容 ensureCapacityInternal(size + 1); // Increments modCount!! //2,将index及其以后的元素都向后移一位 System.arraycopy(elementData, index, elementData, index + 1, size - index); // 3. 将新元素插入至 index 处 elementData[index] = element; size++; } private void rangeCheckForAdd(int index) { //这里判断的index>size,index小于0,超出指定范围就报错 if (index > size || index < 0) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); }
咱们再看看它的使用方法:
import java.util.ArrayList; /** * @author 公众号:程序员的时光 * @create 2020-11-03 17:05 * @description */ public class Test03 { public static void main(String[] args) { ArrayList<String> list=new ArrayList<>(); list.add("海心焰"); //在index为1的位置处添加数据 list.add(1,"玄黄炎"); System.out.println(list); } }
运行结果:
public boolean addAll(Collection<? extends E> c);
这个方法的描述是,按指定集合的Iterator返回的顺序将指定集合中的全部元素追加到此列表的末尾。
简单来说,就是将一个集合的元素所有添加到另一个集合中去。
运行结果:
public boolean addAll(int index, Collection<? extends E> c);
这个方法和上面的方法都是把一个集合中的元素添加到另一个集合中去,不一样的在于上面的方法是默认添加至目标集合的尾部,而此方法是包含索引的,也就是在指定位置开始插入元素。
运行结果:
ArrayList包括不少方法,咱们来简单回顾一下。
//移除指定位置上的元素 public E remove(int index); //移除此列表中首次出现的指定元素(若是存在) boolean remove(Object o); //修改集合元素 public E set(int index, Object o); //查找集合元素 public E get(int index); //清空集合全部数据 public void clear(); //判断集合是否包含指定元素 public boolean contains(Object o); //判断集合是否为空 public boolean isEmpty()
这个请参照3.1章节的扩容步骤,来看看它的核心扩容方法:
总结:
- 扩容的大小是原先数组的1.5倍;
- 若值newCapacity比传入值minCapacity还要小,则使用传入minCapacity,若newCapacity比设定的最大容量大,则使用最大整数值;
比方说如今须要往数组里添加10w条数据,咱们来看看先后的时间变化:
结果是:
ArrayList底层是数组实现的,那么每次添加数据时会不断地扩容,这样的话会占内存,性能低,因此致使时间很长。
咱们能够用ArrayList的指定初始化容量的构造方法来解决性能低的问题。
在前面咱们说过,它有按照索引添加,也有直接添加的。在这以前须要校验长度问题ensureCapacityInternal,若是长度不够,则须要进行扩容操做。
而前面的扩容是扩大原来的1.5倍,采用位运算,右移一位。
若是后面的数据量级过大,在100万条数据中新增一个元素,后面的元素都要拷贝以及移动位置,因此说效率很低。
ArrayList线程是不安全的。线程安全的数组容器是Vector,它的原理是把全部的方法都加上synchronized。
咱们来测试一下,先准备一个线程任务类:
而后定义测试类,对任务类进行测试:
咱们来看结果:
能够看到会报异常错误,有的线程仍是为null,这说明ArrayList线程是不安全的。
固然能够用线程安全的集合Vector来代替ArrayList
或者咱们能够直接加synchronized关键字,把不安全的线程变成安全的:
这样也是能够保证线程安全的。
为啥ArrayList线程不安全?
线程不安全:
线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其余线程不能进行访问直到该线程读取完,其余线程才可以使用。不会出现数据不一致或者数据污染。
线程不安全就是不提供数据访问保护,有可能出现多个线程前后更改数据形成所获得的数据是脏数据。
List接口下面有两个实现,一个是ArrayList,另一个是Vector。从源码的角度分析,由于Vector的方法前加了synchronized关键字。
ArrayList在添加一个元素时,有两步来完成,1. 在 Items[Size] 的位置存放此元素;2. 增大 Size 的值。
在单线程运行的状况下,若是 Size = 0,添加一个元素后,此元素在位置 0,并且 Size=1;
而若是是在 多线程状况下,好比有两个线程,线程 A 先将元素存放在位置 0。可是此时 CPU 调度线程A暂停,线程 B 获得运行的机会。线程B也向此 ArrayList 添加元素,由于此时 Size 仍然等于 0(注意哦,咱们假设的是添加一个元素是要两个步骤哦,而线程A仅仅完成了步骤1),因此线程B也将元素存放在位置0。而后线程A和线程B都继续运行,都增长 Size 的值。
那好,咱们来看看 ArrayList 的状况,元素实际上只有一个,存放在位置 0,而 Size 却等于 2。这就是“线程不安全”了。
当操做是在一列数据的后面添加数据而不是在前面或中间,而且须要随机地访问其中的元素时,使用ArrayList会提供比较好的性能;
当操做是在一列数据的前面或中间添加或删除数据,而且按照顺序访问其中的元素时,使用LinkedList会更好。