容器Collection & Map
容器主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。容器中使用两种设计模式,适配器模式
和迭代器模式
Conllection
- Set
- HashSet : 基于哈希表实现,支持快速查找,但不支持有序性操作,并且失去了元素的插入顺序信息。
- LinkedHashSet : 具有HashSet的查找效率,且内部使用双向链维护元素的插入顺序。
- TreeSet : 基于红黑树,支持有序性操作,例如根据一个范围查找元素操作。但是操作效率不如HashSet,HashSet查找的时间复杂度为O(1),TreeSet为O(logN).
- List
- ArrayList : 基于动态数组实现,支持随机访问
- Vector : 和ArrayList 类似,但它是线程安全的 Synchronized
- LinkedList : 基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList 还可以用作栈、队列、和双向队列。
-Queue - LinkedList : 可以用它来实现双向队列
- PriorityQueue: 基于堆结构实现,可以用它来实现优先队列。
Map
- TreeMap : 基于红黑树实现。
- HashMap : 基于哈希表实现。
- HashTable : 和HashMap 类似,但它是线程安全的,现在可以使用ConcurrentHashMap来支持线程安全,ConcurrentHashMap 引入了分段锁,(Segment)
- LinkedHashMap:使用双向链表来维护元素的顺序,顺序为插入顺序或者最近最少使用(LRU)顺序。
源码分析
以下源码分析基于JDK 1.8 。
ArrayList
在ArrayList 实现了 RandomAccess 接口,实现该接口就表示支持随机访问。毕竟是基于数组实现的。
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
扩容
要说分析ArrayList源码,一般考点都在于它的扩容机制
- add()方法,即添加元素时,
ensureCapacityInternal()
方法保证容量足够,如果容量不够时,需要使用grow()方法进行扩容,新容量的大小为oldCapacity + (oldCapacity >> 1)
,也就是原来的 1.5 倍。 - 扩容操作需要调用
Arrays.copyOf()
把原数组整个复制到新数组中,这个操作代价很高,因此最好在创建 ArrayList 对象时就指定大概的容量大小,减少扩容操作的次数。 - ArrayList 初始数组容量为10
private static final int DEFAULT_CAPACITY = 10;
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
↓↓↓↓↓
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
↓↓↓↓↓
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
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) // 如果扩容超过最大Size,就设置成为Integer.MAX_VALUE
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
删除元素
对于数组而已,随机访问时优点,缺点也会明显,比如说插入和删除时。删除一个元素时,要让该元素后面的元素,都向前移动一位,这个操作非常耗时。看看源码怎么做的。
- 调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,该操作的时间复杂度为 O(N),可以看出 ArrayList 删除元素的代价是非常高的。
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
return oldValue;
}
Fail-Fast(快速失败)
注意,迭代器的快速失败行为无法得到保证,因为一般来说,不可能对是否出现不同步并发修改做出任何硬性保证。快速失败迭代器会尽最大努力抛出 ConcurrentModificationException。因此,为提高这类迭代器的正确性而编写一个依赖于此异常的程序是错误的做法:迭代器的快速失败行为应该仅用于检测 bug。
“快速失败”也就是fail-fast,它是Java集合的一种错误检测机制。当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast机制。记住是有可能,而不是一定。
Fail-Fast产生原因
两个线程,一个线程在对 collection 进行迭代时,一个线程对该 collection 在结构上对其做了修改,这时迭代器就会抛出 ConcurrentModificationException
异常信息,从而产生 Fail-Fast。
根据上面这句话,知道Fail-Fast是在操作迭代器时产生的。现在我们来看看ArrayList中迭代器的源代码:
- 迭代器在调用next()、remove()方法时都是调用checkForComodification()方法
- 方法主要就是检测 modCount == expectedModCount ? 若不等则抛出ConcurrentModificationException 异常,从而产生Fail-Fast机制。
private class Itr implements Iterator<E> {
int cursor;
int lastRet = -1;
int expectedModCount = ArrayList.this.modCount; // 注意这里,expectedModCount 初始为modCount
public boolean hasNext() {
return (this.cursor != ArrayList.this.size);
}
public E next() {
checkForComodification();
/** 省略此处代码 */
}
public void remove() {
if (this.lastRet < 0)
throw new IllegalStateException();
checkForComodification();
/** 省略此处代码 */
}
final void checkForComodification() {
if (ArrayList.this.modCount == this.expectedModCount)
return;
throw new ConcurrentModificationException();
}
}
expectedModCount
是在Itr中定义的int expectedModCount = ArrayList.this.modCount;
所以他的值是不可能会修改的.modCount
是在AbstractList
中定义的,为全局变量ArrayList
中无论add、remove、clear方法只要是涉及了改变ArrayList
元素的个数的方法都会导致modCount
的改变
protected transient int modCount = 0;
根据上面的分析,可知 Fail-Fast本质是迭代器的`expectedModCount和List的modCount不一致。
参考文章
↓↓↓↓
序列化
还有一个关于ArrayList
的考察点呢,就是在ArrayList
序列化时,是怎么处理没有存放元素的数组的。针对源码来看看,怎么实现ArrayList
序列化和反序列化的。
- 保存元素的数组
elementData
使用transient
修饰,该关键字声明数组默认不会被序列化。
transient Object[] elementData; // non-private to simplify nested class access
ArrayList
实现了writeObject()
和readObject()
来控制只序列化数组中有元素填充那部分内容。writeObject()
方法可以看出,只是把当前数组存放元素序列化。- 序列化时需要使用
ObjectOutputStream
的writeObject()
将对象转换为字节流并输出。而writeObject()
方法在传入的对象存在writeObject()
的时候会去反射调用该对象的writeObject()
来实现序列化。反序列化使用的是ObjectInputStream
的readObject()
方法,原理类似。 - 更多关于序列化点击→ Java 如何自定义序列化和反序列化
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
elementData = EMPTY_ELEMENTDATA;
// Read in size, and any hidden stuff
s.defaultReadObject();
// Read in capacity
s.readInt(); // ignored
if (size > 0) {
// be like clone(), allocate array based upon size not capacity
ensureCapacityInternal(size);
Object[] a = elementData;
// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
a[i] = s.readObject();
}
}
}
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();
// Write out size as capacity for behavioural compatibility with clone()
s.writeInt(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();
}
}
Vector
Vector 与ArrayList 类似,但Vector使用synchronized 进行同步。所有它是线程安全的,
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
public synchronized E get(int index) {
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);
return elementData(index);
}
与ArrayList 的比较
- Vector 是同步的,因此开销比ArrayList 开销要大,访问速度慢。
- Vector 扩容是原来的2倍,ArrayList 是原来的1.5倍
如果要使用线程安全的List ,不建议使用Vector ,
- 可以通过
Collections.synchronizedList();
得到一个线程安全的ArrayList 。
List<String> list = new ArrayList<>();
List<String> synList = Collections.synchronizedList(list);
- 也可以使用
concurrent
并发包下的CopyOnWriteArrayList
类。
List<String> list = new CopyOnWriteArrayList<>();
CopyOnWriteArrayList
CopyOnWriteArrayList
相当于线程安全的ArrayList
,其中所有可变操作(add、set等等)都是通过对底层数组进行一次新的复制来实现的。上文有提到过Fail - Fast
,CopyOnWriteArrayList
,就可避免这种情况。
先看下构造函数的源码
- 使用volatile修饰符的Object数组保存容器元素。
- 构造函数中都会根据参数值重新生成一个新的数组。
final transient ReentrantLock lock = new ReentrantLock();
private transient volatile Object[] array; // volatile 保证可见性
final Object[] getArray() {return array;}
final void setArray(Object[] a) {array = a; }
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
public CopyOnWriteArrayList(Collection<? extends E> c) {
Object[] elements;
if (c.getClass() == CopyOnWriteArrayList.class)
elements = ((CopyOnWriteArrayList<?>)c).getArray();
else {
elements = c.toArray();
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elements.getClass() != Object[].class)
elements = Arrays.copyOf(elements, elements.length, Object[].class);
}
setArray(elements);
}
怎么保证线程安全的呢,看add
方法,应该可以理解大半。
add
会加独占锁lock
,其他线程若要调用add方法必须等待释放锁之后,才能获取锁。这样可以防止多线程同时修改数据- 每次添加后,都会拷贝一个新的数组,然后调用
setArray()
方法,更新上文说的由Volatile
修饰的Object
数组,由于Volatile
可以保证可见性,所以,其他线程能第一时间看到新增的元素 。
public boolean add(E e) {
final ReentrantLock lock = this.lock; // 获取独占锁
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
// 重新生成一个新的数组实例,并将原始数组的元素拷贝到新数组中
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e; // 添加新的元素到新数组的末尾
setArray(newElements); // 更新底层数组
return true;
} finally {
lock.unlock();
}
}
有添加,就有删除,继续啃下删除源码
public E remove(int index) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
E oldValue = get(elements, index); // 获取volatile数组中指定索引处的元素值
int numMoved = len - index - 1;
if (numMoved == 0) // 如果被删除的是最后一个元素,则直接通过Arrays.copyOf()进行处理,而不需要新建数组
setArray(Arrays.copyOf(elements, len - 1));
else {
Object[] newElements = new Object[len - 1];
System.arraycopy(elements, 0, newElements, 0, index); // 拷贝删除元素前半部分数据到新数组中
System.arraycopy(elements, index + 1, newElements, index, numMoved);// 拷贝删除元素后半部分数据到新数组中
setArray(newElements); // 更新volatile数组
}
return oldValue;
} finally {
lock.unlock();
}
}
既然CopyonWriteArrayLis
t,是一个线程安全的ArrayList
,那肯定的满足随机访问。而且实现了RandomAccess
- 底层存储结构用的数组实现,所有随机访问时肯定的。
public E get(int index) {
return get(getArray(), index);
}
private E get(Object[] a, int index) {
return (E) a[index];
}
参考文章
↓↓↓↓
https://blog.youkuaiyun.com/mazhimazh/article/details/19210547