结合JDK1.8源码对其进行总结分析。理解其底层实现原理才是关键。
先上总结结果:
一表胜千言
Tips:
除ConcurrentHashMap,均实现 Cloneable, Serializable接口
实现接口 List,能对它进行队列操作
实现 Deque 接口,能当作双端队列使用。
实现接口RandomAccess提供随机访问功能
Java 集合框架
Java集合是java提供的工具包,包含了常用的数据结构
Java集合主要可以划分为4个部分:
- List列表
- Set集合
- Map映射
- 工具类(Iterator迭代器、Enumeration枚举类、Arrays和Collections)
1.Collection 接口
- List 接口
实现类有LinkedList, ArrayList, Vector, Stack。 Set接口
实现类有HastSet和TreeSet
HashSet依赖于HashMap;TreeSet依赖于TreeMapQueue接口
子类Deque双端队列, LinkedList
Iterator遍历集合的工具
- ListIterator是专门为遍历List而存在的
- Enumeration只能在Hashtable, Vector, Stack使用
Arrays和Collections
它们是操作数组、集合的两个工具类
AbstractCollection
- 定义
Collection 接口 的一个直接实现类, Collection 下的大多数子类都继承 AbstractCollection ,比如 List 的实现类, Set的实现类 - 子类
AbstractList, AbstractQueue, AbstractSet,
ArrayDeque, ConcurrentLinkedDeque
AbstractList
- 继承自 AbstractCollection 抽象类
- 实现了Iterable, Collection, List
- 子类:AbstractSequentialList, ArrayList, Vector
AbstractSequentialList
- 继承自 AbstractList,是 LinkedList 的父类
- 是 List 接口 的简化版实现,只支持按次序访问
Vector
线程安全
Vector继承自AbstractList实现接口 List,能对它进行队列操作
Vector实现接口Cloneable、RandomAccess、Serializable
重要对象
- elementData :动态数组,默认容量大小是10
- elementCount:动态数组的实际大小
- capacityIncrement:动态数组的增长系数
若容量增加系数 >0,则将容量的值增加“容量增加系数”;
否则,将容量大小增加一倍。
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
遍历方式
- 通过迭代器Iterator遍历
- 随机访问,通过索引值去遍历(实现RandomAccess接口)
- for循环遍历:for (Integer integer:list)
- Enumeration遍历
Tips:使用索引的随机访问方式最快,使用迭代器最慢
Stack
线程安全
- Stack继承于Vector(矢量队列),即通过数组实现的,而非链表
- 执行peek时(取出栈顶元素,不执行删除),返回数组末尾的元素。
ArrayList
非线程安全
- ArrayList 继承了AbstractList
- 实现接口RandomAccess提供随机访问功能
- 实现接口Cloneable、Serializable
- 实现接口 Iterable, Collection, List
重要对象
- elementData:动态数组,容量默认是10
- size:动态数组的实际大小
- 容量不足ArrayList会重新设置容量:新的容量=原始容量*1.5
- JDK1.8 为1.5倍 !!!
- int newCapacity = oldCapacity + (oldCapacity >> 1);
遍历方式
- 通过迭代器Iterator遍历
- 随机访问,通过索引值去遍历(实现RandomAccess接口)
- for循环遍历
初始化
1.新建默认ArrayList对象
调用默认构造函数,将默认的空数组传给elementData 数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
2.添加一个元素
此时size=0,调用add函数
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
add函数调用ensureCapacityInternal函数判断是否要扩容,传入参数size+1
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
此时elementData 为默认空数组,默认容量10
不为空直接返回minCapacity
(minCapacity为size+1,返回10)
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
modCount自增,表示修改次数加一
(传入参数10)
如果minCapacity 大于数组长度,调用grow,参数为minCapacity
(此时minCapacity =10,length=0 )
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
把旧数组扩容1.5倍
(oldCapacity =0)
若扩容后仍小于minCapacity,取minCapacity
(仍为0,直接把10赋给newCapacity )
若扩容后大于最大容量,取最大容量Integer.MAX_VALUE - 8;
(此处小于最大容量)
//一些JVM实现时,会在数组的前面放一些额外的数据
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
调用Arrays.copyOf 把elementData 的元素拷贝到新的数组
内部是System的arraycopy方法
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);
}
最后在add里把elementData[0] = e
elementData[size++] = e;
此时ArrayList容量为10;size为1
Tips:随机访问效率最高,迭代器效率最低
LinkedList
- LinkedList 继承了AbstractSequentialList的双向链表
- LinkedList 实现 Deque 接口,即能将LinkedList当作双端队列使用。
- 实现接口Cloneable、Serializable
- 实现接口 List,能对它进行队列操作
- LinkedList 是非同步的
重要对象
- header :双向链表的表头
- size:双向链表中节点的个数
遍历方式
- 通过迭代器Iterator遍历
- 快速随机访问,通过索引值去遍历(实现List接口)
- for循环遍历:for (Integer integer:list)
Tips:不要通过随机访问去遍历LinkedList
2.Map接口
- AbstractMap抽象类,实现了map接口,它实现了Map中的大部分API
HashMap,TreeMap,WeakHashMap都是继承于AbstractMap
Hashtable虽然继承于Dictionary,但它实现了Map接口 - ConcurrentMap接口
- SortedMap接口
- Dictionary
- Map.Entry
HashMap
非线程安全
- 继承于AbstractMap
- 实现了Map、Cloneable、java.io.Serializable接口
- key、value都可以为null。映射不是有序的。
- Entry对象的hashCode()由关键字key的hashCode()与值value的hashCode()做异或运算
- HashMap计算hash对key的hashcode进行了二次hash
- hashMap求索引时用的是indexFor(int hash,int length)
//JDK7
static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
//JDK8
//Java 8中的散列值优化函数
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
通过 h & (table.length -1) 来得到该对象的保存位,而HashMap底层数组的长度总是 2 的 n 次方,这是HashMap在速度上的优化
因为使用的掩码是2的次幂,高于掩码的位组成的哈希集合总是冲突,所以把高位移到低位。
混合原始哈希码的高位和低位,以此来加大低位的随机性。
(数组长度-1)正好相当于一个“低位掩码”
重要对象
- DEFAULT_INITIAL_CAPACITY :默认的初始容量是16,必须是2的幂
- MAXIMUM_CAPACITY :最大容量,1<<30
- table:Entry[]数组类型,每一个Entry本质上是一个单向链表
- size:HashMap的大小,键值对的数量
- threshold:HashMap的阈值,threshold=容量*加载因子
- loadFactor:实际加载因子,默认值为0.75
- modCount:HashMap被改变的次数,用来实现fail-fast机制
- 负载因子越大表示散列表的装填程度越高
- HashMap扩容时,将容量变为原来的2倍
- 指定为16是从性能考虑。避免重复计算
- TREEIFY_THRESHOLD :树形和列表的阈值,默认8
- UNTREEIFY_THRESHOLD :树形转换回链式的阈值,默认6
- MIN_TREEIFY_CAPACITY :哈希表的最小树形化容量,默认64
JDK1.8中HashMap的数据结构
HashMap是数组+链表+红黑树
在 Java8 中,当链表中的元素超过了 8 个以后,会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为 O(logN)。
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
根据哈希表中元素个数确定是扩容还是树形化
桶的树形化treeifyBin()
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
Fail-Fast机制
在迭代过程中,判断modCount跟expectedModCount是否相等,如果不相等就表示已经有其他线程修改了Map
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
put过程分析
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
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) {
// 插入到链表的最后面(Java7 是插入到链表的最前面)
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 如果在该链表中找到了"相等"的 key(== 或 equals)
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//e!=null 说明存在旧值的key与要插入的key"相等"
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 值的时候,触发的 resize()
数组从 null 初始化到默认的 16 或自定义的初始容量
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);
(n - 1) & hash找到具体的数组下标,
如果要存放的这个位置刚好没有元素,那么直接初始化 Node 并放置在这个位置
如果此位置有值
- 首先,判断该位置的第一个数据和我们要插入的数据,key 是不是”相等”,如果是,取出这个节点赋值给e
- 如果该节点是代表红黑树的节点,调用红黑树的putTreeVal插值方法
- 否则说明数组该位置上是一个链表,插入到链表的最后面
如果插入后超过8个,会触发 treeifyBin将链表转换为红黑树break
如果在该链表中找到了”相等”的 key(== 或 equals) break
如果e!=null 说明存在旧值的key与要插入的key”相等”
进行 “值覆盖”,然后返回旧值
判断阈值,决定是否扩容
get 过程分析
计算 key 的 hash 值,根据 hash 值找到对应数组下标: hash & (length-1)
判断数组该位置处的元素是否刚好就是我们要找的,如果不是,走第三步
判断该元素类型是否是 TreeNode,如果是,用红黑树的方法取数据,如果不是,走第四步
遍历链表,直到找到相等(==或equals)的 key
遍历方式
- 遍历HashMap的键值对
1.根据entrySet()获取HashMap的“键值对”的Set集合
2.通过Iterator迭代器遍历得到的集合
Hashtable
线程安全
- 继承于Dictionary
- 实现了Map、Cloneable、java.io.Serializable接口
- key、value不可以为null。映射不是有序的。
- 直接使用key的hashcode对table数组的长度直接进行取模
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
- 先用hash&0x7FFFFFFF后(转化为正值),再对length取模(防止数据越界)
Hashtable 是通过”拉链法”实现的哈希表
Hashtable 底层就是一个Entry[] 数组结构,数组中的每一项又是一个链表。
重要对象
- table:Entry[]数组类型,每一个Entry本质上是一个单向链表
- count:Hashtable的大小,键值对的数量,
- threshold:Hashtable的阈值,threshold=容量*加载因子
- loadFactor:实际加载因子,默认值为0.75
- modCount:Hashtable被改变的次数,用来实现fail-fast机制
- 负载因子越大表示散列表的装填程度越高
- 默认容量为11,增加方式为 old*2+1
Fail-Fast机制
遍历
支持Iterator(迭代器)和Enumeration(枚举器)两种方式遍历
TreeMap
是有序的散列表,它是通过红黑树实现
3.Set接口
AbstractSet
继承于AbstractCollection,实现了Set中的绝大部分函数
HastSet 和 TreeSet 是Set的两个实现类。
- HashSet依赖于HashMap,它实际上是通过HashMap实现的。HashSet中的元素是无序的.
- TreeSet依赖于TreeMap,它实际上是通过TreeMap实现的。TreeSet中的元素是有序的。
HashSet
非线程安全
- 继承于AbstractSet
- 实现了Set、Cloneable、java.io.Serializable接口
- 允许使用 null 元素。元素不是有序的。
重要对象
- map:HashMap对象
- PRESENT:向map中添加键值对时,键值对的值固定是PRESENT
将存放的对象当做了 HashMap 的健,value 都是相同的 PRESENT 。由于 HashMap 的 key 是不能重复的,所以每当有重复的值写入到 HashSet 时,value 会被覆盖,但 key 不会收到影响,这样就保证了 HashSet 中只能存放不重复的元素。
HashSet通过iterator()返回的迭代器是fail-fast的
遍历方式
- 通过Iterator遍历HashSet
public Iterator<E> iterator() {
// 实际上返回的是HashMap的“key集合的迭代器”
return map.keySet().iterator();
}
Iterator和Enumeration区别
- 函数接口不同
Enumeration只有2个函数接口。只能读取集合的数据,不能修改。
Iterator只有3个函数接口。Iterator能读取也能数据进行删除操作。 - Iterator支持fail-fast机制,而Enumeration不支持
- Enumeration使用到它的函数包括Vector、Hashtable等类
- Iterator是为了HashMap、ArrayList等集合提供遍历接口
- Enumeration本身并没有支持同步,而在Vector、Hashtable实现Enumeration时,添加了同步。