java中的集合分为两类,一类是Collection体系,一类是Map体系.
Collection接口和Map分别是两大体系的顶层接口,在Collection接口下又有三大子接口,
分别是List接口、Set接口、Queue接口(队列),其中List,Qeue是有序可重复的
,而Set是无序不可重复的。在List接口下主要有两个实现类,分别是ArrayyList实现类和
LinkedList实现类,其中ArrayList底层是数组实现的,与数组的区别是ArrayList是可以
扩容的,即动态数组。Set则是有hashSet实现类。Queue则是有数组和链表两种形式。Map是
属于另一个集合体系的,其中都是以key-value形式存在的,其中key必须唯一,主要有HashMap实现类
HashTable实现类、TreeMap实现类。
在List下ArrayList实现类和LinkedList实现类是使用较多的,其中ArrayList实现类底层是由数组实现的,
LinkedList底层是由链表实现的。
1. ArrayList继承体系
下面是ArrayList源码的一部分:
从上面的继承体系我们可以看出:
- ArrayList实现了List接口、RandomAccess接口、Cloneable接口、Serializable接口
- 实现了List,得到了List集合框架的基础功能,具备了基本的添加、删除、遍历等操作
- 实现了RandomAccess接口,具备了随机访问的功能
- 实现了Cloneable接口,具备了克隆(浅拷贝)的功能
- 实现了Serializable接口,具备了可序列化的功能
2.实现了Cloneable接口
浅拷贝与深拷贝
浅拷贝:只是拷贝了一个地址
package 源码学习;
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList list = new ArrayList();
list.add("abc");
Person person = new Person("张龙");
list.add(person);
ArrayList clone = (ArrayList)list.clone();
System.out.println(list.get(1) == person);
/*
比较添加到集合里的元素和原来的元素的地址,结果为true
说明添加到集合里面的只是一个地址
*/
System.out.println(list.get(1) == clone.get(1)); //比较克隆后两个集合里面元素的地址是否相同
/*
比较原来的集合和克隆结合里的元素是否相等,结果为true
说明克隆结合里面的元素只是原来集合里面元素的地址
*/
System.out.println(list == clone); //比较克隆后两个集合的地址是否相同
/*
比较两个集合的地址是否相等,结果为false,
又因为两个集合里面元素的地址相同,所以这里的克隆
只是new了一个新的集合,然后将旧集合里的元素的地址
添加到了里面,我们称之为浅拷贝
*/
Person p = (Person)list.get(1);
System.out.println(p == list.get(1));
/*
集合里面返回的元素和原来的元素的地址相同,说明返回的是一个地址
*/
person.setName("赵虎");
System.out.println(list.get(1) == clone.get(1));
/*
当修改原来集合中的元素时,克隆集合中对应元素的地址仍然和原来集合
对应的元素地址相同,更加说明了两个集合中元素指向的是同一块地址
*/
}
}
- 从案例中我们可以看出,ArrayList的clone方法,其实只是new了一个ArrayList集合,里面存储的其实都是原来旧集合
- 里面的地址,里面的元素实际与原来的集合中的元素指向同一块地址,只是地址的拷贝,这样的拷贝我们称之为浅拷贝
而我们所说的深拷贝指的是创建一个新的对象,而不是和原来的元素共用一块地址
3.实现RandomAccess接口,具备了随机访问的功能
迭代器 it 的两个基本操作是 next 、hasNext 和 remove。. 调用 **it.next ()
会返回迭代器的下一个元素,并且更新迭代器的状态。 调用 it.hasNext () 用于检测集合中是否还有元素。**
测试ArrayList随机访问和顺序访问的时间差异
package 源码学习.ArrayList中顺序访问与随机访问速度的比较;
import java.util.ArrayList;
import java.util.Iterator;
public class Main {
public static void main(String[] args) {
ArrayList list = new ArrayList();
for(int i = 0; i < 1000000; i++) list.add(i);
long start1 = System.currentTimeMillis();
for(int i = 0; i < 1000000; i++){
list.get(i);
}
long end1 = System.currentTimeMillis();
System.out.println("随机访问的时间:" + (end1 - start1));
long start2 = System.currentTimeMillis();
Iterator iterator = list.iterator();
while(iterator.hasNext()){
iterator.next();
//System.out.println(iterator.next());
}
long end2 = System.currentTimeMillis();
System.out.println("顺序访问的时间:" + (end2 - start2));
}
}
- 看了别人的博客,说的是随机访问的速度快,而我测了三次,却有三种不同的结果.......以后回头再分析
而没有实现RandomAccess接口的LinkedList集合中顺序访问与随机访问的速度比较
package 源码学习.ArrayList中顺序访问与随机访问速度的比较;
import java.util.*;
import java.lang.*;
public class Main {
public static void main(String[] args) {
LinkedList list = new LinkedList();
for(int i = 0; i < 100000; i++) list.add(i);
long start1 = System.currentTimeMillis();
for(int i = 0; i < 100000; i++){
list.get(i);
}
long end1 = System.currentTimeMillis();
System.out.println("随机访问的时间:" + (end1 - start1));
long start2 = System.currentTimeMillis();
Iterator iterator = list.iterator();
while(iterator.hasNext()){
iterator.next();
//System.out.println(iterator.next());
}
long end2 = System.currentTimeMillis();
System.out.println("顺序访问的时间:" + (end2 - start2));
}
}
- 从LinkedList集合的源码可以看出它并没有实现RandomAccess接口,不具备随机访问的功能
- 从图中我们可以明显看出对于LinkedList来说顺序访问比随机访问的速度快得多
4. ArrayList的属性
- DEFAULT_CAPACITY:集合的默认容量,在使用new
- ArrayList()构造方法创建实例时,默认的容量是10,初始容量是0,在添加一个元素之后容量会扩容到10
- EMPTY_ELEMENTDATA:空数组,在使用new ArayList(0)创建实例时使用的是这个空数组
- DEFAULTCAPACITY_EMPTY_ELEMENTDATA :默认容量空数组,在使用new
- ArrayList()创建实例时使用的是这个空数组,与EMPTY_ELEMENTDATA空数组**不同之处在于DEFAULTCAPACITY_EMPT
- Y_ELEMENTDATA 在添加第一个元素之后容量会扩容到10
- elementData:存储数据元素的数组,使用transient修饰,表示该数组不可以被序列化
- size:集合中已经存储的数据元素的个数,与elementData的长度(容量)不一样
5. ArrayList构造方法(三种)
ArrayList(int initialCapacity)有参构造方法
- initialCapacity**代表传入的指定初始容量大小
当initialCapacity > 0创建一个该指定大小的Object数组,并将该集合指针指向该Object数组,此时集合容量大
小为initialCapacity ,集合中元素的个数是0
当initialCapacity == 0将该集合指针指向**EMPTY_ELEMENTDATA**空数组,此时集合容量大小为0,集合中元素
的个数是0
当initialCapacity < 0抛出异常
ArrayList()无参构造方法
无参构造函数将该集合的指针指向DEFAULTCAPACITY_EMPTY_ELEMENTDATA默认空数组集合
此时集合容量大小为0,集合中元素个数是0,当第一次添加元素时会把数组的容量扩容为10
ArrayList(Collection<? extends E> c)有参构造方法
有参构造方法.png)
图片挂了,传不上去(;´д`)ゞ
ArrayList的Collection有参构造函数
在传入Collection序列时,会把该序列转化为数组,再将ArrayList集合的指针
指向该数组,如果该指针不为空,则判断数组是不是Object[]的,如果不是,则转化为Object[]类型,这里涉及到了数组
的创建与拷贝,这时集合的容量大小等于集合的元素数量,为数组的长度,这时集合是满的;如果该指针为空,则将Arra
yList指针指向EMPTY_ELEMENTDATA空数组
注:这里并不知道为什么要转化成Object[]类型,可能是为了满足集合的特性,以后可以存储不同数据类型的元素吧
6. ArrayList的相关操作方法
add操作(4种)
add(E e)向集合末尾插入一个元素**
时间复杂度:O(1)**
//往集合中添加一个元素
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
//当前集合中数据元素的个数++
//执行这个方法,确保内部容量
elementData[size++] = e;
//将当前插入的元素插入的集合的结尾
return true;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
//计算最小容量,只有空参创建的ArrayList对于这一项才可能有意义
//对于其他方式创建的ArrayList来说calculateCapacity并没有意义
//确保明确的容量
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//如果数组等于DEFAULTCAPACITY_EMPTY_ELEMENTDATA默认空数组,返回默认容量10和最小需要容量的最大值
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
//否则返回最小需要的容量
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
//当前集合中数据元素的个数++
// overflow-conscious code
if (minCapacity - elementData.length > 0)
//如果数据元素的个数大于集合的容量,需要执行grow函数进行扩容
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
//老的容量
int newCapacity = oldCapacity + (oldCapacity >> 1);
//新的容量是老的容量加上老的容量的一半,也就是说新的容量是老的容量的1.5倍
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//如果扩容之后还是不够,则以需要的为准进行扩容
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
//如果扩容之后集合的容量大于规定的ArrayList最大长度,则计算最大容量
// 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
//如果最小需要容量小于0,则抛出异常
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ? //如果最小容量大于最大数组容量
//若最小需要容量大于规定的最大ArrayList容量,则将int的最大值赋给minCapacity
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
执行流程:
- 当前集合数据元素个数++,计算最小需要的容量,如果集合是通过new
- ArrayList()创建的DEFAULTCAPACITY_EMPTY_ELEMENTDATA空数组默认初始容量为DEFAULT_CAPACITY默认为10,
返回max(10,最小需要的容量);否则返回最小需要的容量
- 之后检查是否需要扩容,不扩容的话直接插入元素即可;需要扩容的话扩容到原来的1.5倍,如果还是小于最小需要的
- 容量,则以需要的容量为准,如果扩容之后的容量大于最大数组容量,则将容量赋予int的最大值,之后进行数组的创
- 建与拷贝
add(int index, E e)向指定位置添加一个元素的操作
平均时间复杂度:O(n)
public void add(int index, E element) {
rangeCheckForAdd(index);
//检查索引是否为负或者索引超出集合中实际元素的数量,如果为负或者超出集合中实际元素的数量,则抛出异常
ensureCapacityInternal(size + 1); // Increments modCount!!
//确保容量足够
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
//将第一个序列从索引index处开始的数量为size-index的元素依次赋给第二个序列从index+1开始的元素
//将索引处index及之后的元素都向后移动一位,腾出index的位置,共执行了size-index次(索引及之后的元素每个元素向后移动
//1位,这些元素总共移动了size-index次)
elementData[index] = element;
//将添加的元素赋值给指定的索引处的位置
size++;
//元素数量加1
}
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
执行流程:
- 检查索引是否为负或者超出实际元素的数量大小(越界)(注意:这里其实就说明了在通过索引向集合中插入元素时,
- 只能插入到最后一个位置或者之前已经有元素的位置,而不是说随便插入)
- 检查是否需要扩容
- 将索引及索引之后的元素都向后移动一位,给插入的元素腾出位置
- 在指定索引处的位置赋予指定元素
- 集合中元素数量++
addAll(Collection)向集合中添加序列操作
求并集,可以有重复元素(将Collection的序列元素全部添加到集合中)
时间复杂度:O(n)
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
//将添加的集合转化为数组
int numNew = a.length;
//计算添加的集合的实际长度
ensureCapacityInternal(size + numNew); // Increments modCount
//计算需要的最小容量,计算是否需要扩容
System.arraycopy(a, 0, elementData, size, numNew);
//将添加的数组从0开始长度为numNew的元素依次拷贝到elementData集合中,从size位置开始
//将a数组添加到集合的最后
size += numNew;
//更新集合中实际存储的数量
return numNew != 0;
//如果序列c不为空返回true,序列c为空返回false
}
执行流程:
- 将添加的序列转化成数组
- 计算集合需要的最小容量,检查是否需要扩容
- 将添加的数组进行拷贝
- 更新集合中实际元素的数量
- 如果添加的序列不为空,返回true,否则返回false
addAll(int index, Collection c)向指定索引处添加一个序列
public boolean addAll(int index, Collection<? extends E> c) {
rangeCheckForAdd(index);
//检查添加的索引是否为负或者超过集合中实际存储元素的长度
//如果索引为负或者超过集合中实际存储的长度的大小,抛出异常
//也就是说索引只能为集合中已经有元素的位置或者集合的实际长度大小(也就是末尾,第一个没有元素的位置)
Object[] a = c.toArray();
//将添加的序列转换成数组
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
//计算最小需要的容量,检查是否需要扩容
int numMoved = size - index;
//list中元素要移动的次数
if (numMoved > 0)
System.arraycopy(elementData, index, elementData, index + numNew,
numMoved);
//将index索引处及之后的元素往后移动到index+numNew处,这之间的空间存储新添加的序列的元素
System.arraycopy(a, 0, elementData, index, numNew);
//将添加的序列添加到集合中
size += numNew;
//更新集合中元素的数量
return numNew != 0;
//如果新添加的序列的长度为不为0,返回true;否则返回false
}
执行流程:
- 检查索引是否越界
- 将添加的序列转换成数组,检查是否需要扩容,决定是否进行扩容操作
- 将索引及之后的元素移动到添加序列之后实际的位置
- 将添加的序列拷贝到集合中
- 如果添加的序列不为空,返回true;否则返回false
get(int index)操作
get(int index)获取指定索引处的元素
时间复杂度:O(1)
public E get(int index) {
rangeCheck(index);
//检查索引是否越界
return elementData(index);
}
private void rangeCheck(int index) {
if (index < 0 || index >= this.size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
//索引为负或者索引大于等于实际存储的长度,越界抛出异常
}
执行流程:
- 检查索引是否越界(越界抛出异常)
- 未越界返回指定索引处的元素
remove操作
remove(int index)删除指定索引处的元素
时间复杂度:O(n)
public E remove(int index) {
rangeCheck(index);
//检查索引是否越界
modCount++;
//集合底层数组修改次数++
E oldValue = elementData(index);
//获取指定索引处的元素
int numMoved = size - index - 1;
//需要移动的次数
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//要删除索引处的元素,还需要将索引之后的元素全部向前移动一位
elementData[--size] = null; // clear to let GC do its work
//集合的实际长度减1,并将最后一个数据设置为null,方便垃圾回收
return oldValue;
//返回删除的元素
}
执行流程:
- 检查索引是否越界
- 集合底层修改次数++,获取索引处的元素,将index索引之后的元素向前移动一位
- 将集合最后的size位置设置为null,并且size--
- 返回删除的元素
注意:从源码中我们可以看出,在执行remove(int index)操作时集合并没有缩容
remove(Object o)删除指定元素
时间复杂度:O(n)
public boolean remove(Object o) {
//删除指定的元素
if (o == null) {
//如果删除的元素为null,找到第一个为null的元素,执行fastRemove操作,返回true
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
//要删除的元素为null,以null进行比较找到第一次出现null的位置
//再执行fastRemove操作,返回true
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
//如果删除的元素不为空并且集合中存在该元素,使用eauqls操作找到第一次出现要删除元素的索引,
//再执行fastRemove操作,返回true
fastRemove(index);
return true;
}
}
return false;
//如果集合中不存在要删除的元素,返回false
}
private void fastRemove(int index) {
modCount++;
//集合底层数组操作次数++
int numMoved = size - index - 1;
//集合中元素要向前移动的次数
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//集合中索引之后的元素要向前移动一位
elementData[--size] = null; // clear to let GC do its work
//将集合实际存储的最后一位设置为null,方便垃圾回收,并更新集合实际的长度(size--)
}
执行流程:
- 找到第一个出现该元素的位置的索引,如果找到了执行fastRemove(没有检查越界操作,优化了jdk的性能),返回true
- 否则返回false
- fastRemove(int index)将索引之后的元素向前移动一位,并将集合的最后一个位置置为full,方便垃圾回收
retainAll(Collection c)操作
retainAll(Collection)求交集,可以有重复元素
删除集合中未出现在Collection序列中的元素
public boolean retainAll(Collection<?> c) {
Objects.requireNonNull(c);
//检查Collection是否为null
return batchRemove(c, true);
}
public static <T> T requireNonNull(T obj) {
if (obj == null)
throw new NullPointerException();
//为空则抛出异常
return obj;
}
private boolean batchRemove(Collection<?> c, boolean complement) {
//complement为true表示在集合中删除c中不包含的元素
//complement为false表示在集合中删除c中包含的元素
final Object[] elementData = this.elementData;
int r = 0, w = 0;
//采用两个指针,一个读指针,一个写指针,读指针一定不会比写指针慢,一个元素在写之前一定被读过了,所以写操作可以与读 //操作在一个数组上操作
boolean modified = false;
//是否修改
try {
for (; r < size; r++) //每次读指针必定往后移动一位
if (c.contains(elementData[r]) == complement)
//检查c中包含元素elementData[r]的与否与complement是否相同
elementData[w++] = elementData[r];
//相同则写指针往后移动一位
} finally {
// Preserve behavioral compatibility with AbstractCollection,
// even if c.contains() throws.
if (r != size) {
//检查写指针是否等于集合长度,正常的话最后r == size
//否则则说明c的contains方法抛出了异常,这时我们将集合中r索引及之后的元素从w指针全部写到集合中去
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r; //更新写指针
}
if (w != size) {
// clear to let GC do its work
for (int i = w; i < size; i++)
elementData[i] = null;
//将写指针之后的位置设置为null,方便垃圾回收
modCount += size - w;
//更新底层数组操作次数
size = w;
//更新集合的实际长度
modified = true;
//是否修改改为true
}
}
return modified;
//返回modified
}
执行流程:
- 检查c是否为空,为空抛出异常,否则执行以下操作
- 采用两个r、w指针,遍历elementData数组,将c中存在的元素重新写到elementData数组中,更新底层数组修改次数和
- 数组的实际长度
- 将w指针及之后的数组中的位置置为null,方便垃圾回收
removeAll(Collection c)操作
删除集合中在c中的元素
保留不在c中的元素
public boolean removeAll(Collection<?> c) {
Objects.requireNonNull(c);
//检查c是否为null
return batchRemove(c, false);
}
public static <T> T requireNonNull(T obj) {
if (obj == null)
throw new NullPointerException();
//如果传入的序列为空,抛出空指针异常
return obj;
}
private boolean batchRemove(Collection<?> c, boolean complement) {
final Object[] elementData = this.elementData;
int r = 0, w = 0;
//使用读写两个指针在一个数组上进行操作
boolean modified = false;
try {
for (; r < size; r++)
if (c.contains(elementData[r]) == complement)
//检查elementData[r]在c中存在与否与complement是否相等
elementData[w++] = elementData[r];
} finally {
// Preserve behavioral compatibility with AbstractCollection,
// even if c.contains() throws.
if (r != size) {
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
//r!=size直接将r之后的元素全部给写指针及之后的位置
}
if (w != size) {
// clear to let GC do its work
for (int i = w; i < size; i++)
elementData[i] = null;
modCount += size - w;
size = w;
modified = true;
//更新集合实际元素的大小
//将modified修改为已修改
}
}
return modified;
}
这里的removeAll操作与retain的区别在于:remove保留的是不在序列c中的元素,而retain保留的是在序列c中的元素
ArrayList的toString操作
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
//我们发现在ArrayList中没有toString方法,我们可以到它的父类中寻找
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E>
//我们在它的父类中也没有找到toString方法,那我们到父类的父类中找
AbstractCollection<E>
//我们在父类的父类中找到了toString方法
public String toString() {
Iterator<E> it = iterator();
//获取迭代器
if (! it.hasNext())
return "[]";
//如果集合中没有元素插入,返回"[]"
StringBuilder sb = new StringBuilder();
//有元素则执行以下操作
sb.append('[');
for (;;) {
E e = it.next();
sb.append(e == this ? "(this Collection)" : e);
//这里是判断如果当前的元素就是本身返回"(this Collection)" ,否则返回e
//这里可能有点疑问,我们可以看一下下面代码的运行结果,
if (! it.hasNext())
return sb.append(']').toString();
//没有元素了,用"]"结束,并返回
sb.append(',').append(' ');
//还有元素,拼接','和' ',之后继续拼接下一个元素
}
}
7. Fail-Fast机制
我们知道modCount是用来记录ArrayList内部结构的变化次数的。结构发生变化是指至少添加或删除一个元素,或者数组
的大小发生变化,仅仅改变元素值不属于结构的变化
在序列化或者迭代等操作时,需要比较前后的modCount值是否相等,不相等抛出ConcurrentModificationException异常
代码和运行结果如下:
package test;
import org.junit.Test;
import java.util.*;
public class MyTest {
//Fail_Fast机制
@Test
public void test03(){
ArrayList<Integer> list = new ArrayList();
list.add(1);
list.add(2);
list.add(3);
Iterator<Integer> it = list.iterator();
while(it.hasNext()){
Integer i = it.next();
if(i == 1){
list.remove(1);
}
}
}
}
当我们调用list.remove来删除元素时,此时ArrayList内部的modCount会+1,此时modCount !=
迭代器里的exceptModCount,当遍历下一个元素时,会先调用checkForComodification检查modCount
和exceptModCount是否相等,不相等则抛出异常,这就是Fail-Fast机制
下面是ArrayList源码也可以证明:
public E next() {
checkForComodification();
int i = cursor;
if (i >= SubList.this.size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (offset + i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[offset + (lastRet = i)];
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
那么我们如何避免这一现象的发生呢?Iterator为我们提供了remove方法,可以保证删除前后modCount和expectModCount
的值是相等的,其本质是在删除元素之后modCount++,将modCount的值赋给expectModCount,在下一次循环之前就使modCo
unt的值和expectModCount的值相等从而避免该问题
package 练习;
import java.util.ArrayList;
import java.util.Iterator;
public class Main1 {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList();
list.add(1);
list.add(2);
list.add(3);
Iterator<Integer> it = list.iterator();
while(it.hasNext()){
Integer i = it.next();
if(i == 1){
it.remove();
}
}
System.out.println(list.size());
}
}
总结
1. ArrayList内部使用数组进行存储,每次容量不够时进行扩容,每次扩容一半(一般情况),ArrayList不会进行缩容
2. ArrayList支持随机访问,通过索引访问速度很快(高于顺序访问),时间复杂度为O(1)
3. ArrayList添加元素到尾部速度极快,平均时间复杂度为O(1)
4. ArrayList添加元素到中间速度较慢,因为涉及到搬移元素,平均时间复杂度为O(n)
5. ArrayList从尾部删除元素速度极快,平均时间复杂度为O(1)
6. ArrayList从中间删除元素速度较慢,因为涉及到搬移元素,平均时间复杂度为O(n)
7. ArrayList支持求并集(可以有重复元素),调用addAll(Collection c)方法
8. ArrayList支持求交集(可以有重复元素),调用retain(Collection c)方法
9. ArrayList支持求单项差集,调用removeAll(Collection c)方法
ArrayList的序列化机制
我们知道ArrayList实现了Serializable接口,那么它一定是可以被序列化的,但是集合底层的数组elementData却又被
关键字transient修饰的(被transient修饰的成员变量不被序列化),那么我们的ArrayList是如何实现序列化的呢?
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
//防止序列化的过程中被修改
s.defaultWriteObject();
//写出非transient非static属性(会写出size属性)
// Write out size as capacity for behavioural compatibility with clone()
s.writeInt(size);
//写出size
// Write out all elements in the proper order.
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
//将数组中的元素一个一个写出
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
//序列化过程中有修改则抛出异常
}
}
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
elementData = EMPTY_ELEMENTDATA;
// Read in size, and any hidden stuff
s.defaultReadObject();
//读入非transient非static属性(会读入size属性)
// Read in capacity
s.readInt(); // ignored
//会读入size
if (size > 0) {
// be like clone(), allocate array based upon size not capacity
int capacity = calculateCapacity(elementData, size);
//计算最小需要的容量
SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
//底层数组确保为Object[]属性
ensureCapacityInternal(size);
//进行扩容
Object[] a = elementData;
// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
a[i] = s.readObject();
//将数组中的元素一个一个读入
}
}
}
查看源码我们知道了ArrayList重写了writeObject和readObject来自定义序列化和反序列化,那么什么是自定义序列化与
反序列化呢?
- 在序列化的过程中,如果序列化的类中定义了writeObject和readObject方法,虚拟机会试图调用对象类里的writeObje
- ct和readObject方法,进行用户自定义的序列化和反序列化
- 如果没有这样的方法,会默认调用ObjectOutputStream里的defultWriteObject和ObjectInputStrem里的defaultReadOb
- ject来序列化和反序列化
- 用户自定义的writeObject和readObject可以允许用户控制序列化的过程,比如在序列化的过程中动态改变序列化的数
- 值。
可以看到writeObject和readObject方法来把数组中的元素写入ObjectOutputStream和ObjectInputStrem中,那么为什么A
rrayList要采用这种方式来实现序列化和反序列化呢?
ArrayList本质上是一个动态数组,在数组中的容量不够时,会自动进行扩容,数组中容量的大小不一定等于实际存储
的元素数量,例如指定集合的容量为100,而只存储了一个元素,在序列化的过程中会序列化99个null元素,为了避免这
种情况的发生,我们自定义了序列化的和反序列化,采用自定义序列化和反序列化可以节省空间
面试题
ArratList如何扩容
对于利用new ArrayList()构造的集合来说,第一次扩容为10,之后每次扩容到原来的1.5倍,扩容之后检查是否能够存储
下要添加的元素,小于添加元素后的大小则扩容到刚好存储下添加元素的容量。
对于new ArrayList(0)和new ArrayList(int
size)构造的集合来说,每次扩容到原来的1.5倍,扩容之后检查是否能够存储下要添加的元素,小于添加元素后的大小则
扩容到刚好存储下添加元素的容量。
ArrayList频繁扩容导致性能下降该如何解决
在插入100万条数据时,从上面的运行结果我们可以看出两者的差异
解决方案:预先设定好足够的空间进行存储,可以优化性能。
从ArrayList中删除一个元素是否一定比LinkedList慢
从二者底层数据结构来说:
- ArrayList底层是实现了动态数组的数据结构
- LinkedList底层是实现了链表的数据结构
效率对比:
- 首部插入:LinkedList首部插入快,因为只需要修改头部前后结点的prev值和next值即可,AttayList首部插入较慢,
- 因为数组的移位比较耗时间
- 中间插入:LinkedList中间插入慢,因为要遍历到指定位置比较耗时间,而ArrayList中间插入快,只需根据索引就能定位到指定位置,而移位时间不太耗时间
尾部插入:LinkedList尾部插入慢,因为要遍历到尾部比较耗时间,而ArrayList尾部插入快,只需根据索引就可以定
位到指定位置,而移位不太耗时间
总结:
- 在**集合**里面插入元素:首部插入,LinkedList更快;中间和尾部插入,ArrayList更快
- 在**集合**里面删除元素类似:首部删除,LinkedList更快;中间和尾部删除,ArrayList更快
因此,主要进行头部的插入和删除操作的话使用LinkedList效率更高一点,主要进行中间和尾部的插入删除操作的话使
用ArrayList效率更高一点
ArrayList是线程安全的吗?
首先说一下什么是线程安全的,线程安全指的是多线程访问时采用加锁机制,一个线程在访问该类的某个数据时,进行
保护,其它线程不能访问,直到该线程读取完,其他线程才可以使用。不会出现数据不一致或者数据污染
线程不安全就是说不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的的数据是脏数据
要分析ArrayList是否是线程安全的,我们可以从源码分析
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;
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
这里的size存储的是实际存储的元素数量,虽然说size并不是内部数组的容量,但是当size==内部数组的容量时,这个时
候我们进行add操作向集合中插入元素,在执行add操作时
实际可以分为两步:
1. 判断最小容量
2. 将指定元素添加到末尾
这时就产生了第一个线程不安全现象,在多个线程同时执行add操作时,可能会导致数组越界
举例如下:
- 假如当前线程实际元素数量比容量小1,我们假设实际元素数量是9,这时有两个线程同时执行add操作往集合中插入元
- 素,线程1往里面插入1,线程2往里面插入2
- 线程1首先检查容量是否足够,因为刚好还差一位容量才满,所以此时不用扩容,在线程1size++之前,线程2与此同时
- 也检查是否要进行扩容,线程2发现可以容纳所以不进行扩容,但是线程1这时候先执行完了elementData[size++] =
- e操作,size已经加1了,这个时候线程2再执行elementData[size++] = e操作时会产生越界异常
接下来是第二个不安全现象
elementData[size++] = e;这个操作可以分为两步
1. elementData[size] = e
2. size++
我们举个例子:
线程1要添加一个元素1,线程2要添加一个元素2,线程1先开启,线程2后开启
线程1执行elementData[size] =
e操作首先将size位置的数置成1,但是在线程1将size++之前线程2执行elementData[size] =
e操作覆盖掉了线程1添加的元素,这个时候线程2本来要添加到的元素的位置置为null
代码执行结果如下:
package 练习;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
public class Main {
public static void main(String[] args)throws Exception {
final ArrayList<Integer> list = new ArrayList<Integer>();
new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0; i < 1000; i++){
list.add(i);
//该线程将0~999添加到集合中去
}
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
).start();
new Thread(new Runnable() {
@Override
public void run() {
for(int i = 1000; i < 2000; i++)
list.add(i);
//该线程将1000~1999添加到集合中去
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
Thread.sleep(1000);
for(int i = 0; i < list.size(); i++){
System.out.println("第" + (i + 1) + "个元素为:" + list.get(i));
}
}
}
从上面的结果我们可以看出,ArrayList线程是不安全的
综上,我们可以得出结论,ArrayList是线程不安全的!如果要使用线程安全的集合,建议使用Vector集合,Vector集
合是线程安全的但是其相对于ArrayList来说,效率较低,sun公司希望Vector是线程安全的,而ArrayList是高效的,下
面是Vector集合的源码部分,可以看出,Vector之所以是线程安全的,就在于其add操作添加了synchronized(锁)
关键字
//加上了synchronized(锁)关键字,其他线程不能访问
public synchronized void addElement(E obj) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = obj;
}
除了Vector集合外,我们还可以使用如下方式保证线程安全
List list1 = new ArrayList();
List list = Collections.synchronizedList(list1);
这样得到的list也是线程安全的
注意:什么情况下不需要给ArrayList加锁呢?
1. 单线程时不需要加锁,为效率考虑!
2. 当ArrayList作为局部变量的时候不需要加锁,当ArrayList作为成员变量时被所有线程共享,作为局部变量时只
3. 被一个使用,无安全性问题,不需要加锁
如果复制一个ArrayList集合到另一个ArrayList集合中去
- 进行clone操作,因为ArrayList实现了Cloneable接口,具备了拷贝功能
- 使用ArrayLIst的构造方法,new ArrayList(Collection c)
- 使用addAll(Collection c)方法
- 使用add操作,一个一个添加
ArrayList如何做到并发修改,而不出现并发修改异常
下面是测试并发修改的代码和结果
package 源码学习.并发修改异常;
import java.util.ArrayList;
public class MyThread extends Thread{
private static ArrayList list = new ArrayList();
static{
list.add("aaaa");
list.add("bbbb");
list.add("ccc");
}
@Override
public void run(){
for(Object x : list){
System.out.println(x);
list.add("qqqqq");
}
}
}
package 源码学习.并发修改异常;
public class Main {
public static void main(String[] args) {
MyThread myThread = new MyThread();
new Thread(myThread).start();
}
}
出现了并发修改异常,为了解决这个问题,java引入了一个保证读和写都是线程安全的集合(读写分离集合):CopyOnWrit
eArrayList,所以我们只需要将ArrayList集合换成CopyOnWriteArrayList集合即可,下面是解决之后的结果
package 源码学习.并发修改异常;
import java.util.ArrayList;
import java.util.concurrent.CopyOnWriteArrayList;
public class MyThread extends Thread{
private static CopyOnWriteArrayList list = new CopyOnWriteArrayList();
static{
list.add("aaaa");
list.add("bbbb");
list.add("ccc");
}
@Override
public void run(){
for(Object x : list){
System.out.println(x);
list.add("qqqqq");
}
}
}
这样就不会引起读写并发异常了
ArrayList和LinkedList的区别
ArrayList
- 底层是基于动态数组实现的
- 对于随机访问的get和set,其效率优于LinkedList
- 对于随机操作的remove和add,ArrayList并不一定比Linked慢(ArrayList底层是动态数组,实际长度并不一定等于容量)
LinkedList
- 基于链表的数据结构
- 对于顺序操作,速度不一定比ArrayList慢
- 对于随机操作,速度明显慢于ArrayList
文章学习于:
https://blog.csdn.net/u010859650/article/details/82217791
https://www.cnblogs.com/ruoli-0/p/13714389.html
https://www.jianshu.com/p/016b3a12203e
https://www.jianshu.com/p/25aa92f8d681
https://www.jianshu.com/p/6455a4e66e14
$码字不易,如果对您有帮助的话,希望可以给我一个赞$
( ̄y▽, ̄)╭