Java集合
Java常用集合类
Java中的集合分别在Collection、Map接口下
Collection接口
可以看到我们常用的List,Queue,Set
List
常用的类有ArrayList, Vector, LinkenList
ArrayList
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
private static final long serialVersionUID = 8683452581122892189L;
private static final int DEFAULT_CAPACITY = 10;
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData; // non-private to simplify nested class access
private int size;
/**
* Constructs an empty list with the specified initial capacity.
*
* @param initialCapacity the initial capacity of the list
* @throws IllegalArgumentException if the specified initial capacity
* is negative
*/
可以看到,DEFAULT_CAPACITY = 10;
这是默认容量,初始容量是多少呢?
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
得到答案 : 0
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
可以指定初始容量。
接下来我们看看扩容的情况
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);
}
如果是无参方法new的ArrayList,那么第一次扩容数组长度是default,刚才看到了,是10.
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
看,数组长度不够了,需要grow(minCapacity)。
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
newCapacity = oldCapacity + (oldCapacity >> 1);
我们可以得知,无参情况下数扩容情况是0->
10->
15->
22->
33->
49…
ok, ArrayList暂时到此为止。
Vector
众所周知,Vector是线程安全的,那么他是怎么实现的呢?让我们来看看。
public synchronized void addElement(E obj) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = obj;
}
有答案了,使用了synchronized关键字同步。单线程情况下,效率肯定不如ArrayList,至少现在为止我几乎没有使用过Vector。据了解,并发情况下,CopyAndWriteArrayList使用较多,原因可以在后续学习中探究。
LinkedList
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
transient int size = 0;
/**
* Pointer to first node.
* Invariant: (first == null && last == null) ||
* (first.prev == null && first.item != null)
*/
transient Node<E> first;
/**
* Pointer to last node.
* Invariant: (first == null && last == null) ||
* (last.next == null && last.item != null)
*/
transient Node<E> last;
/**
* Constructs an empty list.
*/
成员变量只有三个,可以看出这是一个双向链表。
对比
ArrayList,动态数组,增删效率低,查询效率高。
LinkedList,双向链表,增删效率高,查询效率低。
Vector,应该是在ArrayList出现之前比较常用,线程安全。
Set
比较常用的是HashSet, TreeSet
HeshSet
看看成员变量
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
{
static final long serialVersionUID = -5024744406713321676L;
private transient HashMap<E,Object> map;
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
底层应该是HashMap, 继续往下看吧。
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
看来是这样,意思就是Hashmap的Value不用,不看了,一会看HashMap吧。
TreeSet
呃,怎么没找到,哪去了。。。
public class TreeSet<E> extends AbstractSet<E>
implements NavigableSet<E>, Cloneable, java.io.Serializable
{
呃,NavigableSet,这是什么?
天呐、、
public interface NavigableSet<E> extends SortedSet<E> {
public interface SortedSet<E> extends Set<E> {
意思是有序集合。据说这个扩展接口1.2的时候就有,到了1.6才实现。
又查了一下,呃,就先也理解成有序的吧。
NavigableSet声明了什么方法呢?
E lower(E e);
E floor(E e);
E ceiling(E e);
E higher(E e);
E pollFirst();
E pollLast();
Iterator<E> iterator();
NavigableSet<E> descendingSet();
Iterator<E> descendingIterator();
NavigableSet<E> subSet(E fromElement, boolean fromInclusive,
E toElement, boolean toInclusive);
NavigableSet<E> headSet(E toElement, boolean inclusive);
NavigableSet<E> tailSet(E fromElement, boolean inclusive);
SortedSet<E> headSet(E toElement);
SortedSet<E> tailSet(E fromElement);
注释就不贴了,太长了
floor(E e) 方法返回在这个集合中小于或者等于给定元素的最大元素,如果不存在这样的元素,返回null;
ceiling(E e) 方法返回在这个集合中大于或者等于给定元素的最小元素,如果不存在这样的元素,返回null;
higher(E e)方法用来返回最小元素在这组严格大于给定的元素,或者null,如果不存在这样的元素。
lower…
以上解释是copy其他博客的,看着好累,举例子吧,我的猜测是这样的,如果集合中有1,2,3,4,5这几个元素,那么
floor(3) = 3
ceiling(3) = 3
lower(3) = 2
higher(3) = 4
读到这里是不是恍然大悟,然而以上是我猜的,我确认一下。
No problem!剩下的方法都比较好理解了,开始看TreeSet吧。
接下来老规矩,看成员变量。
public class TreeSet<E> extends AbstractSet<E>
implements NavigableSet<E>, Cloneable, java.io.Serializable
{
/**
* The backing map.
*/
private transient NavigableMap<E,Object> m;
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
就俩,看来是基于TreeSet实现的啦。
LinkedHashSet
这个东西,应该是双向链表和哈希实现的,可以用它实现LRU。看看源码吧。
package java.util;
public class LinkedHashSet<E>
extends HashSet<E>
implements Set<E>, Cloneable, java.io.Serializable {
private static final long serialVersionUID = -2851667679971038690L;
public LinkedHashSet(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor, true);
}
public LinkedHashSet(int initialCapacity) {
super(initialCapacity, .75f, true);
}
public LinkedHashSet() {
super(16, .75f, true);
}
@Override
public Spliterator<E> spliterator() {
return Spliterators.spliterator(this, Spliterator.DISTINCT | Spliterator.ORDERED);
}
}
这啥也没有啊,这怎么用的双向链表呢?
本列文虎克终于找到了答案。
public LinkedHashSet() {
super(16, .75f, true);
}
点进super
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
好吧,Set应该都是基于map实现的。那可以去看Map了。
Map接口
这一大堆,要挑重点看。
HashMap
这个我平时是最常用的。
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
private static final long serialVersionUID = 362498820763181265L;
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
初始容量16,加载因子默认0.75。0.75的原因是Hash冲突的机会"与"空间利用率"之间寻找一种平衡与折衷。
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
new一个HashMap对象最终要调用上面的构造方法。
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
tableSizeFor(int cap) 这个方法用来计算初始容量,逻辑是把入参的二进制数最高位以后都置1,再加一。保证容量是2的正整数次幂(从16开始),这样做的好处是
p = tab[i = (n - 1) & hash]
此时等价于hash % n,位运算更为高效。除此之外,这样可以使的是数据分布更均衡,举个例子,如果cap = 17, 那么计算index时,h & 10000,这样的话有些位置永远不会存放数据。
HashMap底层维护了一个Node数组,为啥叫Node呢,HashMap发生碰撞以后会将新元素以链表头节点的形式插到这个桶上。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
上面代码是put操作,梳理一下基本逻辑。
计算index,如果该位置为null,插入。
如果不为空,判断该位置是否为树节点,如果是,插入节点。如果不是,插入链表,如果链表长度达到了8,那么转换成红黑树结构。
那么为什么是8呢?源码中有写,这里不再深究了,查了一下,与泊松分布有关,8的时候一般情况下效率比较高。
那么HashMap就先到这里吧。
LinkedHashMap
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;
继承了HashMap, 维护一个head,一个tail。其他因该都是常规方法吧。
TreeMap
这个应该是红黑树实现,大名鼎鼎的红黑树,能够以O(log2(N))的时间复杂度进行搜索、插入、删除操作。此外,任何不平衡都会在3次旋转之内解决。这一点是AVL所不具备的。源码以后有空再读吧。
源码摘自jdk1.8
Over~
created by lcy on 2020-09-21
并发集合
CopyOnWriteArrayList
public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
private static final long serialVersionUID = 8673264195747942595L;
实现了List接口
/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;
CopyOnWriteArrayList是通过“volatile数组”来保存数据的。
一个线程读取volatile数组时,总能看到其它线程对该volatile变量最后的写入
与ArrayList类似,看一下add和set方法
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 set(int index, E element) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
E oldValue = get(elements, index);
if (oldValue != element) {
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len);
newElements[index] = element;
setArray(newElements);
} else {
// Not quite a no-op; ensures volatile write semantics
setArray(elements);
}
return oldValue;
} finally {
lock.unlock();
}
}
涉及更新的操作加了锁,可以保证线程安全。
CopyOnWriteArraySet
public class CopyOnWriteArraySet<E> extends AbstractSet<E>
implements java.io.Serializable {
private static final long serialVersionUID = 5457747651344034263L;
private final CopyOnWriteArrayList<E> al;
内部维护了一个CopyOnWriteArrayList
public boolean add(E e) {
return al.addIfAbsent(e);
}
add方法
public boolean addIfAbsent(E e) {
Object[] snapshot = getArray();
return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
addIfAbsent(e, snapshot);
}
每一次插入最坏情况是O(n)的
private boolean addIfAbsent(E e, Object[] snapshot) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] current = getArray();
int len = current.length;
if (snapshot != current) {
// // 如果快照与刚获取的数组不一致说明有修改,重新判断数组中是否有e
int common = Math.min(snapshot.length, len);
for (int i = 0; i < common; i++)
if (current[i] != snapshot[i] && eq(e, current[i]))
return false;
if (indexOf(e, current, common, len) >= 0)
return false;
}
Object[] newElements = Arrays.copyOf(current, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
ConcurrentHashMap
ConcurrentHashMap主要的作用是支持多线程的读写,可以代替HashTable,用法与HashMap基本相同。
jdk1.8中ConcurrentHashMap的实现与1.7有很大差别,放弃了使用分段锁。与HashMap类似,使用数组+Hash+红黑树实现,那么1.8中的ConcurrentHashMap在更新数据时是如何实现同步的呢?我们来看一下
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
//死循环
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
//如果table为空,初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//如果对应位置桶为空,CAS插入尾部
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
//f.hash = 1 协助扩容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
//开始同步
synchronized (f) {
//找到table表下标为i的节点
// 再次检查,即double check 避免进入同步块之前,链表被修改了,不通过继续循环
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
//遍历节点
for (Node<K,V> e = f;; ++binCount) {
K ek;
//put元素,记录binCount
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
//如果节点是红黑树
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
将节点转换成TreeBin,插入
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
//如果binCount > 8树化,但是真正把这个桶转换红黑树结构还要数组长度大于64
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
可以看出,ConcurrentHashMap在执行put方法时,如果对应位置没有节点,通过CAS方式添加Node,如果对应位置有节点,使用synchronized进行同步。使用synchronized的原因是如果该key对应的节点所在的链表已经存在的情况下,可以通过Unsafe的tabAt方法基于volatile获取到该链表最新的头节点,但是需要通过遍历该链表来判断该节点是否存在,如果不使用synchronized对链表头结点进行加锁,则在遍历过程中,其他线程可能会添加这个节点,导致重复添加的并发问题。故通过synchronized锁住链表头结点的方式,保证任何时候只存在一个线程对该链表进行更新操作。
扩容原理(待补充)
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
s = sumCount();
}
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
//检查当前集合元素个数 s 是否达到扩容阈值 sizeCtl
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n);
if (sc < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
}