容器的思维导图:
Java 的 List 是非常常用的数据类型。List 是有序的 Collection。Java List 一共三个实现类: 分别是 ArrayList、Vector 和 LinkedList。
Set 注重独一无二的性质,该体系集合用于存储无序(存入和取出的顺序不一定相同)元素,值不能重 复。对象的相等性本质是对象 hashCode 值(java 是依据对象的内存地址计算出的此序号)判断 的,如果想要让两个不同的对象视为相等的,就必须覆盖 Object 的 hashCode 方法和 equals 方 法。
public class ArrayListextends AbstractList implements List , RandomAccess, Cloneable, java.io.Serializable
ArrayList 是基于数组实现的,所以支持快速随机访问。 继承了AbstractList,实现了List。它是一个数组队列,提供了相关的添加、删除、修改、遍历等功能。
RandomAccess 接口标识着该类支持快速随机访问(只是一个定义了类型的接口,无作用)。 Cloneable 接口,即覆盖了函数 clone(),能被克隆。 java.io.Serializable 接口,这意味着ArrayList支持序列化,能通过序列化去传输。
数组的默认大小为 10。
核心方法:
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);
}
整个流程就是对旧数组位移运算得到新数组,然后把旧数组整个复制到新数组上,操作代价很高,新容量的大小为 oldCapacity + (oldCapacity >> 1)
,也就是旧容量的 1.5 倍。
补充:
移位运算符简介:移位运算符就是在二进制的基础上对数字进行平移。按照平移的方向和填充数字的规则分为三种:<<(左移)、>>(带符号右移)和>>>(无符号右移)。作用:对于大数据的2进制运算,位移运算符比那些普通运算符的运算要快很多,因为程序仅仅移动一下而已,不去计算,这样提高了效率,节省了资源 。
在末尾添加元素:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
这里看到ArrayList在末尾添加元素的实质就相当于为数组赋值。
在指定位置添加元素:
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element; size++;}
让数组自己复制自己实现让index开始之后的所有成员后移一个位置。
删除指定元素:
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index, numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
流程是把需要删除的元素右边的元素向左移动一位,覆盖了需要删除的元素。调用了arraycopy,所以操作代价也很高。
concurrent 并发包下的类,是ArrayList的线程安全解决方案, 通过ReentrantLock获取对象锁的方式来实现线程安全。
读写分离的特点
读:
@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
return (E) a[index];
}
写:
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();
}
}
final void setArray(Object[] a) {
array = a;
}
写的操作需要加锁,防止并发写入导致数据丢失,不直接操作原数组,先copy一个数组进行操作,写完后setArray方法把新的复制数组赋值给旧数组。
CopyOnWriteArrayList 在写操作的同时允许读操作,大大提高了读操作的性能,因此很适合读多写少的应用场景。
缺点:
内存占用:在写操作时需要复制一个新的数组,使得内存占用为原来的两倍左右;
数据不一致:读操作不能读取实时性的数据,因为部分写操作的数据还未同步到读数组中。
所以 CopyOnWriteArrayList 不适合内存敏感以及对实时性要求很高的场景。
1. 概览
内部私有类Node:
private static class Node {
E item;
Node next;
Node prev;
}
定义:
transient Node first;
transient Node last;
补充:transient关键字标记的成员变量不参与序列化过程。
综上可看出,LinkedList是由双向列表实现,使用Node存储节点信息,每个节点都有前节点(next),本节点(item),后节点(prev)。
2.与 ArrayList 的比较
1.整体原理分析
HashMap类中有一个非常重要的字段,就是 Node[] table,即哈希桶数组
static class Node implements Map.Entry {
final int hash;
final K key;
V value;
Node next;
Node(int hash, K key, V value, Node next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
Node是HashMap的一个内部类,实现了Map.Entry接口,本质是就是一个映射(键值对)。
hash:hashcode经过扰动函数得到的值, 然后通过 (n - 1) & hash
判断当前元素存放的位置,如果当前
位置存在hash和key值不相同的元素就使用拉链法解决冲突。
“拉链法” 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
Node
所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。
底层存储结构:
2.put方法分析
map.put("a","b")的整个流程:
1. 先判断散列表是否没有初始化或者为空,如果是就扩容
2. 根据键值 key 计算 hash 值,得到要插入的数组索引
3. 判断要插入的那个数组是否为空:
1. 如果为空直接插入。
2. 如果不为空,判断 key 的值是否是重复(用 equals 方法):
1. 如果是就直接覆盖
2. 如果不重复就再判断此节点是否已经是红黑树节点:
1. 如果是红黑树节点就把新增节点放入树中
2. 如果不是,就开始遍历链表:
1. 循环判断直到链表最底部,到达底部就插入节点,然后判断是否大于链表长度是否大于8:
1. 如果大于8就转换为红黑树
2. 如果不大于8就继续下一步
2. 到底部之前发现有重复的值,就覆盖。
4. 判断是否需要扩容,如果需要就扩容。
下面上源码:
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) {
//tab: 引用当前hashMap的散列表
//p: 表示当前散列表的元素
//n: 表示散列表数组的长度
//i: 表示路由寻址结果
Node[] tab; Node p; int n, i;
// table未初始化或者长度为0,进行扩容(采用了延时初始化,第一次put才会初始化散列表。)
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 寻址找到的桶位为null
//(n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中)
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 寻址找到的桶位已经存在元素
else {
Node e; K k;
// 比较桶中第一个元素(数组中的结点)的hash值相等,key相等,也就是判断是否是重复的值。
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 完全一致
//e:找到的一个与当前要插入的元素一直的元素
e = p;
// hash值不相等,即key不相等;且为红黑树结点
else if (p instanceof TreeNode)
// 放入树中
e = ((TreeNode)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;
}
// 判断链表中结点的key值与插入的元素的key值是否相等
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 相等,跳出循环
break;
// 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
p = e;
}
}
// 表示在桶中找到key值、hash值与插入元素相等的结点
if (e != null) {
// 记录e的value
V oldValue = e.value;
// onlyIfAbsent为false或者旧值为null
if (!onlyIfAbsent || oldValue == null)
//替换
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
// 结构性修改
++modCount;
// 实际大小大于阈值则扩容
if (++size > threshold)
resize();
// 插入后回调
afterNodeInsertion(evict);
return null;
}
tip: JDK1.7之前的put方法和现在流程不同的地方就是采用头插法插入元素。
3.扩容(resize方法)
扩容会伴随着一次重新hash分配,并且会遍历hash表中所有的元素,是非常耗时的。在编写程序中,要尽量避免resize。 源码在下面,仔细阅读即可理解此方法。
final Node[] resize() {
Node[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
//散列表已经被初始化了,是一次正常扩容
if (oldCap > 0) {
// 超过最大值就不再扩充了
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 没超过最大值,就扩充为原来的2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold:翻倍
}
//散列表没有被初始化
else if (oldThr > 0) // 初始化的容量是已经指定了的
newCap = oldThr;
else {
// 默认初始化容量
newCap = DEFAULT_INITIAL_CAPACITY;//16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//12
}
// 计算新的resize上限
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node[] newTab = (Node[])new Node[newCap];
table = newTab;
if (oldTab != null) {
// 把每个bucket都移动到新的buckets中
for (int j = 0; j < oldCap; ++j) {
Node e;
//当前桶位有数据,但不清楚是什么数据。
if ((e = oldTab[j]) != null) {
//方便 GC
oldTab[j] = null;
//如果是单个元素
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
//如果是红黑树
else if (e instanceof TreeNode)
((TreeNode)e).split(this, newTab, j, oldCap);
else { //桶位已经形成链表
Node loHead = null, loTail = null;//低位链表
Node hiHead = null, hiTail = null;//高位链表
Node next;
do {
next = e.next;
// 原索引,给低位链表赋值
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
// 原索引+oldCap,给高位链表赋值
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 原索引放到桶里
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 原索引+oldCap放到桶里
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
在扩容时看原hash值新增的那个bit位是1还是0就好了,是0的话索引没有变,是1的话索引变成“原索引+oldCap(旧数组大小)”
JDK1.7:
ConcurrentHashMap 和 HashMap 实现上类似,最主要的差别是 ConcurrentHashMap 采用了分段锁(Segment),每个分段锁维护着几个桶(HashEntry),多个线程可以同时访问不同分段锁上的桶,从而使其并发度更高(并发度就是 Segment 的个数)。 Segment 继承自 ReentrantLock ,默认的并发级别为 16 。
简单理解就是,ConcurrentHashMap 是一个 Segment 数组,Segment 通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每 个 Segment 是线程安全的,也就实现了全局的线程安全。
结构如图:
JDK1.8:
使用了 CAS 操作来支持更高的并发度,在 CAS 操作失败时使用内置锁 synchronized。 synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。
实现原理: HashSet底层由HashMap实现 ,值存放于HashMap的key上 ,HashMap的value统一为PRESENT 。
检查重复: 先对插入的元素的hashcode值和现有的元素的hashcode作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现,直接插入。但是如果发现有相同hashcode值的对象,这时会调用equals()
方法来检查hashcode相等的对象是否真的相同。 如果两者相同,HashSet就不会让加入操作成功 。
class LRUCache extends LinkedHashMap {
private static final int MAX_ENTRIES = 3;
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > MAX_ENTRIES;
}
LRUCache() {
super(MAX_ENTRIES, 0.75f, true);
}
}
public static void main(String[] args) {
LRUCache cache = new LRUCache<>();
cache.put(1, "a");
cache.put(2, "b");
cache.put(3, "c");
cache.get(1);
cache.put(4, "d");
System.out.println(cache.keySet());
}
通过阅读 TreeMap 的 put 方法的源码发现:TreeMap 实现元素不重复就是通过调用 compareTo 方法,而要使用 compareTo 方法就必须实现Compare接口。
ArrayBlockingQueue 底层是数组,有界队列,如果我们要使用生产者-消费者模式,这是非常好的选择。
LinkedBlockingQueue 底层是链表,可以当做无界和有界队列来使用,所以大家不要以为它就是无界队列。
DelayQueue是一个无界阻塞队列,只有在延迟期满时才能从中提取元素。该队列的头部是延迟期满后保存时间最长的Delayed 元素。
SynchronousQueue 本身不带有空间来存储任何元素,使用上可以选择公平模式和非公平模式。
PriorityBlockingQueue 是无界队列,基于数组,数据结构为二叉堆,数组第一个也是树的根节点总是最小值。