Java 集合类位于 java.util 包下,JDK1.5 之后还在 java.util.concurrent 包下提供了一些多线程支持的集合类。Java 集合主要由两个接口派生而出:Collection 和 Map。Collection 的父接口是 Iterable(迭代器),所以 Collection 的子接口全部可以使用 Iterable 遍历集合。
Collection 的子接口包括 List、Set 和 Queue 接口。
从 Java 源码来看,Java 先实现了 Map,然后通过包装一个所有 value 都为 null 的 Map 就实现了 Set 集合。
1. List集合
多线程 | 底层 | 说明 | 动态扩容 | |
---|---|---|---|---|
ArrayList | 线程不安全 | 数组 | 有序可重复 | 默认大小10,每次扩容容量为原来的1.5倍 |
Vector | 线程安全 | 数组 | 方法都加上了synchronized,保证了线程安全 | 每次扩容容量为原来的2倍 |
LinkedList | 线程不安全 | 双向循环链表,既可以作队列,可以作栈 | 没有初始化大小,所以也没有扩容机制,就是一直在前面或者后面新增就好 |
Vector 是线程安全的,所以 ArrayList 性能相对 Vector 会好很多。
1.1 ArrayList
List<Integer> list = new ArrayList<Integer>();
list.add(2); // 虽然集合里不能放基本类型的值,但Java支持自动装箱
ArrayList 线程不安全,底层是数组实现的,支持动态扩容。
源码解析:
public class ArrayList<E> extends AbstractList<E> implements List<E>,
RandomAccess, // 支持快速随机访问
Cloneable, // 能被克隆
java.io.Serializable { // 支持序列化
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData; // 数组
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
public ArrayList(int initialCapacity) {
this.elementData = new Object[initialCapacity];
}
...
}
调用无参构造方法 new 对象时,实际上只是将 elementData 指向一个空数组而已。调用带参数的构造方法时,就是初始化一个参数 initialCapacity 大小的新数组。
不管是上面哪种情况,调用 add() 操作时,都会涉及动态扩容,无参构造函数创建的 ArrayList 第一次 执行 add() 操作时,就会动态扩容出一个长度为 10 的数组。
ArrayList 动态扩容其实就是创建新的长度的数组,然后把老数组数据 copy 到新数据,再覆盖掉老数组的过程。
ArrayList 每次扩容容量为原来的 1.5 倍,需要注意的是 Vector 每次扩容容量是原来的 2 倍。
通过源码可以看到,ArrayList 并不是线程安全的,方法里即没用到锁,也没用到 CAS 操作。
1.2 LinkedList
有序可重复,既可以被当作栈(先进后出)使用,也可以当成队列(先进先出)使用。
LinkedList<Integer> linkedList = new LinkedList<Integer>();
linkedList.offer(1); // 将元素加入队列的尾部
linkedList.offerFirst(2); // 将元素添加到队列的头部
Integer element1 = linkedList.peekLast(); // 访问、并不删除队列的最后一个元素
Integer element2 = linkedList.pollLast(); // 访问、并删除队列的最后一个元素
linkedList.push(3); // 将元素加入栈的顶部
Integer element3 = linkedList.peekFirst(); // 访问、并不弹出栈顶的元素
Integer element4 = linkedList.pop(); // 将栈顶的元素弹出
// 以List的方式(按索引访问的方式)来遍历集合元素
for (int i = 0; i < linkedList.size(); i++) {
Integer element = linkedList.get(i);
}
LinkedList 线程不安全,底层是双向循环链表实现的,没有初始化大小,所以也没有扩容机制,就是一直在前面或者后面新增就好。
源码解析:
public class LinkedList<E> extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable {
transient int size = 0;
transient Node<E> first; // 头节点
transient Node<E> last; // 尾节点
public LinkedList() {
}
private static class Node<E> { // 节点
E item; // 元素
Node<E> next; // 下一个节点
Node<E> prev; // 上一个节点
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
...
}
通过源码可以看到,LinkedList 并不是线程安全的,方法里即没用到锁,也没用到 CAS 操作。
对于遍历 List 集合元素,ArrayList 最好使用随机访问方法(get)来遍历,LinkedList 则最好用迭代器(Iterator)来遍历,这样性能最好。
2. Set集合
多线程 | 底层 | 说明 | |
---|---|---|---|
HashSet | 线程不安全 | 数组 | 无序,集合元素值允许为null |
TreeSet | 线程不安全 | 红黑树 | 有序,会按照自然排序,集合元素值允许为null |
2.1 HashSet
Set<Integer> set = new HashSet<>();
// 遍历集合元素
Iterator<Integer> it = set.iterator();
while (it.hasNext()) {
Integer i = it.next();
}
HashSet 判断两个元素相等的标准:两个对象通过 equals() 比较相等且两个对象的 hashCode() 返回值也相等。
源码解析:
public class HashSet<E> extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable {
private transient HashMap<E, Object> map;
public HashSet() {
map = new HashMap<>();
}
...
}
通过源码可以看到,HashSet 是通过 HashMap 实现的,简而言之,HashSet 就是一个 value 为空对象的 HashMap。
2.2 TreeSet
TreeSet 采用红黑树的数据结构来存储集合元素,支持两种排序方法:自然排序和定制排序,默认自然排序。
TreeSet<Integer> treeSet = new TreeSet<Integer>();
Integer first = treeSet.first(); // 获取第一个元素
Integer last = treeSet.last(); // 获取最后一个元素
SortedSet headSet = treeSet.headSet(25); // 获取小于25的子集,不包含25
SortedSet tailSet = treeSet.tailSet(10); // 获取大于10的子集,如果Set中包含10,子集中还包含10
SortedSet subSet = treeSet.subSet(-10, 20); // 获取大于等于-10,小于20的子集
源码解析:
public class TreeSet<E> extends AbstractSet<E>
implements NavigableSet<E>, Cloneable, java.io.Serializable {
private transient NavigableMap<E, Object> m;
TreeSet(NavigableMap<E, Object> m) {
this.m = m;
}
public TreeSet() {
this(new TreeMap<E, Object>());
}
// 省略部分代码
}
通过源码可以看到,TreeSet 是通过 TreeMap 实现的,简而言之,TreeSet 就是一个 value 为空对象的 TreeMap。
3. Map集合
多线程 | 底层 | 说明 | 动态扩容 | |
---|---|---|---|---|
HashMap | 线程不安全 | hash 表 | 无序,key 不允许null,value 可以null,为快速查询而设计的 | hash 表中数组默认大小16,如果数组元素超过当前大小 * 扩容因子 0.75 就会扩容,扩容 2 倍后重排 |
LinkedHashMap | 线程不安全 | hash 表 | 继承 HashMap | |
ConcurrentHashMap | 线程安全 | hash 表 | ||
TreeMap | 线程不安全 | 红黑树 | 自然有序,key 不允许null,value 可以null,速度略慢于 HashMap | |
Hashtable | 线程安全 | hash 表 | ||
Properties | 线程安全 | hash 表 | 继承自 Hashtable,可以把Map对象和属性文件关联起来 | |
IdentityHashMap | 线程不安全 | 数组 | 根据对象内存地址计算 hash |
Map 用于保存具有映射关系(key-value)的数据,key 不允许重复,value 可以重复。从 Java 源码来看,Java 先实现了Map,然后通过包装一个所有 value 都为 null 的 Map 就实现了 Set 集合。
速度对比:HashMap(最快) > Hashtable > TreeMap
3.1 HashMap
hash(哈希、散列)算法的功能:能保证快速查找被检索的对象,hash 算法的价值在于速度,当查询某个元素时,hash 算法可以直接根据该元素的 hashCode 值计算出该元素的存储位置,从而快速定位。
Map<String, Object> map = new HashMap<String, Object>();
boolean b = map.containsKey("A"); // 判断是否包含指定key
boolean b1 = map.containsValue(90); // 判断是否包含指定value
// 遍历集合元素
for (Map.Entry<String, Object> entry : map.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
}
map.remove("A"); // 删除key-value
源码解析:
transient Set<K> keySet;
transient Collection<V> values;
通过源码可以看到,Map 里的 key 是通过 Set 组织起来的,通过 Set 意味着它是有去重功能的,这也是为什么 key 不允许重复的原因,而 value 是通过 Collection 组织起来的。
HashMap 数据结构:
HashMap JDK1.8 前采用了数组 + 链表实现的,数组的特点是查询快增删慢,链表的特点是查询慢增删快,HashMap 结合了两者的优势,同时 HashMap 的操作是非 synchronized 的,因此效率比较高。
HashMap hash 表中数组默认大小16,如果数组元素超过当前大小 * 扩容因子 0.75 就会扩容,扩容 2 倍后重排。
最坏情况下,hash() 计算后总是会命中同一个数组元素,那么 HashMap 的性能将会从原先的 O(1) 变成 O(n)。
针对这个问题,JDK1.8 及以后变成了数组 + 链表 + 红黑树实现,使用了一个 TREEIFY_THRESHOLD 常量来控制是否将链表转换为红黑树来存储它们,这意味着可以将最坏情况下的性能从 O(n) 提高到 O(logn)。
通过 JDK1.8 HashMap 源码可以看出,HashMap 是通过一个 table 数组和链表组成的复合结构。
transient Node<K,V>[] table; // table数组
transient Set<Map.Entry<K,V>> entrySet;
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key; // 键
V value; // 值
Node<K,V> next; // 下一个Node
// 省略部分代码
}
// hash算法
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
Node 是由 hash 值,键值对以及指向的下一个 Node 组成的,一个 table 数组元素可以理解为一个 “桶”。
HashMap put() 方法的逻辑:
- 若 table 数组未被初始化,则进行初始化操作;
- 对 key 计算 hash 值,依据 hash 值计算 table 数组下标;
- 若未发生碰撞,即该下标的 “桶” 还没有存储头节点,则直接放入 “桶” 的头节点中;
- 若发生碰撞,即该下标的 “桶” 已经存储了头节点,则执行链表的添加操作;
- 若链表的长度超过树化阈值 (TREEIFY_THRESHOLD,默认 8)则被改造成红黑树,而当删除操作导致低于最低树化阈值(UNTREEIFY_THRESHOLD,默认 6)的时候,红黑树则转回成链表,保证更高的性能;
- 若键值对已经存在,则用新值替换旧值;
- 若某个 “桶” 满了(默认容量 16 * 扩容因子 0.75 = 12,则默认超过 12 就会扩容),就需要 resize(扩容 2 倍后重排)。
这里树化阈值为什么默认是 8 呢?
如何有效减少碰撞,提升 HashMap 性能?
- 扰动函数:促使元素位置分布均匀,较少碰撞机率;
- 使用 final 对象,并采用合适的 equals() 和 hashCode() 方法。
扩容?
扩容的问题?
- 多线程环境下,调整大小会存在条件竞争,容易造成死锁;
- 原先 HashMap 的键值对需要重新移动到新的 HashMap 里,这将是一个非常耗时的过程。
HashTable 是早起 Java 类库提供的哈希表的实现,HashTable 的方法都加的 synchronized 修饰,是线程安全的,但是也导致了多线程环境下串行化的执行,性能较差,所以已经很少使用了。HashTable 的源码相对简单,也没有树化逻辑,有时间可以阅读一下。
Map<String, Object> hashMap = new HashMap<String, Object>();
Map<String, Object> safeHashMap = Collections.synchronizedMap(hashMap);
其实 HashMap 也可以通过 Collections.synchronizedMap() 方法对自身方法进行 synchronized 加锁处理,但是跟 HashTable 一样,在多线程环境下是串行化的执行,性能较差。
3.2 ConcurrentHashMap
为了提升 HashMap 多线程下的性能,JDK1.5 引入了 ConcurrentHashMap 这个类。
ConcurrentHashMap 是出自 JUC 包(java.util.concurrent)的,ConcurrentHashMap 和 HashMap 非常类似,包括属性参数。
HashMap 数据结构:
早期的 ConcurrentHashMap 是通过分段锁技术来实现的,将 HashMap 的 table 数组逻辑上拆分成多个子数组,默认会分成 16 段,每个段配一把锁,这样多个线程如果操作的是不同的段,则不会被阻塞,理论上会比 HashTable 的效率提升 16 倍。
JDK 1.8 ConcurrentHashMap 的实现取消了分段锁,而 CAS + synchronized 使锁更细化。同时对结构也做了进一步优化,跟 JDK 1.8 的 HashMap 一样,由数据 + 链表 + 红黑树实现,synchronized 只锁定当前链表或者红黑树的首节点,这样只要 hash 不冲突,就不会产品线程安全问题,效率得到了进一步的提高。
ConcurrentHashMap put() 方法的逻辑:
- 若 table 数组未被初始化,则进行初始化操作;
- 对 key 计算 hash 值,依据 hash 值计算 table 数组下标;
- 若未发生碰撞,即该下标的 “桶” 还没有存储头节点,则使用 CAS 操作放入 “桶” 的头节点中,添加失败则循环重试;
- 检查到内部正在扩容,就帮助它一块扩容;
- 若发生碰撞,即该下标的 “桶” 已经存储了头节点,则使用 synchronized 锁住头节点(链表或者红黑树),如果是链表结构则执行链表的添加操作,如果是红黑树结构则执行树添加操作;
- 若链表的长度超过树化阈值 (TREEIFY_THRESHOLD,默认 8)则被改造成红黑树,而当删除操作导致低于最低树化阈值(UNTREEIFY_THRESHOLD,默认 6)的时候,红黑树则转回成链表,保证更高的性能;
- 若键值对已经存在,则用新值替换旧值;
- 若某个 “桶” 满了(默认容量 16 * 扩容因子 0.75 = 12,则默认超过 12 就会扩容),就需要 resize(扩容 2 倍后重排)。
ConcurrentHashMap size() 方法和 mappingCount() 方法的异同,两者计算是否准确?
多线程环境下 ConcurrentHashMap 如何扩容?
HashMap、HashTable、ConcurrentHashMap的区别
3.3 TreeMap
底层红黑树,每个 key-value 对即作为红黑树的一个节点。TreeMap 支持两种排序方式:自然排序和定制排序,TreeMap 的使用方法这里不再详细给出,建议查看 API 文档学习。
4. Queue集合
多线程 | 底层 | 说明 | |
---|---|---|---|
ArrayDeque | 线程不安全 | 数组 | |
LinkedList | 线程不安全 | 双向循环链表 |
4.1 ArrayDeque
Deque 接口实现了 Queue 接口,代表双端队列,Deque 接口里定义了一些双端队列的方法,这些方法允许从两端来操作队列的元素。Deque 接口提供了一个典型的实现类 ArrayDeque,ArrayDeque 是基于数组实现的双端队列,ArrayDeque 不仅可以当成 “栈” 使用,而且还可以当成 “队列” 使用。
把 ArrayDeque 当成 “栈” 使用:
ArrayDeque<Integer> stack = new ArrayDeque<Integer>();
// 将元素push入"栈"
stack.push(2);
// 访问第一个元素, 但并不将其pop出"栈"
Integer element = stack.peek();
// pop出第一个元素
Integer element1 =stack.pop();
把 ArrayDeque 当成 “队列” 使用:
ArrayDeque<Integer> queue = new ArrayDeque<Integer>();
// 将元素加入"队列"
queue.push(2);
// 访问队列头部的元素, 但并不将其poll出"队列"
Integer element = queue.peek();
// poll出第一个元素
Integer element1 =queue.poll();
5. Collections
Collections 工具类提供了大量方法对集合元素进行查询、排序、修改等操作:
// 对Collection集合进行查找
Collections.max(collection); // 获取集合最大元素
Collections.min(collection); // 获取集合最小元素
Collections.frequency(collection , 1); // 判断1在集合中出现的次数
// 同步控制, 创建线程安全的集合对象
Collection c = Collections.synchronizedCollection(new ArrayList());
List l = Collections.synchronizedList(new ArrayList());
Set s = Collections.synchronizedSet(new HashSet());
Map m = Collections.synchronizedMap(new HashMap());
// 对List集合元素进行排序
Collections.reverse(list); // 将元素的次序反转
Collections.sort(list); // 将元素的按自然顺序排序
Collections.shuffle(list); // 将元素的按随机顺序排序
// 对List集合元素进行替换
Collections.replaceAll(list , 0 , 1); // 将List中的0使用1来代替
Collections.binarySearch(list , 1); // 使用二分法搜索指定的List集合, 以获得List集合中的索引, 只有排序后的List集合才可用二分法查询
// 设置不可变集合, 如果试图改变, 将引发UnsupportedOperationException异常
List unList = Collections.emptyList(); // 创建一个空的、不可改变的List对象
Set unSet = Collections.singleton(2); // 创建一个只有一个元素, 且不可改变的Set对象
Map<String, Object> map = new HashMap<String, Object>();
map.put("C" , 100);
Map unMap = Collections.unmodifiableMap(map); // 返回普通Map对象对应的不可变版本
除了使用 Collections 工具类创建线程安全的集合对象外,java.util.concurrent 包还提供了 HashMap 等集合线程安全的高效实现:ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet、ConcurrentLinkedDeque。这些集合使用复杂的算法,通过允许并发地访问数据结构的不同部分来使竞争极小化。
6. 常见问题
1、TreeMap、HashMap、LinkedHashMap 的区别?
这三种集合都是非线程安全的。
HashMap 键值无序,但是结果一般会按照自然结果排序,底层 hash 表;
LinkedHashMap 是 HashMap 的子类,增加了一个链表,用于记录插入时的顺序,所以可以保证插入的顺序,底层 hash 表;
TreeMap 键值有序,底层红黑树,默认按键的自然顺序排序,原理:按照实现的 Comparable 接口的 compareTo 这个方法的比较结果进行存储的;
注意:LinkedHashMap 记录的是插入顺序,TreeMap 记录的是自然顺序。
2、ArrayList 和 LinkedList 性能对比?
由于数组以一块连续内存区保存所有数组元素,所以内部以数组作为底层实现的集合在随机访问时性能最好;而内部以链表作为底层实现的集合在执行插入、删除操作时有较好的性能。总体 ArrayList 性能优于 LinkedList,大部分考虑使用 ArrayList。
- 对于遍历List集合元素,ArrayList最好使用随机访问方法(get)来遍历,这样性能最好;LinkedList则最好用迭代器(Iterator)来遍历集合元素;
- 如果要经常插入删除大量数据的List,建议使用LinkedList。
3、HashSet、TreeSet 性能?
HashSet 性能总是比 TreeSet 好,特别是添加、查询等操作,因为 TreeSet 需要额外的红黑树算法来维护集合元素次序。只有当需要一个保持排序的 Set 时才用 TreeSet。
参考:
http://tool.oschina.net/apidocs/apidoc?api=jdk-zh