提示:本章节为《java面试系列》
上一章:java中IO
java容器
容器关系总图
集合介绍
-
Set
1、TreeSet:基于红黑树实现,支持有序性操作,例如根据一个范围查找元素的操作。但是查找效率不如 HashSet,HashSet 查找的时间复杂度为 O(1),TreeSet 则为 O(logN)。2、HashSet:基于哈希表实现,支持快速查找,但不是有序的。
3、 LinkedHashSet:具有 HashSet 的查找效率,并且内部使用双向链表维护元素的插入顺序。
-
List
1、 ArrayList:基于动态数组实现,支持随机访问。2、Vector:和 ArrayList 类似,但它是线程安全的。
3、LinkedList:基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList 还可以用作栈、队列和双向队列。
-
Queue
1、LinkedList:可以用它来实现双向队列。2、PriorityQueue:基于堆结构实现,可以用它来实现优先队列。
-
Map
1、TreeMap:基于红黑树实现。2、HashMap:基于哈希表实现。
3、HashTable:和 HashMap 类似,但它是线程安全的,这意味着同一时刻多个线程同时写入 HashTable 不会导致数据不一致。它是遗留类,不应该去使用它,而是使用 ConcurrentHashMap 来支持线程安全,ConcurrentHashMap 的效率会更高,因为 ConcurrentHashMap 引入了分段锁。
4、 LinkedHashMap:使用双向链表来维护元素的顺序,顺序为插入顺序或者最近最少使用(LRU)顺序。
注意:
Java中的集合,从上层接口上看分为了两类,Map和Collection。也就是说,我们平时接触到的常用的集合,包括HashMap,ArrayList和HashSet等都直接或者间接的实现了这两个接口之一。而Collection接口的子接口又包括了Set和List接口。这样我们常见的Map,Set和List三大集合接口就出来了。接口类图如下所示:
Map是和Collection并列的集合上层接口,没有继承关系;List和Set是Collection的子接口。
集合中的设计模式
迭代
iterator()—(增强for循环)
适配器
把数组类型转换为 List 类型。如:
List list = Arrays.asList(3, 4, 5);
底层原理
对于JDK1.8
- ArrayList
1、概要
该集合是基于数组实现的,默认的大小是10,可以“自动”动态扩大容量,提供快速随机访问。
2、扩容
oldCapacity + (oldCapacity >> 1),为旧容量的 1.5 倍。扩容时把原数组复制到新数组,所以空间资源浪费较大。
3、删除元素
时间复杂度为n,移动删除元素后面的元素到前面。
4、序列化
对象转换为字节流并输出,反序列化就是相反。
5、速度
因为没有同步,所以访问等操作较快,但在多线程的情况下,不安全。
6、快速失败
modCount 用来记录 ArrayList 结构发生变化的次数。结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化。
在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果改变了需要抛出 ConcurrentModificationException。
安全实现
List<String> list = new ArrayList<>();
List<String> synList = Collections.synchronizedList(list);
也可以使用 concurrent 并发包下的 CopyOnWriteArrayList 类。适合读多写少的应用场景。(但内存浪费,数据不能实时)
List<String> list = new CopyOnWriteArrayList<>();
- Vector
安全
使用关键字synchronized同步,如在add,get方法上。(不排除一些特殊情况,导致不安全。就是说不能说100%安全)
扩容
默认情况扩容时每次都令 capacity 为原来的两倍,具体情况查找源码。
性能
因为加了锁,所以进行操作的时候,要申请锁等资源,所以访问速度较ArrayList差了点,但在多线程的操作情况下一般是安全的。
-LinkedList
1、概要
它是基于双向链表实现的,在jdk1.8中用node存储。它和ArrayList的区别,也是链表与数组的区别。
- HashMap
主要介绍JDK1.7先
1. 存储结构
jdk1.7 数组+链表(jdk1.8 数组+链表+红黑树)
2.工作原理
- 拉链法
链表插入是通过头插法方式进行的, - 查找
计算键值对所在的桶;
在链表上顺序查找,时间复杂度显然和链表的长度成正比。
3.put操作
简单理解就是首先如果key存在,就替换旧值,否则就直接插入。
注意:HashMap 使用第 0 个桶存放键为 null 的键值对。(因为无法调用 null 的 hashCode() 方法)
4.put过程
4.1 桶下标值
确定一个键值对所在的桶下标。如下代码所示:
int hash = hash(key);
int i = indexFor(hash, table.length);
4.2 hash值
计算过程如下:
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
4.3 取模
key 的 hash 值对桶个数取模:hash%capacity。
5. 扩容原理
capacity:table 的容量大小,默认为 16。需要注意的是 capacity 必须保证为 2 的 n 次方。
size:键值对数量。
threshold:size 的临界值,当 size 大于等于 threshold 就必须进行扩容操作。
loadFactor:装载因子,table 能够使用的比例,threshold = (int)(capacity* loadFactor)。
当需要扩容时,令 capacity 为原来的两倍。
扩容使用 resize() 实现,需要注意的是,扩容操作同样需要把 oldTable 的所有键值对重新插入 newTable 中,因此这一步是很费时的。
6. 重新计算桶下标
在进行扩容时,需要把键值对重新计算桶下标,从而放到对应的桶上
7. 计算数组容量
HashMap 构造函数允许用户传入的容量不是 2 的 n 次方,因为它可以自动地将传入的容量转换为 2 的 n 次方。
8.链表转红黑树
JDK 1.8 开始,一个桶存储的链表长度大于等于 8 时会将链表转换为红黑树。
杂谈
Hashtable 使用 synchronized 来进行同步。
HashMap 可以插入键为 null 的 Entry。
HashMap 的迭代器是 fail-fast 迭代器。
HashMap 不能保证随着时间的推移 Map 中的元素次序是不变的。
-ConcurrentHashMap
结构
数组+链表+红黑树基于分段锁(Segment),CAS 。
Segment (默认16)继承自 ReentrantLock。
static final class Segment<K,V> extends ReentrantLock implements Serializable {
private static final long serialVersionUID = 2249069246763182397L;
static final int MAX_SCAN_RETRIES =
Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
transient volatile HashEntry<K,V>[] table;
transient int count;
transient int modCount;
transient int threshold;
final float loadFactor;
}
size操作
执行 size 操作时,需要遍历所有 Segment 然后把 count 累计起来(基于CAS)。如果尝试的次数超过 3 次,就需要对每个 Segment 加锁。
新改进
JDK 1.7 使用分段锁机制来实现并发更新操作,核心类为 Segment,它继承自重入锁 ReentrantLock,并发度与 Segment 数量相等。
JDK 1.8 使用了 CAS 操作来支持更高的并发度,在 CAS 操作失败时使用内置锁 synchronized。
并且 JDK 1.8 的实现也在链表过长时会转换为红黑树。
-LinkedHashMap
结构
双向链表+红黑树。一个双向链表,用来维护插入顺序或者 LRU 顺序。
实现细节
afterNodeAccess(),afterNodeInsertion(),removeEldestEntry() 等函数感兴趣可以自己查看源码或者查阅资料。
- LRU 缓存
。。。
-WeakHashMap
结构
WeakHashMap 的 Entry 继承自 WeakReference,被 WeakReference 关联的对象在下一次垃圾回收时会被回收。
WeakHashMap 主要用来实现缓存,通过使用 WeakHashMap 来引用缓存对象,由 JVM 对这部分缓存进行回收。
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V>
-ConcurrentCache
Tomcat 中的 ConcurrentCache 使用了 WeakHashMap 来实现缓存功能。
ConcurrentCache 采取的是分代缓存:
经常使用的对象放入 eden 中,eden 使用 ConcurrentHashMap 实现,不用担心会被回收(伊甸园);
不常用的对象放入 longterm,longterm 使用 WeakHashMap 实现,这些老对象会被垃圾收集器回收。
当调用 get() 方法时,会先从 eden 区获取,如果没有找到的话再到 longterm 获取,当从 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);
}
}