集合概览
Iterator迭代
1.Collection接口
1.1 List接口
1.1.1 ArrayList
本质是动态数组,动态扩容
无参构造
List list = new ArrayList();
//源码分析
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
有参构造
List list2 = new ArrayList(6);
//源码分析
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
//创建指定长度的数组
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
//赋值elementData={}
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
}
}
add方法
public boolean add(E e) {
//确定容量,动态扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
//将要添加的元素添加到数组中
elementData[size++] = e;
return true;
}
如何实现动态扩容?
/**
* Default initial capacity.
*/
private static final int DEFAULT_CAPACITY = 10;
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* The array buffer into which the elements of the ArrayList are stored.
*/
transient Object[] elementData; // non-private to simplify nested class access
/**
* The size of the ArrayList (the number of elements it contains).
*/
private int size;
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) {
// 假设10个长度已满,第一次扩容,oldCapacity=10
int oldCapacity = elementData.length;
// oldCapacity >> 1: 10转成二进制1010右移一位,变成0101=5
// 那么newCapacity = 10 + 5 = 15
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);
}
get方法
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
// 检查是否下标越界
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
// 根据下标从数组中获取元素
E elementData(int index) {
return (E) elementData[index];
}
set方法
public E set(int index, E element) {
// 先判断是否超出数组长度
rangeCheck(index);
// 先获取原来值,并最终返回旧值
E oldValue = elementData(index);
elementData[index] = element; // 向对应下标位置放值
return oldValue;
}
remove方法
public E remove(int index) {
rangeCheck(index); // 检查下标是否越界
modCount++;
E oldValue = elementData(index); // 获取删除前的旧值
// {1,2,3,4,5,6,7,8,9,10} size=8, index=3 remove4
// numMoved=10-3-1=6
int numMoved = size - index - 1;
if (numMoved > 0)
// 从下标4的位置开始copy 6个长度{5,6,7,8,9,10}
// copy到{1,2,3,4},从下标3的位置开始覆盖
// 最终得到{1,2,3,5,6,7,8,9,10}
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);
FailFast机制
快速失败的机制,Java集合类为了应对并发访问在集合迭代过程中内部结构发生变化的一种防护措施。
这种错误检查机制为可能出现的错误抛出java.util.ConcurrentModificationException
1.1.2 LinkedList
LinkedList通过双向链表实现的,它的数据结构具有双向链表的优缺点,所以它的顺序访问效率很高,但是随即访问效率比较低。它包含一个重要的私有内部静态类:Node
private static class Node<E> {
E item; // 节点的元素
LinkedList.Node<E> next; // 下一个节点
LinkedList.Node<E> prev; // 上一个节点
Node(LinkedList.Node<E> prev, E element, LinkedList.Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
push方法
public void push(E e) {
addFirst(e);
}
public void addFirst(E e) {
linkFirst(e);
}
// 添加到头部
private void linkFirst(E e) {
final LinkedList.Node<E> f = first;
final LinkedList.Node<E> newNode = new LinkedList.Node<>(null, e, f);
first = newNode;
if (f == null)
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}
add方法
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {
final LinkedList.Node<E> l = last;
final LinkedList.Node<E> newNode = new LinkedList.Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
get方法
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
// 检查下标是否越界
private void checkElementIndex(int index) {
if (!isElementIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
// 根据下标索引获取节点元素
LinkedList.Node<E> node(int index) {
// assert isElementIndex(index);
// size >> 1: 10(1010)右移一位,则是(0101)5
// size >> 1: 6(0110)右移动一位,则是(0011)3
// size >> 1:相当于是取size的一半
if (index < (size >> 1)) {//如果index小于size的一半,则从前往后遍历
LinkedList.Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {//如果index大于等于size的一半,则从后往前遍历
LinkedList.Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
set方法
public E set(int index, E element) {
// 检查下标是否越界
checkElementIndex(index);
// 获取下标对应节点
LinkedList.Node<E> x = node(index);
E oldVal = x.item;
// 将新值赋给此节点,并返回旧值
x.item = element;
return oldVal;
}
remove方法
public E remove(int index) {
checkElementIndex(index); //检查下标是否越界
return unlink(node(index));
}
E unlink(LinkedList.Node<E> x) {
// assert x != null;
final E element = x.item;
final LinkedList.Node<E> next = x.next;
final LinkedList.Node<E> prev = x.prev;
if (prev == null) { // 如果没有上一个节点,那么下一个节点就是首节点
first = next;
} else { // 如果有下一个节点,那么就把下一个节点作为上一个节点的后节点
prev.next = next;
x.prev = null; // 当前节点和上一个节点取消关联
}
if (next == null) { // 如果没有下一个节点,那么上一个节点就是尾节点
last = prev;
} else { // 如果有下一个节点,那么就把上一个节点作为下一个节点的前节点
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
1.1.3 Vector
和ArrayList很类似,都是以动态数组形式保存数据
但是Vector是线程安全的,每个操作方法都加有synchronized关键字,这会大大影响性能,所以很少使用此类
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
那么使用ArrayList如何保证线程安全呢?
我们可以使用Collections工具类提供的一些转换方法:
List syncList = Collections.synchronizedList(list);
// 每个操作方法上添加了一个同步代码块
public E get(int index) {
synchronized (mutex) {return list.get(index);}
}
public E set(int index, E element) {
synchronized (mutex) {return list.set(index, element);}
}
public void add(int index, E element) {
synchronized (mutex) {list.add(index, element);}
}
public E remove(int index) {
synchronized (mutex) {return list.remove(index);}
}
public int indexOf(Object o) {
synchronized (mutex) {return list.indexOf(o);}
}
public int lastIndexOf(Object o) {
synchronized (mutex) {return list.lastIndexOf(o);}
}
public boolean addAll(int index, Collection<? extends E> c) {
synchronized (mutex) {return list.addAll(index, c);}
}
1.2 Set接口
1.2.1 HashSet
本质上是一个HashMap,由哈希表支持,它不保证Set的迭代顺序,但是可以保证没有重复元素,允许元素为null。
public HashSet() {
map = new HashMap<>();
}
add方法
本质上是将数据保存在HashMap中,保存的元素作为HashMap的key,而value是一个固定的Object对象。
private static final Object PRESENT = new Object();
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
1.2.2 TreeSet
本质上是一个TreeMap,实现了SortedSet接口,所以对元素进行了排序,
public TreeSet() {
this(new TreeMap<E,Object>());
}
add方法
本质上是将数据保存在TreeMap中,保存的元素作为TreeMap的key,而value是一个固定的Object对象。
private static final Object PRESENT = new Object();
public boolean add(E e) {
return m.put(e, PRESENT)==null;
}
2.Map接口
Map的特点:
1> key唯一,不可重复
2> value可以重复
3> 值的顺序取决于键的顺序
4> 键和值都可以存储null元素
2.1 HashMap
HashMap底层结构
1> JDK1.7及以前采用数组+单向链表
2> JDK1.8之后采用数组+单向链表 或者 数组+红黑树方式进行元素的存储
当put存储值时,根据hash算法对key进行计算得到存储在数组的某个位置,但是可能会出现重复的位置,这个时候会通过链表存储相同位置的值;但是如果链表长度过长,查询效率很低,所以JDK1.8之后,如果链表长度达到8,就会将链表转换成红黑树来存储。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认初始数组容量16
static final int MAXIMUM_CAPACITY = 1 << 30; //数组最大容量:亿级别
static final float DEFAULT_LOAD_FACTOR = 0.75f; //当数组被使用了0.75个长度时扩容
static final int TREEIFY_THRESHOLD = 8; //链表转红黑树的临界值,当链表长度达到8,转成红黑树
static final int UNTREEIFY_THRESHOLD = 6; //红黑树转链表的临界值,当红黑树元素达到6,转成链表
//链表转红黑树的数组长度临界值,当数组长度达到64,数组中某个位置链表长度达到8,该位置链表转红黑树
static final int MIN_TREEIFY_CAPACITY = 64;
transient Node<K,V>[] table; //HashMap中的数组结构
transient Set<Map.Entry<K,V>> entrySet; //数组值
transient int size; //HashMap中元素个数
transient int modCount; //对HashMap操作的次数
int threshold; //扩容的临界值:容量*扩容因子
final float loadFactor; //扩容因子
put方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
// ^:异或运算符,两个值先转换成二进制,相同位值相同则0,相同位值不同则1,e.g. 2^3=10^11=01
// >>>无符号右移运算符,高位补0
// key.hashCode()得到一个32位的hash值,右移16位则只取前16位hash值,再与原32hash值异或运算
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length; //第一次添加上元素时,设置默认16长度的节点数组
if ((p = tab[i = (n - 1) & hash]) == null) //如果该下标下没有内容
tab[i] = newNode(hash, key, value, null); //则创建新节点直接赋值
else {
HashMap.Node<K,V> e; K k;
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p; //如果插入的key和数组当前位置key相同,则获取原有节点对象,后面修改内容
else if (p instanceof HashMap.TreeNode) //数组的节点是红黑树节点
e = ((HashMap.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;
}
2.2 TreeMap
TreeMap本质上是红黑树的实现
private transient TreeMap.Entry<K,V> root; //TreeMap中根节点root
// Entry中包含以下属性,按照红黑树规则设计
K key; // key
V value; // value
TreeMap.Entry<K,V> left; // 左子节点
TreeMap.Entry<K,V> right; // 右子节点
TreeMap.Entry<K,V> parent; // 父节点
boolean color = BLACK; // 节点颜色:红/黑
put方法
public V put(K key, V value) {
TreeMap.Entry<K,V> t = root;
if (t == null) {
compare(key, key); // type (and possibly null) check
root = new TreeMap.Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
TreeMap.Entry<K,V> parent;
// split comparator and comparable paths
Comparator<? super K> cpr = comparator;
if (cpr != null) {
// 一直循环找到要插入节点的父节点t
do {
parent = t;
cmp = cpr.compare(key, t.key); //当前put的key和父节点的key做比较
if (cmp < 0) //如果当前key比父节点key小,则作为左子节点
t = t.left;
else if (cmp > 0) //如果当前key比父节点key大,则作为右子节点
t = t.right;
else //如果和父节点key相等,则修改值
return t.setValue(value);
} while (t != null); //继续循环往下比较
}
else { //如果比较器不存在
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
TreeMap.Entry<K,V> e = new TreeMap.Entry<>(key, value, parent);
if (cmp < 0) //插入的节点在parent的左子节点
parent.left = e;
else //插入的节点在parent的右子节点
parent.right = e;
fixAfterInsertion(e); //实现红黑树的平衡
size++;
modCount++;
return null;
}
红黑树自平衡
private void fixAfterInsertion(TreeMap.Entry<K,V> x) {
x.color = RED; //首先设置被添加节点颜色为红色
//循环的条件:当前节点不为空,不是根节点,父节点颜色为红色
while (x != null && x != root && x.parent.color == RED) {
//如果父节点是祖父节点的左子节点
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
//获取祖父节点的右子节点
TreeMap.Entry<K,V> y = rightOf(parentOf(parentOf(x)));
if (colorOf(y) == RED) { //如果祖父节点的右子节点为红色
setColor(parentOf(x), BLACK); //设置父节点颜色为黑色
setColor(y, BLACK); //设置祖父节点的右子节点为黑色
setColor(parentOf(parentOf(x)), RED); //设置祖父节点颜色为红色
x = parentOf(parentOf(x)); //将祖父节点设置为插入节点继续循环判断
} else { //祖父节点的右子节点为黑色
if (x == rightOf(parentOf(x))) { //如果插入节点在右侧
x = parentOf(x); //将父节点作为插入节点
rotateLeft(x); //左旋
}
setColor(parentOf(x), BLACK); //设置父节点颜色为黑色
setColor(parentOf(parentOf(x)), RED); //设置祖父节点颜色为红色
rotateRight(parentOf(parentOf(x))); //祖父节点右旋
}
} else { //如果父节点是祖父节点的右子节点
// 获取祖父节点的左子节点
TreeMap.Entry<K,V> y = leftOf(parentOf(parentOf(x)));
if (colorOf(y) == RED) {/ /如果叔叔节点为红色
setColor(parentOf(x), BLACK); //设置父节点为黑色
setColor(y, BLACK); //设置叔叔节点为黑色
setColor(parentOf(parentOf(x)), RED); //设置祖父节点为红色
x = parentOf(parentOf(x)); //将祖父节点设置为插入节点
} else { //如果叔叔节点为黑色
if (x == leftOf(parentOf(x))) { //插入节点在左侧
x = parentOf(x); //将父节点作为插入节点
rotateRight(x); //右旋
}
setColor(parentOf(x), BLACK); //设置父节点颜色为黑色
setColor(parentOf(parentOf(x)), RED); //设置祖父节点颜色为红色
rotateLeft(parentOf(parentOf(x))); //左旋
}
}
}
root.color = BLACK;//根节点的颜色为红色
}
2.3 ConcurrentHashMap
HashMap是线程非安全的,多线程下会存在线程安全问题
HashTable是线程安全的,因为在方法上有加synchronized同步锁,但是锁的粒度太大,会影响性能,所以引入了CHM.
ConcurrentHashMap在性能和安全方面做了平衡,在方法内部代码块上加了synchronized锁。
使用方法
computeIfAbsent:如果不存在则修改值
computeIfPresent:如果存在则修改值
compute:computeIfAbsent和computeIfPresent的结合
merge:数据合并
存储结构和实现
jdk 1.7:使用segment分段锁,锁的粒度大
jdk 1.8:使用链表 + 红黑树
ConcurrentHashMap和HashMap原理基本类似,只是在HashMap的基础上需要支持并发操作,保证多线程情况下对HashMap操作的安全性。当某个线程对集合内的元素进行数据操作时,会锁定这个元素,如果其他线程操作的数据hash得到相同的位置,就必须等到这个线程释放锁之后才能进行操作。
数据结构
- 最外层是初始16位长度的数组,数据达到阈值(16 * 0.75)时会自动扩容(16 >> 1 = 32)
- 插入数据时,先对key进行hash计算得到数据将要插入到数组的位置下标,如果此位置为空,则插入;
- 如果此位置有数据,并且key相同,则替换做修改操作;
- 如果此位置有数据,但key不同,则追加到此下标位置;
- 初始情况下标位置是以单向链表结构存储数据,后续数据追加到链表尾部;
- 当数组长度扩容到64,且某个位置链表长度达到8时,会将单向链表转换为红黑树结构
- 做删除操作时,如果某个位置元素小于8时,会将红黑树转换为单向链表
扩容过程(满足两种情况会扩容):
- 当新增节点后,所在位置链表元素个数达到阈值8,并且数组长度小于64;
- 当增加集合元素后,当前数组内元素个数达到扩容阈值(16 * 0.75)时就会触发扩容;
- 当线程处于扩容状态下,其他线程对集合进行操作时会参与帮助扩容;
默认是16位长度的数组,如果扩容就会新创建一个32位长度的数组,并对数据进行迁移,采用高低位迁移;
高低位迁移原理
扩容之后,数据迁移,有些数据需要迁移,有些数据不需要,低位不变,高位迁移;
数据扩容,但是计算存储位置下标的公式不变:i = (n - 1) & hash,所以有些key在扩容前后得到的下标位置相同,而有些key在扩容后hash得到的下标位置发生了改变;
假设:某个key的hash为9,数组长度为16,扩容到32,hash后得到的位置依然是9
假设:某个key,数组长度为16时hash值为4,而扩容为32长度时hash值变成了20
所以,table长度发生变化之后,获取同一个key在集合数组中的位置发生了变化,那么就需要迁移
链表转红黑树
当数组长度大于等于64,且某个数组位置的链表长度大于等于8,会把该位置链表转化为红黑树
原理
put插入元素
public V put(K key, V value) {
return putVal(key, value, false); // 是否只有当不存在时才put
}
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 (ConcurrentHashMap.Node<K,V>[] tab = table;;) {
ConcurrentHashMap.Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new ConcurrentHashMap.Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (ConcurrentHashMap.Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
ConcurrentHashMap.Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new ConcurrentHashMap.Node<K,V>(hash, key,
value, null);
break;
}
}
}
else if (f instanceof ConcurrentHashMap.TreeBin) {
ConcurrentHashMap.Node<K,V> p;
binCount = 2;
if ((p = ((ConcurrentHashMap.TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
3.工具类:
Collections
Arrays
4.比较器
Comparable
Comparator