前言
在集合框架中, ArrayList 是一个普通的类,实现了 List 接口,具体框架图如下:
1. ArrayList 实现了 RandomAccess 接口,表明 ArrayList 支持随机访问
2. ArrayList 实现了 Cloneable 接口,表明 ArrayList 是可以 clone 的
3. ArrayList 实现了 Serializable 接口,表明 ArrayList 是支持序列化的
4. 和 Vector 不同, ArrayList 不是线程安全的,在单线程下可以使用,在多线程中可以选择 Vector 或者CopyOnWriteArrayList
5. ArrayList 底层是一段连续的空间,并且可以动态扩容,是一个动态类型的顺序表
一、ArrayList的三个构造方法解析
方法 | 解释 |
ArrayList() |
无参构造 |
ArrayList (Collection<? extends E> c) |
利用其他 Collection 构建 ArrayList |
ArrayList (int initialCapacity) |
指定顺序表初始容量 |
public static void main(String[] args) { ArrayList<Integer> arrayList1 = new ArrayList<>(); ArrayList<Integer> arrayList2 = new ArrayList<>(12); arrayList1.add(1); ArrayList<Integer> arrayList3 = new ArrayList<>(arrayList1); System.out.println(arrayList3); }
注意:使用ArrayList(Collection<? extends E> c)这个构造方法时候,因为这里是通配符的上界,所以注意传入的类型必须是E或者E的子类。
二、ArrayList是如何扩容的?(源码分析)
首先看不带参数的构造方法:
DEFAULT_CAPACITY = 10
默认容量为10;transient Object[] elementData
,类似于顺序表背后的数组;private int size
,size为有效元素的个数,此时为0,说明DEFAULTCAPACITY_EMPTY_ELEMENTDATA
的长度为0,这个构造方法没有分配数组内存。
那么现在就有一个问题,DEFAULTCAPACITY_EMPTY_ELEMENTDATA的长度为0 与 默认容量为10为什么是矛盾的呢?此时没有大小,那第一次add添加数据的时候是怎么放进去的呢?
接下来,我们继续点击进入add方法里面:
说明存储元素的时候,默认放到最后一个元素的后边的。但是这里用到了一个ensureCapacityInternal(size + 1)方法,我们在点击进入,
此时size为0,当size+1传进入的时候,这个时候, minCapacity为1;那么这里又有两个方法,这个calculateCapacity(elementData, minCapacity)方法的返回值 要作为 ensureExplicitCapacity()这个方法的参数。
那么我们再点击进入calculateCapacity(elementData, minCapacity)方法,这里顾名思义就是计算容量,此时minCapacity为1,因为此时调用的是没有带参数的构造方法,那么此时 elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA。
此时 If 语句满足,那么就会求最大值,此时 minCapacity为1,DEFAULT_CAPACITY = 10,所以第一次调用add的时候,这个时候就会返回10。那么这个时候,返回值10就会作为ensureExplicitCapacity()的参数,那么我们再点击进入这个方法内部:
此时 minCapacity为10,此时 elementData这个数组的长度为0,那么10-0 > 0,那么就会调用grow这个函数,把10传过去。我们再点击grow进入这个函数内部:
此时oldCapacity、newCapacity通过计算出来都是0,因为0 - 10 < 0的,那么此时newCapacity = minCapacity = 10。接下来再看下面一个if语句:
此时MAX_ARRAY_SIZE为 int 的最大值减8。这个时候这个if语句进不来,继续往下执行,那么此时这个数组才真正的分配内存。
elementData = Arrays.copyOf(elementData, newCapacity); public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; }
相当于 ensureCapacityInternal(size + 1)这个方法走完,数组的容量才是10,然后才能把元素放进去。所以可以总结,前提是调用不带参数的构造方法,第一次add的时候,默认容量才是10。
那么它是如何扩容的呢?比如在放第11个元素的时候?
接下来,我们还是继续看grow这个函数。
private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; //按照1.5倍方式扩容 int newCapacity = oldCapacity + (oldCapacity >> 1); // 如果用户需要扩容大小 超过 原空间1.5倍,按照用户所需大小扩容 if (newCapacity - minCapacity < 0) newCapacity = minCapacity; // 如果需要扩容大小超过MAX_ARRAY_SIZE,重新计算容量大小 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); }
int newCapacity = oldCapacity + (oldCapacity >> 1);其实这一句代码就是在进行扩容,而且是1.5倍的扩容。
其实带参数的构造方法,初始值给的多少,那么初始容量就是多少:
ArrayList(Collection<? extends E> c)这种构造方法,其实是进行的数组拷贝:
小结:
1. 检测是否真正需要扩容,如果是调用grow准备扩容
2. 预估需要库容的大小 :初步预估按照1.5倍大小扩容 ;如果用户所需大小超过预估1.5倍大小,则按照用户所需大小扩容 ;真正扩容之前检测是否能扩容成功,防止太大导致扩容失败。
三、ArrayList的常用方法
方法 | 解释 |
boolean add(E e) |
尾插e |
void add (int index, E element) |
将 e 插入到 index 位置 |
boolean addAll (Collection<? extends E> c) |
尾插 c 中的元素 |
E remove (int index) |
删除 index 位置元素 |
boolean remove (Object o) |
删除遇到的第一个 o |
E get (int index) |
获取下标 index 位置元素 |
E set (int index, E element) |
将下标 index 位置元素设置为 element |
void clear () |
清空 |
boolean contains (Object o) |
判断 o 是否在线性表中 |
int indexOf (Object o) |
返回第一个 o 所在下标 |
int lastIndexOf (Object o) |
返回最后一个 o 的下标 |
List<E> subList (int fromIndex, int toIndex) |
截取部分 list |
这里需要注意List<E> subList(int fromIndex, int toIndex)这个方法:
public static void main(String[] args) { ArrayList<Integer> arrayList1 = new ArrayList<>(); arrayList1.add(1); arrayList1.add(2); arrayList1.add(3); arrayList1.add(4); arrayList1.add(5); List<Integer> list = arrayList1.subList(1,3); System.out.println(list);//2,3 list.set(0,99); System.out.println(arrayList1);// 1,99,3,4,5 System.out.println(list);//99,3 }
这个会发现,如果修改list里面的数据,那么arraylist里面的数据也会被修改。来看一张内存图就会明白怎么回事。
四、ArrayList的三种遍历
ArrayList 可以使用三方方式遍历:for循环+下标、foreach、使用迭代器
public static void main(String[] args) { ArrayList<Integer> arrayList1 = new ArrayList<>(); arrayList1.add(1); arrayList1.add(2); arrayList1.add(3); arrayList1.add(4); arrayList1.add(5); int size = arrayList1.size(); for (int i = 0; i < size; i++) { System.out.print(arrayList1.get(i)+" "); } System.out.println(); for (int x : arrayList1) { System.out.print(x+" "); } System.out.println(); Iterator<Integer> it = arrayList1.iterator(); while (it.hasNext()) { System.out.print(it.next()+" "); } }
五、ArrayList的缺陷
ArrayList底层是使用数组来存储元素的, 由于其底层是一段连续空间,当 在 ArrayList 任意位置插入或者删除元素时,就需要将后序元素整体往前或者往后 搬移,时间复杂度为 O(n) ,效率比较低,因此 ArrayList 不适合做任意位置插入和删除比较多的场景。而且,在扩容之后,可能会带来空间的浪费。 ArrayList适合在给定了下标位置的情况下进行查找元素,此时时间复杂度可以达到O(1)。 因此: java 集合中又引入了 LinkedList ,即链表结构。