容器
Hash表
散列表根据关键字值进行访问儿都数据结构,把关键字值映射到一个位置访问记录,加快查找的速度
避免hash冲突
拉链法
多个相同的key连到一个链表里
线性探测法
大小为M的数组保存N个键值对,M>N
概览
主要包括Collection和Map两种,Collection存储着对象的集合,Map存储着键值对的映射表
Collection
- Set
- TreeSet :基于红黑树实现,支持有序操作,范围内查找元素。查找效率不如HashSet,HashSet 查找的时间复杂度为 O(1),TreeSet 则为 O(logN)。
- HashSet:基于HashMap,支持快速查找,不支持有序性操作。失去元素插入顺序信息,使用Iterator遍历得到HashSet结果是不确定的
- LinkedHashSet:具有HashSet查找效率,内部使用双向链表维护元素的插入顺序
- List
- ArrayList,动态数组,支持随机访问
- Vector:线程安全
- LinkedList:双向链表,只能顺序访问,快速在链表中插入,删除元素。可以做栈,队列,双向队列
- Queue
- LinkedList:双向队列
- PriorityQueue:堆结构实现,优先队列
Map
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D2P1nvUb-1630569077526)(C:\Users\MECHREVCO\Desktop\新征程\pic\image-20210804164304145.png)]
- TreeMap:红黑树
- HashMap:哈希表
- HashTable:线程安全,遗留类,应该使用ConcurrentHashMap
- LinkedHashMap:双向链表维护元素顺序,顺序为插入顺序或最近最少使用(LRU)顺序
List源码
ArrayList
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JrY8vYgc-1630569077530)(C:\Users\MECHREVCO\Desktop\新征程\pic\image-20210804172310816.png)]
长度为10,从1开始计数;index表示数组下标,从0开始计数
- DEFAULT_CAPACITY 数组初始长度 默认是10
- size,当前数组大小,int ,没有volatile修饰,非线程安全的
- modCount,记录被修改的版本次数,数组变动+1
类注释
- 允许put null 会自动扩容
- size,isEmpty,get,set,add等方法时间复杂度都是O(1);
- 非线程安全的,synchronizedList
- 增强for循环,或迭代器迭代时,数组大小被改变,会快速失败
初始化
三种初始化
-
无参,数组大小为空
//一开始为空,不是10 public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }
-
指定大小初始化
-
指定初始数据初始化
添加 add与扩容
扩容本质:
Arrays.copyOf()本地方法,先新建符合预期容量的新数组,老数组拷贝进去
-
先确保内部容量,ensureCapacityInternal,判断是否为空,为空则默认长度,非空则+1
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) {
// 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); //复制扩容
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
需要注意:
- 扩容不是翻倍,是原来1.5倍
- ArrayList数组最大值是 Integer.MAX_VALUE,超过这个值,JVM不会分配内存空间
- 新增时可以有null;
- 源码扩容时,有数组大小溢出意识
删除:
-
值删除
public boolean remove(Object o) { if (o == null) { //判断是否为null for (int index = 0; index < size; index++) if (elementData[index] == null) { fastRemove(index); //快速删除 return true; } } else { for (int index = 0; index < size; index++) if (o.equals(elementData[index])) { //equals要看具体实现 fastRemove(index); return true; } } return false; } private void fastRemove(int index) { modCount++; //版本+1 int numMoved = size - index - 1; if (numMoved > 0) //底层还是调用System.arraycopy System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; // clear to let GC do its work,最后一个位置为null帮助GC }
-
下标删除
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; }
-
批量删除
迭代器
-
hasNext
public boolean hasNext() { return cursor != size;//cursor 表示下一个元素的位置,size 表示实际大小,如果两者相等,说明已经没有元素可以迭代了,如果不等,说明还可以迭代 }
-
next
public E next() { //迭代过程中,判断版本号有无被修改,有被修改,抛 ConcurrentModificationException 异常 checkForComodification(); //本次迭代过程中,元素的索引位置 int i = cursor; if (i >= size) throw new NoSuchElementException(); Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) throw new ConcurrentModificationException(); //如果现在的长度不匹配抛出异常 // 下一次迭代时,元素的位置,为下一次迭代做准备 cursor = i + 1; // 返回元素值 return (E) elementData[lastRet = i]; } // 版本号比较 final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }
-
remove
public void remove() { // 如果上一次操作时,数组的位置已经小于 0 了,说明数组已经被删除完了 if (lastRet < 0) throw new IllegalStateException(); //迭代过程中,判断版本号有无被修改,有被修改,抛 ConcurrentModificationException 异常 checkForComodification(); try { ArrayList.this.remove(lastRet); cursor = lastRet; // -1 表示元素已经被删除,这里也防止重复删除 lastRet = -1; // 删除元素时 modCount 的值已经发生变化,在此赋值给 expectedModCount // 这样下次迭代时,两者的值是一致的了 expectedModCount = modCount; } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } }
线程安全
作为共享变量时是不安全的,使用SynchronizedList或vector,可以用CopyOnWriteArrayList保证线程安全,每个方法加了Synchronized
Vector
方法与ArrayList一样,加了synchronized修饰
扩容
是两倍不是1.5倍了
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);
}
ArrayList不同
vector是同步的,开销比ArrayList大,速度慢
LinkedList
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VNWYCLqf-1630569077534)(pic\image-20210810200813271.png)]
- 每个节点叫node,有prev和next,first的prev为空,last的next为空
- 没有数据时,last和first一个节点,前后都空
add
-
add&addLast都是从尾部插入
void linkLast(E e) { final Node<E> l = last; final Node<E> newNode = new Node<>(l, e, null); last = newNode; if (l == null) //如果last为空表示目前链表里没东西 first = newNode; else l.next = newNode; size++; modCount++; } void linkBefore(E e, Node<E> succ) { // assert succ != null; final Node<E> pred = succ.prev; final Node<E> newNode = new Node<>(pred, e, succ); succ.prev = newNode; if (pred == null) first = newNode; else pred.next = newNode; size++; modCount++; }
-
addFirst
private void linkFirst(E e) { final Node<E> f = first; final Node<E> newNode = new Node<>(null, e, f); first = newNode; if (f == null) last = newNode; else f.prev = newNode; size++; modCount++; }
remove
remove默认removeFirst还有removeLast
get
Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) { //分成两部分 从first开始或者从last开始
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
迭代器
Iterator只支持从头到尾的访问,还有个迭代接口ListIterator,向前和向后的访问
- 从头到尾:next,hasNext,nextIndex
- 从尾到头:previous,hasPrevious,previousIndex
Map源码
HashMap
数组+链表+红黑树,链表长度>8,并且hashmap容量>64,才会转化为红黑树。红黑树大小<6时转化为链表,初始容量16,影响因子0.75,modcount会快速失败
数组查找复杂度O(1),链表O(n),红黑树O(log(n))
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E2jTs6FK-1630569077538)(pic\image-20210812173313513.png)]
类注释
- 允许null
- 很多数据时建议固定容量
- 影响因子,均衡空间时间,值越大扩容减少,但是hash冲突增加
扩容有两种情况
- 初始化时给定数组大小,通过tableSizeFor计算,数组大小永远接近2的幂次,给定19则实际初始化32
- resize扩容则大小 = 数组容量*0.75
新增
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//如果hashmap为空或长度为0则resize
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//如果数组(buket)对应的下标内容为空直接赋值
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//如果不为空,有对应的值
else {
Node<K,V> e; K k;
//如果数组内容有值且key等于内容的key则直接替换
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就只有是链表类型了
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向后移
p = e;
}
}
//如果e不为空,则说明已经新增位置已经找到
if (e != null) { // existing mapping for key
V oldValue = e.value;
//当onlyIfAbsent为false时才覆盖
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
//返回旧值
return oldValue;
}
}
//版本++
++modCount;
//如果实际容量大于扩容门槛,开始扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
链表新增
链表长度>8,并且hashmap容量>64才会转化为红黑树
为什么是8,泊松分布,当链表长度为8的时候,出现的概率为 0.00000006,不到千万分之一
- 首先判断新增的节点在红黑树上是不是已经存在,判断手段有如下两种:
1.1. 如果节点没有实现 Comparable 接口,使用 equals 进行判断;
1.2. 如果节点自己实现了 Comparable 接口,使用 compareTo 进行判断。
- 新增的节点如果已经在红黑树上,直接返回;不在的话,判断新增节点是在当前节点的左边 还是右边,左边值小,右边值大;
- 自旋递归 1 和 2 步,直到当前节点的左边或者右边的节点为空时,停止自旋,当前节点即为 我们新增节点的父节点;
- 把新增节点放到当前节点的左边或右边为空的地方,并于当前节点建立父子节点关系;
- 进行着色和旋转,结束。
查找
- 根据hash算法定位数组索引,,equals判断
- 当前节点有没有next,有的话是链表还是红黑树
- 走链表or红黑树查找方法
链表查找–自旋
do {
// 如果当前节点 hash 等于 key 的 hash,并且 equals 相等,当前节点就是我们要找的节点
// 当 hash 冲突时,同一个 hash 值上是一个链表的时候,我们是通过 equals 方法来比较
key 是否相等的
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
// 否则,把当前节点的下一个节点拿出来继续寻找
} while ((e = e.next) != null);
红黑树查找
- 根节点递归查找
- hashcode比较左右节点,左小右大
- 判断 有无定位,无的话重复23
- 自旋定位
WeakHashMap
Entry继承WeakReference,被WeakReference 关联的对象在下一次垃圾回收时会被回收。
WeakHashMap 主要实现缓存,通过使用WeakHashMap 引用缓存对象,由JVM回收。
ConcurrentCache
Tomcat的ConcurrentCache就是WeakHashMap 实现
TreeMap
TreeMap底层数据结构就是红黑树
比较器,如果有外部传进来的Comparator比较器优先使用外部;如果外部比较器为空,则使用key自己实现的Comparable的compareTo方法
containsKey,get,put,remove方法时间复杂度都是log(n)
put
public V put(K key, V value) {
Entry<K,V> t = root;
//判断根节点是否为空
if (t == null) {
//compare保证key不为null
compare(key, key); // type (and possibly null) check
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
Entry<K,V> parent;
// split comparator and comparable paths,如果自带Comparator
Comparator<? super K> cpr = comparator;
if (cpr != null) {
//自旋,不断地找父节点,直到父节点为空
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
//如果相等,直接覆盖
else
return t.setValue(value);
} while (t != null);
}
else {
//插入节点不能为空
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
//自身的Comparable
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);
}
Entry<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0)
parent.left = e;
else
parent.right = e;
//开始染色旋转
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
- 新增节点时,利用红黑树的左小右大特性,从根节点往下查找,直到节点是null
- 查找时发现key已经存在直接覆盖
- TreeMap禁止key是null
get
一样的逻辑,先看比较器,再自旋查找
public V get(Object key) {
Entry<K,V> p = getEntry(key);
return (p==null ? null : p.value);
}
final Entry<K,V> getEntry(Object key) {
// Offload comparator-based version for sake of performance
if (comparator != null)
return getEntryUsingComparator(key);
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
Entry<K,V> p = root;
while (p != null) {
int cmp = k.compareTo(p.key);
if (cmp < 0)
p = p.left;
else if (cmp > 0)
p = p.right;
else
return p;
}
return null;
}
remove
之后补充
LinkedHashMap
HashMap无序的,TreeMap按照key排序,LinkedHashMap维护插入顺序
本身继承HashMap,还有两大特性:
- 按照插入顺序进行访问
- 实现访问最少最先删除,把很久没有访问的key自动删除
结构
static class Entry<K,V> extends HashMap.Node<K,V> {
//为node添加一个before和after
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
private static final long serialVersionUID = 3801124242820219131L;
/**
* The head (eldest) of the doubly linked list.
* 链表头
*/
transient LinkedHashMap.Entry<K,V> head;
/**
* The tail (youngest) of the doubly linked list.
* 链表尾
*/
transient LinkedHashMap.Entry<K,V> tail;
/**
* 控制两种访问模式字段,默认false
* true按照访问顺序,把经常访问的key放到队尾
* false按插入顺序访问
*/
final boolean accessOrder;
新增
新增继承hashmap的put,但是重写了newNode/newTreeNode方法。使新增节点加到链表尾部
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
LinkedHashMap.Entry<K,V> last = tail;
tail = p;
//如果last为空,链表为空,首尾节点相等
if (last == null)
head = p;
//有数据建立新增与上个节点的前后关系
else {
p.before = last;
last.after = p;
}
}
按顺序访问
LinkedHashMap只提供单向访问,按照插入的顺序从头到尾。
通过迭代器访问,entrySet.Iterator;keySet.Iterator;valueSet.Iterator
LRU策略
Least recently used最近最少使用;经常访问的元素被追加到队尾,不常访问的数据靠近队头;通过设置删除策略,把头结点删除
//简单实现
public static void testAccessOrder() {
// 新建 LinkedHashMap
LinkedHashMap<Integer, Integer> map = new LinkedHashMap<Integer,
Integer>(4, 0.75f, true) {
{
put(10, 10);
put(9, 9);
put(20, 20);
put(1, 1);
}
@Override
protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
return size() > 3;
}
};
log.info("初始化:{}",JSON.toJSONString(map));
Assert.requireNonEmpty(map.get(9));
log.info("map.get(9):{}",JSON.toJSONString(map));
Assert.requireNonEmpty(map.get(20));
log.info("map.get(20):{}",JSON.toJSONString(map));
}
//结果
14:14:24.126 [main] INFO Demo - 初始化:{9:9,20:20,1:1}
14:14:24.130 [main] INFO Demo - map.get(9):{20:20,1:1,9:9}
14:14:24.130 [main] INFO Demo - map.get(20):{1:1,9:9,20:20}
实现方法
get方法中有一个是否开启LRU判断,get,getOrDefault,computeIfAbsent,merge,computeIfPresent
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
//是否开启LRU,将当前key移到队尾
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
删除策略
LinkedHashMap本身没有put方法,调用HashMap的put,有一个afterNodeInsertion实现删除
// 删除很少被访问的元素,被 HashMap 的 put 方法所调用
void afterNodeInsertion(boolean evict) {
// 得到元素头节点
LinkedHashMap.Entry<K,V> first;
// removeEldestEntry 来控制删除策略,如果队列不为空,并且删除策略允许删除的情况下,删除头节点
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
// removeNode 删除头节点
removeNode(hash(key), key, null, false, true);
}
}
Map的hash算法
static final int hash(Object key) {
int h;
//先使用底层hashcode算法,计算key的hash值,然后将h向右移动16位使hash值更分散,再和异或时高16位和低16位都能参与计算
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//元素下标位置,n是数组长度,一半都是2的n次
tab[i = (n - 1) & hash]
//数学上有个公式,b是2的幂次时 a%b = a & (b-1),而且&是本地方法
问题关于Map问题
-
如果在put时,数组中已经有key,不想替换value/取值时如果calue是空想返回默认值怎么办
如果有key不想覆盖value,选择putIfAbsent,有一个onlyIfAbsent判断,一般put是false允许覆盖;
返回默认就是getOrDefault
-
自定义对象作为Map的key时需要注意
DTO是数据载体,如果是Hashmap,LinkedHashMap时要重写equals和hashcode方法
如果是Treemap,需要实现Comparable接口
Set源码
HashSet
类注释
- 底层基于hashmap,迭代时不能保证插入顺序
- add/remove,contains,size等方法的耗时性能不会随着数据量的增加而增加,时间复杂度是O(1)
- 线程不安全
- 快速失败
234为List,Set,Map共同点
初始化
private transient HashMap<E,Object> map;
// map的value都是PRESENT
private static final Object PRESENT = new Object();
public HashSet(Collection<? extends E> c) {
//容量计算,默认16,+1是确定期望值比扩容的阈值大1就不会扩容
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
TreeSet
底层是TreeMap复用的两种思路
复用1
Treeset的add方法
//直接拿来用
public boolean add(E e) {
return m.put(e, PRESENT)==null;
}
复用2
TreeSet定义想要的api,让TreeMap去实现
返回默认就是getOrDefault
-
自定义对象作为Map的key时需要注意
DTO是数据载体,如果是Hashmap,LinkedHashMap时要重写equals和hashcode方法
如果是Treemap,需要实现Comparable接口
Set源码
HashSet
类注释
- 底层基于hashmap,迭代时不能保证插入顺序
- add/remove,contains,size等方法的耗时性能不会随着数据量的增加而增加,时间复杂度是O(1)
- 线程不安全
- 快速失败
234为List,Set,Map共同点
初始化
private transient HashMap<E,Object> map;
// map的value都是PRESENT
private static final Object PRESENT = new Object();
public HashSet(Collection<? extends E> c) {
//容量计算,默认16,+1是确定期望值比扩容的阈值大1就不会扩容
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
TreeSet
底层是TreeMap复用的两种思路
复用1
Treeset的add方法
//直接拿来用
public boolean add(E e) {
return m.put(e, PRESENT)==null;
}
复用2
TreeSet定义想要的api,让TreeMap去实现