数组简介
数组对于我们来说并不陌生,在内存中是一块连续的内存空间,通过下标可以随机访问数组元素 如图一所示,而在JDK中提供了功能更加强大的ArrayList其底层是基于数据实现的,为什么JDK已经提供了数据还要使用ArrayList呢?我们通过比较两者之间的差异来聊聊ArrayList的优势
java数组中如何获取数组实际元素
在面试中经常会问在java中数组长度和ArrayList中的size的区别?
通过下面的代码我们其实已经看出来数组获取其长度的时候永远是在声明的时候定义的长度,即使数组中只有一个元素,通过下面的输出有人会问arr[1]输出的是0,这个是因为 int 类型在java中为基本类型所以在初始化数组时会默认用0填充
在ArrayList中的长度为list中实际元素的数量,这一点跟数组有着明显的差别,具体如何实现后面会根据源码分析
数组删除元素和添加元素
在数组中删除一个元素后需要移动元素保证数组的连续性,如果删除的是数组最后一个元素则不需要移动,如果删除了下标为3的元素后使数组连续则需要将3号元素后的元素依次向前移动
删除元素后下标为7的元素不再含有有效元素,如果我们使用的数组删除数据后这些移动的操作需要我们写相关的代码来完成,使用ArrayList删除与数据移动都将自动完成不需要我们手动处理
在数组中添加元素如果插入元素后元素个数超过了数组的长度则会报数组越界的错误,这是因为数组的容量是固定的不能动态的增加
如下图原数组容量为8如果想要成功添加第9个元素则需要将原来数组容量扩大为9然后移动指定位置的元素并将9插入,正因为这样数组不具有动态性因此在使用数据时需要额外考虑很多数组本身的问题导致不能专注于核心代码的编写
ArrayList的出现正式解决了数组存在以下问题
- 动态扩容
- 有效数据长度
- 增加删除元素后数据移动
- 数据遍历
结合ArrayList一些面试问题来看下具体源代码
- ArrayList的默认初始化容量
- 何时扩容
- 扩容容量大小
- 遍历删除fast fail原理
- 迭代器删除为什么不越界
首先看下ArrayList的两个重要的属性
- elementData 存储数据的数组
- size 数组中实际元素个数
- transient Object[] elementData; // non-private to simplify nested class access
- /**
- * The size of the ArrayList (the number of elements it contains).
- *
- * @serial
- */
- private int size;
ArrayList 的构造函数分为有参和无参两种方式
- 无参构造函数为ArrayList()其初始化后并未分配内存空间而是在第一次add操作时分配
- 有参构造函数ArrayList(int initialCapacity)初始化即分配制定大小内存
- 有参构造函数ArrayList(Collection c) 初始化即分配内存
问题一: 无参初始化默认容量是多少,何时扩容,扩容大小为多少都在下面的注释中可以找到
具体分析请看下面注释
- // 默认初始化elementData为空数组
- private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
- public ArrayList() {
- this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
- }
- //接下来进行add操作
- public boolean add(E e) {
- // 检查当前数据容量是否充足
- ensureCapacityInternal(size + 1); // Increments modCount!!
- // size 自增可以保证有效元素的个数
- elementData[size++] = e;
- return true;
- }
- //计算容量大小主要用于默认初始化第一次add操作
- private static int calculateCapacity(Object[] elementData, int minCapacity) {
- //默认初始化时elementData=DEFAULTCAPACITY_EMPTY_ELEMENTDATA
- // 此处返回默认容量DEFAULT_CAPACITY,此值大小为10
- if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
- return Math.max(DEFAULT_CAPACITY, minCapacity);
- }
- return minCapacity;
- }
- private void ensureCapacityInternal(int minCapacity) {
- ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
- }
- private void ensureExplicitCapacity(int minCapacity) {
- modCount++;
- //检查是否扩容,如果当前容量大于数组长度进行扩容
- // overflow-conscious code
- if (minCapacity - elementData.length > 0)
- grow(minCapacity);
- }
- //根据原来数据长度进行扩容
- //oldCapacity >> 1 位运算相当于 oldCapacity=oldCapacity/2
- //新容量为 newCapacity = oldCapacity + (oldCapacity >> 1);即为原来的1.5倍
- //将老数组中的数据移动到新数组中
- //此处解释下为何为1.5倍,这是一种折中是对扩容次数和空间大小的折中,如果扩容次数太多会降低效率如果空间太大会浪费空间
- 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);
- }
问题二:数据遍历,fast-fail,迭代器删除为什么不越界
在如下代码注释中将找到答案
- ArrayList 遍历支持for,foreach,迭代器遍历
- fail-fast 机制是java集合(Collection)中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。
例如:当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。
- //ArrayList 迭代器是通过两个游标数组的下标
- 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() {} // 如果游标不等于size 代表还有下一个元素 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;
- // 更新lastRet游标为当前数组下标
- 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(); } } }