容器主要包含两类
Collection及Map,前者存储对象,后者存储键值对。
Collection
Set:
TreeSet:基于红黑树实现,支持有序操作。如根据范围查找元素,但效率不如HashSet,复杂度为O(logN)
HashSet:基于哈希表实现,支持快速查找,不支持有序操作。不维护元素的插入先后顺序信息。
LinkedHashSet:和hashSet一样的查找效率,而且内部采用双向链表维护插入顺序。
List:
ArrayList:采用动态数组实现,支持随机访问。
Vector:与ArrayList类似,但是是线程安全的。
LinkedList:基于双向链表实现,只能顺序访问,可以实现快速插入删除,可以用作栈,队列,双向队列。
Queue:
LinkedList:用以实现双向队列
PriorityQueue:基于堆结构实现,实现优先队列。
Map
TreeMap:基于红黑树实现
HashMap:基于哈希表实现
HashTable:与HashMap类似,但是线程安全。现在使用ConcurrentHashMap代替此类。
LinkedHashMap:使用双向链表维护插入顺序,插入顺序或RLU(最近最少使用)顺序
迭代器模式
对于Collection接口的实现都可以采用迭代器进行遍历
其中,增强for就是内置了迭代器:
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
for (String item : list) {
System.out.println(item);
}
适配器模式
可以将数组转换成List
Integer[] arr = {1, 2, 3};
List list = Arrays.asList(arr);List list = Arrays.asList(1, 2, 3);
ArrayList
支持随机访问(来自RandomAccess接口),基于数组实现的。
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
数组默认大小10
private static final int DEFAULT_CAPACITY = 10;
添加元素的时候,使用EnsureCapacityInternal确保容量足够
如果容量不够,需要扩容,扩展到原来大小的1.5倍:old + (old >>1)
扩容的时候会将原来的数组复制到新的数组,势必造成耗时,所以如果预知数组大小,尽量提前指定。
删除元素,是将待删除元素后面的元素复制到当前的位置,也非常耗时:
不支持并发增删元素:
在操作前保存结构变化次数,相当于一个version。操作后比较一下,如果不匹配,将抛出ConcurrentModificationException异常。
序列化与反序列化均只涉及到已经用到的数组位置,不是将数组直接序列化:
反序列化:
序列化:
使用ObjectOutputStream包装FileOutputStream进行序列化,ObjectInputStream包装FileInputStream进行反序列化
ArrayList list = new ArrayList();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
oos.writeObject(list);
Vector
与ArrayList实现类似,但是采用Synchronized进行同步:
Vector每次扩容的大小是原来的2倍
想要实现同步的List,可以采用以下两种替代方案:
1.通过Collections的synchronizedList方法获取一个同步的List
List<String> list = new ArrayList<>();
List<String> synList = Collections.synchronizedList(list);
2. 使用concurrent包下的CopyOnWriteArrayList
List<String> list = new CopyOnWriteArrayList<>();
CopyOnWriteArrayList
实现读写分离,写操作在一个复制的数组上进行,读操作还是在原来的数组上进行。
写操作需要加锁。
写操作结束后,将原来的数组指向操作后的数组。
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();
}
}
final void setArray(Object[] a) {
array = a;
}
@
SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
return (E) a[index];
}
适合读多写少的情况。
缺点:
内存占用是原来的两倍
读取的数据实时性不够,新插入的数据是读不到的。
LinkedList
使用双向链表保存次序信息
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
}
每个链表存储着首指针和尾指针:
transient Node<E> first;
transient Node<E> last;
HashMap
内部维护着Entry数组:
transient Entry[] table;
每个Entry包含四个字段:
hashCode、next、key、value
采用拉链的方式,对于相同hash与桶模运算结果的键值对通过链表存储在相同的数组位置:
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
public final K getKey() {
return key;
}
public final V getValue() {
return value;
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry e = (Map.Entry)o;
Object k1 = getKey();
Object k2 = e.getKey();
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
Object v1 = getValue();
Object v2 = e.getValue();
if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
}
return false;
}
public final int hashCode() {
return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
}
public final String toString() {
return getKey() + "=" + getValue();
}
}
采用拉链式存储:
HashMap<String, String> map = new HashMap<>();
map.put("K1", "V1");
map.put("K2", "V2");
map.put("K3", "V3");
默认桶的大小为16,
插入K1的时候,计算位置是hash%16 位置是3
插入K2,计算位置是6
插入K3的位置也是6,这时采用头插的方式,将元素插到K2的前面。
插入元素:
键为空会强制插入到0的位置。
找出是否存在键为key的键值对,存在直接更新。否则执行头插法。
确定位置:
static int indexFor(int h, int length) {
return h & (length-1);
}
因为桶的长度是可以保证为2的指数次大小,所以上述的与运算可以代替取模运算。
扩容:
当数组元素个数达到临界,需要扩容。
容量扩大为原来的两倍:
扩容还是将原有的数组复制:
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
transfer(newTable);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;
do {
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}
从 JDK 1.8 开始,一个桶存储的链表长度大于 8 时会将链表转换为红黑树。
与HashTable相比:
HashTable 使用 synchronized 来进行同步。
HashMap 可以插入键为 null 的 Entry。
HashMap 的迭代器是 fail-fast 迭代器。
HashMap 不能保证随着时间的推移 Map 中的元素次序是不变的。
ConcurrentHashMap
与HashMap非常相似,
但采用分段锁提高并发访问的性能。
final Segment<K,V>[] segments;
默认并发数量16:
static final int DEFAULT_CONCURRENCY_LEVEL = 16;
求size的时候是把所有的分段锁里维护的键值对数量count进行累加计算得到:
static final int RETRIES_BEFORE_LOCK = 2;
public int size() {
// Try a few times to get accurate count. On failure due to
// continuous async changes in table, resort to locking.
final Segment<K,V>[] segments = this.segments;
int size;
boolean overflow; // true if size overflows 32 bits
long sum; // sum of modCounts
long last = 0L; // previous sum
int retries = -1; // first iteration isn't retry
try {
for (;;) {
// 超过尝试次数,则对每个 Segment 加锁
if (retries++ == RETRIES_BEFORE_LOCK) {
for (int j = 0; j < segments.length; ++j)
ensureSegment(j).lock(); // force creation
}
sum = 0L;
size = 0;
overflow = false;
for (int j = 0; j < segments.length; ++j) {
Segment<K,V> seg = segmentAt(segments, j);
if (seg != null) {
sum += seg.modCount;
int c = seg.count;
if (c < 0 || (size += c) < 0)
overflow = true;
}
}
// 连续两次得到的结果一致,则认为这个结果是正确的
if (sum == last)
break;
last = sum;
}
} finally {
if (retries > RETRIES_BEFORE_LOCK) {
for (int j = 0; j < segments.length; ++j)
segmentAt(segments, j).unlock();
}
}
return overflow ? Integer.MAX_VALUE : size;
}
两次读取数据一致,认为是正确的,否则执行第三次,超过三次要加锁。
JDK 1.8 使用了 CAS 操作来支持更高的并发度,在 CAS 操作失败时使用内置锁 synchronized。
LinkedHashMap
继承自HashMap,具备告诉查询性能。
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>
内部是用双向链表维护插入次序:
/**
* The head (eldest) of the doubly linked list.
*/
transient LinkedHashMap.Entry<K,V> head;
/**
* The tail (youngest) of the doubly linked list.
*/
transient LinkedHashMap.Entry<K,V> tail;
次序分为两种:
插入次序或最近最少使用次序,默认是插入次序
final boolean accessOrder;
使用以下函数维护次序:
如果是最近最少使用次序,在afterNodeAccess后会将元素插入到链表的末尾,使得最少使用的在链表头部,最近使用的在尾部
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMap.Entry<K,V> last;
if (accessOrder && (last = tail) != e) {
LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e, b = p.before, a = after;
p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a != null)
a.before = b;
else
last = b;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}
插入元素以后,检测是否移除最近不使用的节点:
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
默认情况下evict在构建Map的时候是false,其他是true
removeEldestEntry默认是false,可以通过继承LinkedHashMap覆盖方法实现
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
LRU的一种实现:
对超过3个元素的时候,移除最不常用的元素
class LRUCache<K, V> extends LinkedHashMap<K, V> {
private static final int MAX_ENTRIES = 3;
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > MAX_ENTRIES;
}
LRUCache() {
super(MAX_ENTRIES, 0.75f, true);
}
}
测试:
WeakHashMap
继承自weakReference,用以实现缓存,被其引用的对象下一次垃圾回收期间会被垃圾回收。
JVM的ConcurrentCache实现原理就是如此。
将经常使用的对象放置到eden中,使用ConcurrentHashMap实现,不会轻易被垃圾回收。
不常使用的放入longterm,使用weakHashMap实现,会被垃圾回收。
调用get时,先到eden获取,找不到回去longterm获取,获取到即放入eden中。
调用put方法,如果eden大小超过了size,会将所以的eden元素放到longterm,利用回收机制回收一部分不常用的数据。
public final class ConcurrentCache<K, V> {
private final int size;
private final Map<K, V> eden;
private final Map<K, V> longterm;
public ConcurrentCache(int size) {
this.size = size;
this.eden = new ConcurrentHashMap<>(size);
this.longterm = new WeakHashMap<>(size);
}
public V get(K k) {
V v = this.eden.get(k);
if (v == null) {
v = this.longterm.get(k);
if (v != null)
this.eden.put(k, v);
}
return v;
}
public void put(K k, V v) {
if (this.eden.size() >= size) {
this.longterm.putAll(this.eden);
this.eden.clear();
}
this.eden.put(k, v);
}
}