概述
java的集合类在工作中非常的常用,可是大部分人可能工作个5年都只用过 ArrayList,HashMap 也从来不懂里面的原理到底是什么样的。这次就来整体的梳理一下java集合类的相关知识点。
知识图谱
先来看看大概有哪些常用的类需要了解
java的集合类可以大体的分为 Collection 和 Map . 可以笼统的理解为两个都是存放数据的容器。只不过Collection存放的是用户自定义的数据
public interface Collection<E> extends Iterable<E> {
}
而Map里面存放的都是 Entry, 然后 Entry包含了两个用户自定义的 key和value。
Collection
Collection是一个接口,继承于 Iterable接口。Iterable接口属于迭代器接口,其设计思想是每个集合的数据结构是不同的,用户是不知道如何正确的遍历该容器,所以抽象出一个Iterator接口,然后作为实现容器需要在容器内部自己写一个Iterator实现类完成遍历
Collection接口只提供
add和remove操作 ,没有提供 get 操作。因为不同结构的集合get方式不能统一,只能交给更具体的子类来定义get方法。例如线性结构的 List 就可以提供 RandomAccess,而非线性结构 Set就做不到。下图显示了List,Set,Queue在接口层面对于Collection的扩展
Map
Map 往下并没有什么扩展的接口直接就是实现类了。 可以理解为Map是一种特殊的Collection,里面存放的都是Entry。
Iterator 遍历器
我们先来看看 Iterator 的接口定义
public interface Iterator<E> {
# 是否有下一个
boolean hasNext();
# 返回下一个数据
E next();
# 删除当前数据
default void remove() {
throw new UnsupportedOperationException("remove");
}
}
相信我们在学习java的时候就听过不要在for循环的时候删除元素,需要用 Iterator 来删除,但是知道其中原理的人并不是很多。
我们拿ArrayList来举例子.看看为什么不能在for循环的时候删除元素。先看代码,我进行了简化
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
# 这个属性在父类AbstractList中,我拿出来好看
protected transient int modCount = 0;
# 装数据的容器
transient Object[] elementData;
public Iterator<E> iterator() {
return new Itr();
}
# 就像我上文中说的,每个容器都有自己的迭代器实现
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;
public E next() {
checkForComodification();
int i = cursor;
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
cursor = i + 1;
return (E) elementData[lastRet = i];
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
}
这里有几个关键的变量
- modCount 修改次数
- cursor 当前迭代器的指针
- lastRet 当前迭代器指针的前一个位置
- expectedModCount 期望的修改次数
我们看来看这些变量是怎么工作的: 假设现在有一个容器里面装着 A B C D四个数据。
- 我创建了一个迭代器。这时候迭代器的 cursor 指向 A 的坐标, 当我调用了 next方法的时候 cursor和lastRet往前挪动了一格,然后把lastRet指向的A返回给用户。
- 我调用了集合的删除方法删除了A元素,看代码可知删除元素后,整个数组会移动,所以现在容器内部结构变成 B C D
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
}
- 原本我已经取出A,然后在B的位置,但是我删除了A之后,再想取B的时候其实取出了C。
这就造成了数据的错误,但是如果这个操作删除的是B之后的数据,那么数据又不会错误。
这种行为叫做 结果不可预期行为或者未决行为
未决行为的解决
我们已经知道了在迭代器遍历的时候,如果调用了集合的删除元素方法,可能造成不可预期的错误。有的人可能会想,那么在删除的时候把指针往后挪一格是不是就不会出错了呢?答案是没错的。在迭代器中删除元素之后就会把指针再指回去,所以并不是遍历不能删除元素,而是不能删除元素的时候迭代器感知不到,这点很重要,因为在 ArrayBlockingQueue中的容器删除元素的方法里面主动的调用了所有的迭代器。那么就可以在遍历的时候调用容器的删除方法。
public void remove() {
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
ArrayBlockingQueue 是在容器的层面解决这个问题,我们来看看在迭代器的角度如何解决这个未决行为。 我们再来回头看看迭代器的几个参数
- modCount 修改次数
- cursor 当前迭代器的指针
- lastRet 当前迭代器指针的前一个位置
- expectedModCount 期望的修改次数
当创建迭代器的时候 expectedModCount = modCount, 迭代器记录了当前容器的修改次数。然后在容器操作数据的时候都会 modCount++。当迭代器执行 next方法遍历的时候,都会
checkForComodification判断我创建的时候的 modCount和我现在正在遍历的容器的modCount是否相等,如果不相等,那么就说明已经在我不知道的时候进行了修改,不论这个修改会不会产生影响,我都直接报错,这就是Fail-fast的思想