ArrayList
我们先了解一下数组Array的概念
数组是一种顺序储存的线性表,所有的元素的内容地址都是连续的
数组有个致命缺点,就是无法动态的修改容量
而 ArrayList 的底层是基于数组实现的,其容量大小可动态变化
Arraylist 与 LinkedList 区别
1.是否保证线程安全:ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全
2.底层数据结构: Arraylist
底层使用的是 Object 数组;LinkedList
底层使用的是 双向链表 数据结构
3.是否支持快速随机访问:** LinkedList
不支持高效的随机元素访问,而 ArrayList
支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)
方法)。
4.内存空间占用: ArrayList的空间浪费主要体现在在list列表的结尾会预留一定的容量空间,而LinkedList的空间浪费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)。
ArrayList和Vector的区别
这两个类都实现了List接口,他们都是有序集合,即存储在这两个集合中的元素的位置都是有顺序的
(1)同步性:
Vector是线程安全的,它的方法之间是线程同步的。ArrayList是线程序不安全的,它的方法之间是线程不同步的。在不考虑线程安全的情况下,使用ArrayList,因为它效率高;如果考虑线程安全的安全,使用Vector
(2)数据增长:
ArrayList与Vector都有一个初始的容量大小,10。
当添加元素超过集合的长度时,会对原数组进行扩容
Vector扩容的长度为原来数组 ✖ 2 ,ArrayList扩容的长度为原来数组 ✖ 1.5
ArrayList 常用属性
// ArrayList默认初始容量 10
private static final int DEFAULT_CAPACITY = 10;
// 记录ArrayList集合中元素个数
private int size;
// 定义一个Object[]类型的空数组
private static final Object[] EMPTY_ELEMENTDATA = {
};
// ArrayList集合用来储存对象元素的数组
transient Object[] elementData;
ArrayList 构造器
// 无参构造器
public ArrayList() {
// 空参创建空的ArrayList数组
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
public ArrayList(Collection<? extends E> c) {
// 把传入的集合转换为数组 给Object类型的数组elementData
elementData = c.toArray();
//
if ((size = elementData.length) != 0) {
// 当c.toArray转换的数组类型不等object[]类型时
if (elementData.getClass() != Object[].class)
// 使用Arrays.copyOf(original,newLength,newType)方法
// @param original 要复制的数组
// @param newLength 新数组的长度
// @param newType 新数组的类型
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// 否则弄个空的数组给他
this.elementData = EMPTY_ELEMENTDATA;
}
}
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);
}
}
ArrayList的扩容机制
当ArrayList调用 add() 方法时,当 size == elementData.length 的时候,会调用 grow() 方法对ArrayList进行扩容
// 数组扩容方法
// minCapacity = size + 1
private int newCapacity(int minCapacity) {
// 当前数组长度
int oldCapacity = elementData.length;
// 新的数组容量 = 旧数组长度 + 旧数组长度 / 2
// oldCapacity = 10 oldCapacity >> 1 --- 5
// 例如10的二进制为 : 0000 1010 >> 1 -----> 0000 0101 = 5
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity <= 0) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
// 如果一开始没有定义初始容量这时newCapacity=0,返回默认容量10
// 可以得出当无参new 一个ArrayList()时候,这个ArrayList()为空集合,size为0
return Math.max(DEFAULT_CAPACITY, minCapacity);
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return minCapacity;
}
return (newCapacity - MAX_ARRAY_SIZE <= 0)
? newCapacity // 这里返回的长度为原数组的1.5倍
: hugeCapacity(minCapacity);
}
由此可得出结论
通过无参构造器实例化的ArrayList初始容量为 0
在首次调用add()方法之后,返回一个容量为10的数组,后面每次扩容后新数组的长度为原数组长度的 1.5 倍
可以简单理解为:首次扩容为10 ,后面每次扩容为原数组的1.5倍
ArrayList频繁扩容导致添加性能极具下降,该如何处理
解决方案:通过有参构造器 ,提前设置好容量, 空间换时间
public static void main(String[] args) {
// 创建集合对象
ArrayList<String> list = new ArrayList<>();
// 添加元素
list.add("A");
list.add("B");
list.add("C");
Long startTime = System.currentTimeMillis();
// 需求:需要添加100W跳数据
for (int i = 0; i < 1000000; i++) {
list.add(i+"");
}
Long endTime = System.currentTimeMillis();
// 添加100W条数据用时:129
System.out.println("添加100W条数据用时:" + (endTime - startTime));
// ArrayList(initialCapacity) 通过有参构造器指定ArrayList集合容量
ArrayList<String> list1 = new ArrayList<>(1000000);
startTime = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
list1.add(i+"");
}
endTime = System.currentTimeMillis();
// 提前声明数组长度后,添加100W条数据用时:51
System.out.println("提前声明数组长度后,添加100W条数据用时:" + (endTime - startTime));
}
ArrayLis插入或删除元素一定比LinkedList慢么
不一定
先看下ArrayList删除功能的源码
public E remove(int index) {
// 范围校验
Objects.checkIndex(index, size);
// 将源数组赋给es
final Object[] es = elementData;
// 将index对应的元素赋值给 oldValue
E oldValue = (E) es[index];
// 调用fastRemove方法
fastRemove(es, index);
return oldValue;
}
private void fastRemove(Object[] es, int i) {
// 修改次数+1
modCount++;
// newSize新数组的长度
final int newSize;
if ((newSize = size - 1) > i)
// 如果新数组长度newSize 大于 源数组size所要删除元素的索引
// 使用arraycopy方法进行拷贝
//注意 :这里源数组跟目标数组都为es,说明没有创建新数组
System.arraycopy(es, i + 1, es, i, newSize - i);
// 将源数组最后一个元素设置为null,今早让垃圾回收机制对其进行回收
es[size = newSize] = null;
}
通过源码分析,我们得出一个结论
ArrayList删除元素的时候,并没有创建一个新的数组
而是把数组中要删除的元素从后往前给覆盖了,然后把最后一个元素设为值null
在对比一下LinkedList删除功能的源码
public E remove(int index) {
// 校验索引
checkElementIndex(index);
/*
if(return index >= 0 && index < size){
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
*/
return unlink(node(index));
}
看下unlink()方法里的node()方法
Node<E> node(int index) {
// 如果删除索引index小于LinkedList集合长度的一半
if (index < (size >> 1)) {
// 如果小于,那么把第一个节点赋值给x
Node<E> x = first;
// 从头开始往后找
for (int i = 0; i < index; i++)
x = x.next;
// 返回找到的节点
return x;
} else {
// 把最后一个节点赋值给x
Node<E> x = last;
// 从最后一个位置往前找
for (int i = size - 1; i > index; i--)
x = x.prev;
// 返回找到的节点
return x;
}
}
根据源码得出结论,LinkedList每次删除元素的时候都要逐个检索元素
如果LinkedList容量很大,删除的元素索引处于中间部位
这时LinkedList删除效率并不见的比ArrayList效率快
如何赋值某个ArrayList到另一个ArrayList中去
1. 使用clone()方法
这里是浅拷贝操作,不是添加操作
public Object clone() {
try {
// 把源集合中的成员信息拷贝到 V
ArrayList<?> v = (ArrayList<?>) super.clone();
// 把源集合中的数组元素拷贝到v的数组元素中
v.elementData = Arrays.copyOf(elementData, size);
// 把v中的修改次数变量重置为0
v.modCount = 0;
return v;
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
}
2. 使用ArrayList构造方法
这里回顾之前ArrayList的有参构造方法
public ArrayList(Collection<? extends E> c) {
// ArrayList的toArray()返回结果是object类型的数组
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// 当c.toArray转换的数组类型不是object[]类型时
// 即c可能为其他类型的toArray()方法返回的不是object类型
if (elementData.getClass() != Object[].class)
// 这时就使用Arrays.copyOf(original,newLength,newType)方法
// 通过Arrays.copyOf()方法把 c 转换为object类型的数组 (对应ArrayList的数组类型)
// @param original 要复制的数组
// @param newLength 新数组的长度
// @param newType 新数组的类型
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// 否则弄个空的数组给他
this.elementData = EMPTY_ELEMENTDATA;
}
}
3. 使用addAll用法
这里不是拷贝操作,而是将新集合添加到ArrayList集合的后面
public boolean addAll(Collection<? extends E> c) {
// 把传入的集合转换为数组 给Object类型的数组elementData
Object[] a = c.toArray();
modCount++;
int numNew = a.length;
if (numNew == 0)
return false;
Object[] elementData;
final int s;
// 这里判断,如果新集合长度加上源集合的元素个数 大于源集合容量时,进行grow()扩容
if (numNew > (elementData = this.elementData).length - (s = size))
elementData = grow(s + numNew);
// 调用 System.arraycopy ()方法进行数组的拷贝
System.arraycopy(a, 0, elementData, s, numNew);
size = s + numNew;
return true;
}
ArrayList(int initialCapacity)会不会初始化数组大小?
会初始化数组大小
但是ArrayList大小没有变,即 size = 0
因为在ArrayList的有参构造函数中,没有把int initialCapacity赋值给size
ArrayList中获得元素个数 size()方法
这里我们猜想一下,当调用set,remove方法时,肯定会有索引校验的方法
如果这个时候调用set()方法,就会出现索引越界异常
这是Java Bug里面的一个经典问题了,大家仔细品读一下。
结尾
可能大家会觉得
那也只能硬啃,慢慢品
ArrayList在面试里面问的频率没HashMap,ConcurrentHashMap高,但是也有一定概率问到的,还是那句话,小心使得万年船