Java基础之Collection
Collection集合
一、Collection集合概述
java中的集合类大概分为了单例和双列的。至于为什么不能有三列的或者是多列的,因为双列中已经满足了多例的使用方式。
首先看下单列的集合体系图
Collection集合类是一个父接口,里面定义了子类中一定要实现的方法。对于两个子接口:list和set接口来说,各有各的特点和优势。
对于List集合来说,元素是可以重复的;对于set接口来说,规定元素是不可以有重复的。
比较常用的方法:
public boolean add(E e): 把给定的对象添加到当前集合中
public void clear() :清空集合中所有的元素。
public boolean remove(E e): 把给定的对象在当前集合中删除。
public boolean contains(Object obj): 判断当前集合中是否包含给定的对象。
public boolean isEmpty(): 判断当前集合是否为空。
public int size(): 返回集合中元素的个数。
public Object[] toArray(): 把集合中的元素,存储到数组中
直接去查看两个重要子类的实现方式,在此之前,首先查看下子类中的实现。
二、ArrayList类
2.1、成员属性
//序列化版本号(类文件签名),如果不写会默认生成,类内容的改变会影响签名变化,导致反序列化失败
private static final long serialVersionUID = 8683452581122892189L;
//如果实例化时未指定容量,则在首次添加元素时会进行扩容使用此容量作为数组长度
private static final int DEFAULT_CAPACITY = 10;
/** static修饰,所有的未指定容量的实例(也未添加元素)共享此数组,两个空的数组有什么区别呢?
就是第一次添加元素时知道该 elementData 从空的构造函数还是有参构造函数被初始化的。
以便确认如何扩容。空的构造器则初始化为10,有参构造器则按照扩容因子1.5倍扩容
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 在进行序列化的时候,如果不需要来进行序列化,那么可以在属性前面加上这个关键字
// 真正保存元素的容器
transient Object[] elementData;
// 容器中元素的个数
private int size;
注意:elementData.length()和size的区别
elementData长度可以是10,如果没有元素填充,那么全部都是null;假如说,elementData.length=10,而只有一个元素,那么此时的size就为1。
2.2、构造方法
无参构造:
public ArrayList() {
// 指向默认空数组
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
有参构造1:
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
// 创建一个Object类型的数组
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
// 如果是0,那么使用不是默认的空数组
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
}
}
有参构造2:
将Collection集合类的子类转换成ArrayList类。注意这里的泛型,需要和现有的数据类型的保持一致。
public ArrayList(Collection<? extends E> c) {
// 1、首先转换成一个数组
Object[] a = c.toArray();
// 2、1判断数组长度
if ((size = a.length) != 0) {
// 3、判断原来的数据集合类型是否是ArrayList类型
if (c.getClass() == ArrayList.class) {
// 3.1、如果是的话,那么就直接进行赋值即可
elementData = a;
} else {
// 如果不是ArrayList数据类型的,那么需要来保持一致
elementData = Arrays.copyOf(a, size, Object[].class);
}
// 如果长度不为0,那么转换过来的肯定是一个空数组,直接赋值为空值即可
} else {
// replace with empty array.
elementData = EMPTY_ELEMENTDATA;
}
}
2.3 、扩容机制
扩容方式针对的是无参构造和有参构造中指定了容量的构造方式来进行操作的。
2.3.1、无参构造进行扩容
无参构造在创建新集合的时候,是没有指定容量的,那么此时此刻容量就是为0,也就是size是为0的,elementData指向的是一个空数组而已。
只有在进行add的时候,才会真的来进行elementData的初始化操作。下面来看下
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!! 增加操作次数,这里为错误机制提供了一种方式
elementData[size++] = e;
return true;
}
首先需要调用的是内部的方式ensureCapacityInternal,确保内部容量,size此时是为0的,那么size+1意味着要向其中来尝试添加一个元素。
看下具体方法:
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
// 计算当前集合保存数据集合的容量。
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 是否保存集合的数组指向了一个空数组
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 默认容量和传入进来的容量来进行比较,取出来最大值
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
// 不是空数组,那么就继续执行下面逻辑
return minCapacity;
}
确保明确的容量方法:
private void ensureExplicitCapacity(int minCapacity) {
// 每一次操作,这里都会自增+1
modCount++;
// 如果尝试添加后的容量是大于当前当前容器的容量的,那么需要来进行扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
这里首先不需要来看扩容的方法,默认就是小于当前的容量。那么继续,看下面怎么添加的。
小于当前数组容量的添加方法:
public boolean add(E e) {
ensureCapacityInternal(size + 1);
// 首先将添加的元素放入到elementData[size]=e上,然后size自增,也就是游标向后移动一位
elementData[size++] = e;
return true;
}
返回添加为true。
那么如果说添加的容量大于了集合类中的容器的容量,那么这里来执行对应的步骤:
private void ensureExplicitCapacity(int minCapacity) {
// 每一次操作,这里都会自增+1
modCount++;
// 如果尝试添加后的容量是大于当前当前容器的容量的,那么需要来进行扩容
// 注意这里相等的时候,不需要来进行扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
那么看一下grow方法:
private void grow(int minCapacity) {
// 计算当前数组的长度
int oldCapacity = elementData.length;
// 这里体现出来了扩容机制是1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 如果扩容后依然小于最小值,那么依然是使用最小的
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 如果达到了最大值,那么依然是最小的那个值
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 最终采用提供的扩容机制来进行实现。将最终的数组来进行返回
// 在这里已经将原来的值都来进行了copy
elementData = Arrays.copyOf(elementData, newCapacity);
}
然后再在尾部来进行追加。
注意:第一次由0到10,接下来的扩容才是真的1.5倍扩容。扩容创建一个数组,然后将原来数组中的值按照原来的顺序来进行排列,然后预留剩下的空数组容量交给后来需要添加的元素预留位置。
2.3.2、有参构造进行扩容
有参构造也需要走原来的逻辑。只不过有一点是有点差距的:
// 计算当前集合保存数据集合的容量。
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 是否保存集合的数组指向了一个空数组
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 默认容量和传入进来的容量来进行比较,取出来最大值
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
// 不是空数组,那么就继续执行下面逻辑
return minCapacity;
}
这里执行的是直接返回的最小容量,因为elementData不再指向原来的空数组了。那么接下来走的就是原来的逻辑了。
2.3.3 、总结图
扩容方法是add方法引起的。最终将上述叙述总结一张图:
2.4、重点方法
2.4.1、add方法
add刚刚说了一个最原始的方式,还是在尾部来进行追加的。那么现在的添加方法是在数组中间来进行添加:
add(int index, E element)
public void add(int index, E element) {
// 范围检查,超出范围,索引越界异常
rangeCheckForAdd(index);
// 尝试添加判断剩余容量是否是足够的
ensureCapacityInternal(size + 1); // Increments modCount!!
// 开始进行拷贝,然后将添加位置和添加位置之后的元素每个向后移动一个位置
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
// 在原来的值上赋添加进来的值
elementData[index] = element;
size++;
}
还有另外两种添加方法,都可以看一下:
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);
size += numNew;
return numNew != 0;
}
直接将一个集合中的元素添加到原数组的尾部:
测试一下:
@Test
public void testAddAll1(){
List<Integer> integers1 = new ArrayList<>();
integers1.add(1);
integers1.add(2);
List<Integer> integers2 = new ArrayList<>();
integers2.add(3);
integers2.add(4);
System.out.println(integers1);
integers1.addAll(integers2);
System.out.println(integers1);
}
查看控制台输出:
[1, 2]
[1, 2, 3, 4]
再看另外一种,在指定位置上插入对应的集合
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;
if (numMoved > 0)
System.arraycopy(elementData, index, elementData, index + numNew,
numMoved);
System.arraycopy(a, 0, elementData, index, numNew);
size += numNew;
return numNew != 0;
}
测试类:
@Test
public void testAddAll2(){
List<Integer> integers1 = new ArrayList<>();
integers1.add(1);
integers1.add(2);
List<Integer> integers2 = new ArrayList<>();
integers2.add(3);
integers2.add(4);
System.out.println(integers1);
integers1.addAll(1,integers2);
System.out.println(integers1);
}
查看控制台:
[1, 2]
[1, 3, 4, 2]
2.4.2、remove方法
删除掉指定下标的方法:
public E remove(int index) {
// 检查坐标是否大于等于当前size,因为坐标和当前索引相比是为1的
rangeCheck(index);
// 操作次数+1
modCount++;
// 将旧值先进行保存,然后需要进行返回
E oldValue = elementData(index);
// 计算需要将删除的坐标向前移动多少次
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 将原来的的引用全部置空,GC回收
elementData[--size] = null; // clear to let GC do its work
// 返回旧值
return oldValue;
}
再看一个:
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
// 从这里可以看到使用的是equals方法,那么在删除的时候,需要使用到对象的重写的equals方法
if (o.equals(elementData[index])) {
// 和上面逻辑一致
fastRemove(index);
return true;
}
}
return false;
}
可以看到如果删除成功,返回true;删除失败,说明没有这个值,无法来进行删除。
2.4.3、set方法
最近用到了这个方法,用来修改指定下表的元素。
public E set(int index, E element) {
// 检查坐标
rangeCheck(index);
// 拿到旧值
E oldValue = elementData(index);
// 然后将新值放到原来位置上
elementData[index] = element;
return oldValue;
}
2.4.4、查询方法
这个一般来说也是常用的
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
2.4.5、遍历方法
public void forEach(Consumer<? super E> action) {
// 首先判断操作
Objects.requireNonNull(action);
// 不希望在遍历的时候进行其它操作
final int expectedModCount = modCount;
@SuppressWarnings("unchecked")
final E[] elementData = (E[]) this.elementData;
final int size = this.size;
// 不断的进行判空操作,然后消费每个数据
for (int i=0; modCount == expectedModCount && i < size; i++) {
action.accept(elementData[i]);
}
if (modCount != expectedModCount) {
// 最终判断,如果不行,那么抛出异常
throw new ConcurrentModificationException();
}
}
2.4.6、clear方法
这个方法有点意思的
public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
可以看到这里是将原来的元素全部置空,然后size变为0.但是数组的长度是没有发生变化的。这里的长度指的是数组的长度,而不是有效元素的个数的长度。
2.5、迭代器
public Iterator<E> iterator() {
return new Itr();
}
private class Itr implements Iterator<E> {
// 代表下一个要访问的元素下标
int cursor;
// 代表上一个要访问的元素下标
int lastRet = -1;
// 代表对 ArrayList 修改次数的期望值,初始值为 modCount
int expectedModCount = modCount;
// 如果下一个元素的下标等于集合的大小 ,就证明到最后了
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
// 判断expectedModCount和modCount是否相等,ConcurrentModificationException
checkForComodification();
int i = cursor;
// 对cursor进行判断,看是否超过集合大小和数组长度
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
// 自增1。开始时,cursor=0,lastRet=-1;每调用一次next方法,cursor和lastRet都会自增1。
cursor = i + 1;
// 将cursor赋值给lastRet,并返回下标为lastRet的元素
return (E) elementData[lastRet = i];
}
public void remove() {
// 判断lastRet的值是否小于0
if (lastRet < 0)
throw new IllegalStateException();
// 判断expectedModCount和modCount是否相等,ConcurrentModificationException
checkForComodification();
try {
// 直接调用ArrayList的remove方法删除下标为lastRet的元素
ArrayList.this.remove(lastRet);
// 将lastRet赋值给curso
cursor = lastRet;
// 将lastRet重新赋值为-1,并将modCount重新赋值给expectedModCount。
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
小结:
1、在创建ArrayList集合对象后,调用iterator()方法时,会创建Itr迭代器对象。此时Itr对象中有一个最重要的属性:expectedModCount,这个属性用来记录迭代器在遍历过程中,是否存在迭代器之外的第三方来操作这里的List集合对象。一旦造成修改,将会导致expectedModCount和modCount的值不同,从而造成并发修改异常。
2、遍历的时候每次都会来进行检查!可以在下面的问题中找到这里的答案。
2.6、常见问题和解决方式
并发修改异常
情景一:迭代器+集合修改
直接看下代码,如下所示:
public static void main(String[] args) {
List<String> platformList = new ArrayList<>();
platformList.add("博客园");
platformList.add("CSDN");
platformList.add("掘金");
for (String platform : platformList) {
if (platform.equals("博客园")) {
platformList.remove(platform);
}
}
System.out.println(platformList);
}
抛出并发修改异常:
首先看下上面这段代码生成的字节码,如下所示:
通过分析源代码可以发现,在迭代器进行遍历的时候,又利用了platformList进行了修改。那么此时迭代器中的expectedModCount和list集合中的modCount的值已经不同了,在下一次进行var2.hasNext()方法调用的时候,可以看到再次检查迭代器中的expectedModCount和list集合中的modCount的值已经不同了,这个时候会爆出异常信息。
情景二:多线程操作
那么我们写个代码来实验一下:
@Test
public void testFastFail(){
List<String> stringList = new ArrayList<>();
for (int i = 0; i < 2; i++) {
new Thread(()->{
for (int i1 = 0; i1 < 10000; i1++) {
stringList.add(Thread.currentThread().getName());
}
}).start();
}
try {
Thread.sleep(1000);
System.out.println(stringList.size());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
输出结果:
16864
从这里看出来,已经缺少值了,因为少添加了。那么这里就会出现着问题。
那么如何才能体现出来对应的异常信息呢?可以看到查询里面都有这个异常,那么在添加的时候看一下:
@Test
public void testFastFail(){
List<String> stringList = new ArrayList<>();
for (int i = 0; i < 2; i++) {
new Thread(()->{
for (int i1 = 0; i1 < 10; i1++) {
stringList.add(Thread.currentThread().getName());
}
}).start();
}
stringList.forEach(System.out::println);
try {
Thread.sleep(1000);
System.out.println(stringList.size());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
查看控制台输出:
Thread-2
java.util.ConcurrentModificationException
at java.util.ArrayList.forEach(ArrayList.java:1262)
可以看得到在遍历的时候,检测出来
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
期望操作的线程和当前的线程不一致,原本希望的是当前的线程来进行操作。但是有别的线程来进行入侵了,所以就会有这种结果的产生。
而在上面分析的过程中,可以看到add、remove都会涉及到对modCount的值,这才是涉及到根本原因的地方。
解决方式
1、当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。
例如:当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。
2、通过上面的现象来说明的话,不止这一种情况。还有在使用迭代器操作的同时,又使用了List来操作集合中的数据。
这两种情况都会造成出现并发修改异常。
1、Collections提供的同步list机制。解决多线程情况下
List<String> arrList = Collections.synchronizedList(new ArrayList<>());
2、CopyOnWriteArrayList
List<String> list = new CopyOnWriteArrayList<>();
3、Vector类【不推荐】
List<String> arrList = new Vector<>();
但是这个类在使用上一定要注意添加上全局锁!!!对于Vector来说使用不当也会造成线程不安全线程。
但是对于ArrayList来说,加锁也可以保证线程安全,所以综合来说,使用起来比较鸡肋。
4、只使用迭代器方法
public static void main(String[] args) {
List<String> platformList = new ArrayList<>();
platformList.add("博客园");
platformList.add("CSDN");
platformList.add("掘金");
Iterator<String> iterator = platformList.iterator();
while (iterator.hasNext()) {
String platform = iterator.next();
if (platform.equals("博客园")) {
iterator.remove();
}
}
System.out.println(platformList);
}
能够解决问题的原因在于:
可以看出,每次删除一个元素,都会将modCount的值重新赋值给expectedModCount,这样2个变量就相等了,不会触发java.util.ConcurrentModificationException异常。
5、使用removeIf()方法(JDK8推荐)
从JDK1.8开始,可以使用removeIf()方法来代替 Iterator的remove()方法实现一边遍历一边删除,其实,IDEA中也会提示:
看下removeIf()方法的源码,会发现其实底层也是用的Iterator的remove()方法:
不可变集合
Collections.unmodifiableList可以将list封装成不可变集合(只读),但实际上会受源list的改变影响
public void unmodifiable() {
// 缓存不可变配置
List list = new ArrayList(Arrays.asList(4,3,3,4,5,6));
// 只读
List modilist = Collections.unmodifiableList(list);
// 会报错UnsupportedOperationException
// modilist.set(0,1);
// modilist.add(5,1);
// 但是修改原来集合确实可以的
list.set(0,1);
// 打印1
System.out.println(modilist.get(0));
}
不能够使用modilist来做修改,但是可以使用list(原集合)操作集合中的数据。
Java中比较常见的使用方式: Collections.unmodifiableList(list)。
创建一个不可变的集合,然后将这个集合暴露出去,只给外部做遍历使用。也就是只读模式
Arrays.asList
public void testArrays(){
long[] arr = new long[]{1,4,3,3};
List list = Arrays.asList(arr);//基本类型不支持泛型化,会把整个数组当成一个元素放入新的数组,传入可变参数
System.out.println(list.size());//打印1
}
//可变参数
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
基本类型不支持泛型化,会把整个数组当成一个元素放入新的数组,传入可变参数,因此size打印结果是1。
解决输出为1的问题
long[] arr = new long[]{1l,4l,3l,3l};
List list = Arrays.asList(arr);
System.out.println(list.get(0));
只需要将上面的代码做修改,从基本类型修改成引用类型即可:
Long[] arr = new Long[]{1L,4L,3L,3L};
// 基本类型不支持泛型化,会把整个数组当成一个元素放入新的数组,传入可变参数
List list = Arrays.asList(arr);
// 打印4
System.out.println(list.size());