最常用的哈希表, 内部通过数组 + 单链表的方式实现。
关于哈希表可以查看数据结构基础——数组、散列表
public class HashMap<K, V> extends AbstractMap<K, V> implements Map<K, V>, Cloneable, Serializable {
private static final long serialVersionUID = 362498820763181265L;
static final int DEFAULT_INITIAL_CAPACITY = 16;
// 最大容量
static final int MAXIMUM_CAPACITY = 1073741824;
// 填充比
static final float DEFAULT_LOAD_FACTOR = 0.75F;
// 元素链表中的数据超过此值后,链表转换为树结构
static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;
// 保存元素的数组
transient HashMap.Node<K, V>[] table;
transient Set<Entry<K, V>> entrySet;
// 元素个数
transient int size;
// 操作数
transient int modCount;
// 临界值 当实际大小(容量*填充比)超过临界值时,会进行扩容
int threshold;
final float loadFactor;
}
可以看到和List相比Map中存在的元素多了很多,除了Node来保存数据之外,还存在一个DEFAULT_LOAD_FACTOR
的值,其是map的填充比。填充比如果填充比很大,说明利用的空间很多,可能出现很多哈希冲突,如果一直不进行扩容的话,链表就会越来越长,这样查找的效率很低。但是填充比太小又会导致空间浪费。如果关注内存,填充比可以稍大,如果主要关注查找性能,填充比可以稍小。
内部类node
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
return new Node<>(hash, key, value, next);
}
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
// ......
}
node类很类似LinkedList中的内部节点,实际上也是发挥类似的作用。因为存在哈希冲突的原因,map中每个元素的值保存的不是当前元素内容,而是类似链表的结构,链表中保存着key、value和下一节点的信息等内容。
public V put(K key, V value) {
// 使用hash(key) 求key的hash值
return this.putVal(hash(key), key, value, false, true);
}
/**
*
* @param onlyIfAbsent onlyifabsent如果为true,则不更改现有值
* @param evict 如果为false,则表处于创建模式
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 如果map为空旧进行容器扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
// 如果当前hash值对应的位置没有内容,则新建一个Node
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 如果此hash值对应的位置有元素,且元素的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 {
// 进入循环
for (int binCount = 0; ; ++binCount) {
// 循环到当这个节点的下一个节点为空的时候则插入的元素新建一个节点链接到之前节点后方
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 此时判断链表长度大于8时,将链表转红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 或者循环到如果此hash值对应的位置有元素,且元素的key和插入的key相同则进行更新
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
// 进入下一循环钱更新p的对象
p = e;
}
}
// 此时取出的是要插入位置的元素
if (e != null) { // existing mapping for key
V oldValue = e.value;
// 判断是否允许覆盖,并且value是否为空
if (!onlyIfAbsent || oldValue == null)
e.value = value;
// 回调以允许LinkedHashMap后置操作
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 大于临界值,重新调整数据长度
if (++size > threshold)
resize();
// 后置回调
afterNodeInsertion(evict);
return null;
}
HashMap的新增和修改中主要考虑两个问题,一个是容器空间扩容一个是哈希冲突。因为哈希冲突的存在,每个table数组的元素可能存在0到N个数据,之前版本Java使用链表保存,在JAVA8的时候除了使用链表还使用了树结构进行优化。
整个新增数据的流程:
final Node<K,V>[] resize() {
// 旧的元素信息
Node<K,V>[] oldTab = table;
// 旧的长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 旧的临界值
int oldThr = threshold;
// 声明新的长度和临界值
int newCap, newThr = 0;
// 一个非空的table
if (oldCap > 0) {
// 如果数组长度达到最大值,则修改临界值为Integer.MAX_VALUE
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 否则,新的数据table长度为旧的长度的2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 同时最大临界值也修改为之前的两倍
newThr = oldThr << 1; // double threshold
}
// 如果旧的临界值大于0,则设置新table的长度为旧的临界值
else if (oldThr > 0)
newCap = oldThr;
else {
// 初始阶段则设置数据table的长度为默认长度 16
newCap = DEFAULT_INITIAL_CAPACITY;
// 新的临界值为 0.75f * 1 << 4 (1 << 4 为 16)
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 如果临界值还为0,则设置临界值
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
// 更新threshold
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
// 此时数据部位空,因为进行扩容了所以需要修改元素tab位置
if (oldTab != null) {
// 遍历旧的表
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
// 说明这个node没有链表直接放在新表的e.hash & (newCap - 1)位置
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
// 此时表示这个node后面为链表,需要重新计算元素的位置
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
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;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
容器扩容一般来说是一个常被问到的问题。在resize
方法中的容器初始化和扩容可以知道下面几个信息:
DEFAULT_INITIAL_CAPACITY
的值,其长度为16。临界值为0.75×16为12假如我自定义容器长度呢?
实际上HashMap有一个构造方法提供了自定义构造容器长度的方法,但这个方法并不会完全按照你的内容去初始化长度,其计算长度的方法为
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
此方法是为了求里你设置的参数下一个2的幂。他会计算得到2\4\8\16…这些值
public V remove(Object key) {
Iterator<Entry<K,V>> i = entrySet().iterator();
Entry<K,V> correctEntry = null;
if (key==null) {
while (correctEntry==null && i.hasNext()) {
Entry<K,V> e = i.next();
if (e.getKey()==null)
correctEntry = e;
}
} else {
while (correctEntry==null && i.hasNext()) {
Entry<K,V> e = i.next();
if (key.equals(e.getKey()))
correctEntry = e;
}
}
V oldValue = null;
if (correctEntry !=null) {
oldValue = correctEntry.getValue();
i.remove();
}
return oldValue;
}
和新增相比,Map的移除会稍微简单些。
在HashMap中移除数据的逻辑是:拿到map的遍历器,然后开始遍历数据。当获取到对应的key的值得时候,遍历结束,此时移除遍历器中的值,然后将被移除的值返回出去。
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 找到hash值对应的位置
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 检查第一个元素是否是需要的内容
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 假如第一个不是的话就需要去判断后续节点。
// 但是后续节点有两种tree和链表,需要单独判断。
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
相比其他操作Map的查找就显得非常简单了。根据hash函数获得对应table中的节点,如果节点存在值但是key不相同则循环直到找到key相同或者循环完毕返回null。
HashMap可以说是开发中常用的数据集合了。对于其面试的内容也是相当的多。主要问题在于实现原因(哈希冲突)、其初始长度、自定义初始长度、每次扩容系数、以及其填充比系数。
继承自HashMap, 底层额外维护了一个双向链表来维持数据有序
public class LinkedHashMap<K, V> extends HashMap<K, V> implements Map<K, V> {
private static final long serialVersionUID = 3801124242820219131L;
transient LinkedHashMap.Entry<K, V> head;
transient LinkedHashMap.Entry<K, V> tail;
final boolean accessOrder;
}
关于LinkedHashMap可以看到其主要继承了HashMap,其主要是在HashMap的基础上进行自己的业务逻辑。通过维护了一个节点头和节点尾来维护一个数据链表。 虽然LinkedHashMap是在HashMap基础上实现了他自己的策略,但是实际中查看代码可以发现其并没有重写添加和删除的方法。
HashMap为LinkedHashMap提供的方法
我们再去看下HashMap的方法会发现三个空实现的内容
// Callbacks to allow LinkedHashMap post-actions
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }
这三个方法是为了方便LinkedHashMap通过重写其内容来实现它自己的逻辑的方法。
在更新数据的时候,使用了afterNodeAccess和afterNodeInsertion的方法。
afterNodeAccess
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMap.Entry<K,V> last;
if (accessOrder && (last = tail) != e) {
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a != null)
a.before = b;
else
last = b;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}
可以看到此时会将指定节点转移至最后,其实就是每次操作的节点被移动到最后面。
fterNodeInsertion
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
在此方法中则是为了移除最老的节点,因为节点被访问后会被移动到链表的尾部,此时头部的节点一定是很久都没有被访问的元素。
删除数据的时候LinkedHashMap并没有重写方法,一切都是基于HashMap的。而在HashMap中调用了afterNodeRemoval(node);
方法,此方法在LinkedHashMap中实现。
void afterNodeRemoval(Node<K,V> e) { // unlink
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
p.before = p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a == null)
tail = b;
else
a.before = b;
}
其主要作用是移除在LinkedHashMap中的此元素。
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
其查询主要是使用了HashMap的方法,不同之处是,添加了更新元素访问后被移动到链表尾部的逻辑。
LinkedHashMap和HashMap相比,因为是继承关系,两者在对一些元素操作、扩容、初始化的逻辑上几乎没有区别。LinkedHashMap主要是通过HashMap中预留的三个方法来维护其内部链表关系,从而保证在数据顺序上是有序的。
TreeMap的实现是红黑树算法的实现。与HashMap相比,TreeMap可以进行元素大小的比较,根据传入的key进行大小比较,可以使用集合中的自定义比较器进行排序。
public class TreeMap<K, V> extends AbstractMap<K, V> implements NavigableMap<K, V>, Cloneable, Serializable {
private final Comparator<? super K> comparator;
private transient TreeMap.Entry<K, V> root;
private transient int size = 0;
private transient int modCount = 0;
private transient TreeMap<K, V>.EntrySet entrySet;
private transient TreeMap.KeySet<K> navigableKeySet;
private transient NavigableMap<K, V> descendingMap;
private static final Object UNBOUNDED = new Object();
private static final boolean RED = false;
private static final boolean BLACK = true;
private static final long serialVersionUID = 919286545866124006L;
}
public V put(K key, V value) {
Entry<K,V> t = root;
// 此时不存在根节点,第一次操作,则设置当前元素为根节点
if (t == 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<? 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);
//在t存在值得时候,循环会进行下去。
} while (t != null);
}
else {
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
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);
// 如果新增节点的key小于parent的key,则当做左子节点
// 注意,因为之前循环的原因,在每次循环的时候parent都会指向新的节点,需要注意
if (cmp < 0)
parent.left = e;
else
// 如果新增节点的key大于parent的key,则当做右子节点
parent.right = e;
// 上面已经完成了排序二叉树的的构建,将新增节点插入该树中的合适位置
// 下面fixAfterInsertion()方法就是对这棵树进行调整、平衡
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
TreeMap的更新就是向其树结构插入数据的过程。
java使用fixAfterInsertion
方法进行红黑树的重排。这里就不细说了,后续会专门来讲一讲树的调整等问题。这里只是简单分析下其代码。
private void fixAfterInsertion(Entry<K,V> x) {
// 设置传入节点为红色
x.color = RED;
// 要求X不能为空,X不能为根节点并且X父节点不能为红色
while (x != null && x != root && x.parent.color == RED) {
// 如果X的父节点是其父节点的父节点的左节点
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
// 获得父节点另一侧的节点
Entry<K,V> y = rightOf(parentOf(parentOf(x)));
// 为红色
if (colorOf(y) == RED) {
// 将父节点设置为黑色
setColor(parentOf(x), BLACK);
// 将父节点另一侧的节点设置为黑色
setColor(y, BLACK);
// 父节点的父节点设置为红色
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
// 为黑色
} else {
// 如果X节点为其父节点的右子树,则进行左旋转
if (x == rightOf(parentOf(x))) {
x = parentOf(x);
rotateLeft(x);
}
// 设置父节点为黑色
setColor(parentOf(x), BLACK);
// 设置爷爷节点为红色
setColor(parentOf(parentOf(x)), RED);
// 以X的父节点的父节点为中心右旋转
rotateRight(parentOf(parentOf(x)));
}
// 如果X的父节点是其父节点的父节点的右节点
} else {
// 获得父节点另一侧的节点
Entry<K,V> y = leftOf(parentOf(parentOf(x)));
// 其为红色
if (colorOf(y) == RED) {
// 父节点设置为黑色
setColor(parentOf(x), BLACK);
// 父节点另一侧的节点设置为黑色
setColor(y, BLACK);
// 设置爷爷节点为红色
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
} else {
// 其为黑色
if (x == leftOf(parentOf(x))) {
x = parentOf(x);
rotateRight(x);
}
// 父节点设置为黑色
setColor(parentOf(x), BLACK);
// 设置爷爷节点为红色
setColor(parentOf(parentOf(x)), RED);
// 以爷爷节点为中心右旋转
rotateLeft(parentOf(parentOf(x)));
}
}
}
// 设置根节点为黑色
root.color = BLACK;
}
public V remove(Object key) {
Entry<K,V> p = getEntry(key);
if (p == null)
return null;
V oldValue = p.value;
deleteEntry(p);
return oldValue;
}
private void deleteEntry(Entry<K,V> p) {
modCount++;
size--;
// 当被删除的节点左右节点都不为空的时候,
// 就使用其子节点来进行替代,使用哪个节点是successor判断的
// 主要策略是右分支最左边,或者 左分支最右边的节点
if (p.left != null && p.right != null) {
Entry<K,V> s = successor(p);
p.key = s.key;
p.value = s.value;
p = s;
} // p has 2 children
// replacement为替代节点,如果P的左子树存在那么就用左子树替代,否则用右子树替代
Entry<K,V> replacement = (p.left != null ? p.left : p.right);
// 替代节点不为空
if (replacement != null) {
// Link replacement to parent
replacement.parent = p.parent;
// 假如被删除节点无父节点,则是root节点,
// 那么替代节点为根节点
if (p.parent == null)
root = replacement;
// 如果P为左节点,则用replacement来替代为左节点
else if (p == p.parent.left)
p.parent.left = replacement;
else
// 如果P为右节点,则用replacement来替代为右节点
p.parent.right = replacement;
// Null out links so they are OK to use by fixAfterDeletion.
p.left = p.right = p.parent = null;
// 若P为红色直接删除,红黑树保持平衡
// 但是若P为黑色,则需要调整红黑树使其保持平衡
if (p.color == BLACK)
fixAfterDeletion(replacement);
// 父节点为空,则证明此节点为根节点,且没有子节点
} else if (p.parent == null) { // return if we are the only node.
root = null;
// 父节点不为空,但是子节点为空
} else {
if (p.color == BLACK)
// 如果P节点的颜色为黑色,对红黑树进行调整
fixAfterDeletion(p);
// 删除p节点,调整期父节点的链接
if (p.parent != null) {
if (p == p.parent.left)
p.parent.left = null;
else if (p == p.parent.right)
p.parent.right = null;
p.parent = null;
}
}
}
和之前两种Map删除不同,TreeMap是因为是基于红黑树的关系,在删除元素的时候会涉及到重构树结构的过程。整个过程存在几种可能。
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;
}
TreeMap的查询就是根据其key,然后再tree中进行排序知道找到key相同或者没有找到key相同的数据(返回null)。
TreeMap 实现了 SortMap接口,其能够根据键排序。因为数据插入和删除的时候涉及到重构树结构所以在插入和删除操作上会有些性能损耗,并且TreeMap 的键和值都不能为空。这样可以看出来在平时我们多数会选择HashMap,除非我们需要对数据进行排序这个时候使用TreeMap可以保证我们在遍历数据的时候数据已经被排序。
个人水平有限,上面的内容可能存在没有描述清楚或者错误的地方,假如开发同学发现了,请及时告知,我会第一时间修改相关内容。假如我的这篇内容对你有任何帮助的话,麻烦给我点一个赞。你的点赞就是我前进的动力。