HashMap是键值对的集合。为什么要写它呢? 首先是因为HashMap日常使用比较多,并且面试中是大概率被问到的面试题。 所以我们对它的设计和源码来做一个分析。
准备的技术点
单链表、双链表、红黑树、二叉搜索树,hash
单链表
单链表是一种链式存取的数据结构,用一组地址任意的存储单元存放线性表中的数据元素。链表中的数据是以结点来表示的,每个结点的构成:元素(数据元素的映象) + 指针(指示后继元素存储位置),元素就是存储数据的存储单元,指针就是连接每个结点的地址数据。 单链表的实际使用场景并不多,比如只是频繁对头/尾结点进行操作,单链表最佳
双链表
双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。一般我们都构造双向循环链表。 双链表的主要优点是对于任意给的结点,都可以很轻易的获取其前驱结点或者后继结点,而主要缺点是每个结点需要添加额外的next域,因此需要更多的空间开销,同时结点的插入与删除操作也将更加耗时,因为需要更多的指针指向操作。
二叉搜索树
又称二叉查找树,亦称二叉搜索树,满足下面性质 若左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若右子树不空,则右子树上所有结点的值均大于它的根结点的值; 左、右子树也分别为二叉排序树; 没有键值相等的结点。
红黑树
红黑树是每个节点都带有颜色属性的二叉查找树,颜色或红色或黑色。 在二叉查找树强制一般要求以外,对于任何有效的红黑树我们增加了如下的额外要求:
性质1. 节点是红色或黑色。 性质2. 根节点是黑色。 性质3.所有叶子都是黑色。(叶子是NUIL节点) 性质4. 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点) 性质5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
这些约束强制了红黑树的关键性质: 从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。结果是这个树大致上是平衡的。因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,而不同于普通的二叉查找树。 性质4导致路径上不能有两个连续的红色节点。最短的可能路径都是黑色节点,最长的可能路径有交替的红色和黑色节点。因为根据性质5所有最长的路径都有相同数目的黑色节点,这就表明了没有路径能多于任何其他路径的两倍长
hash算法
希算法并不是一个特定的算法而是一类算法的统称。哈希算法也叫散列算法,一般来说满足这样的关系:f(data)=key,输入任意长度的data数据,经过哈希算法处理后输出一个定长的数据key。同时这个过程是不可逆的,无法由key逆推出data。 如果是一个data数据集,经过哈希算法处理后得到key的数据集,然后将keys与原始数据进行一一映射就得到了一个哈希表。一般来说哈希表M符合M[key]=data这种形式。 哈希表的好处是当原始数据较大时,我们可以用哈希算法处理得到定长的哈希值key,那么这个key相对原始数据要小得多。我们就可以用这个较小的数据集来做索引,达到快速查找的目的。 稍微想一下就可以发现,既然输入数据不定长,而输出的哈希值却是固定长度的,这意味着哈希值是一个有限集合,而输入数据则可以是无穷多个。那么建立一对一关系明显是不现实的。所以"碰撞"(不同的输入数据对应了相同的哈希值)是必然会发生的,所以一个成熟的哈希算法会有较好的抗冲突性。同时在实现哈希表的结构时也要考虑到哈希冲突的问题。
源码
本质上就是数组+链表
数组
transient Node<K,V>[] table;
单链表
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; } public final K getKey() { return key; } public final V getValue() { return value; } public final String toString() { return key + "=" + value; } public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } public final boolean equals(Object o) { if (o == this) return true; if (o instanceof Map.Entry) { Map.Entry<?,?> e = (Map.Entry<?,?>)o; if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue())) return true; } return false; } }
双链表-红黑树
static class LinkedHashMapEntry<K,V> extends HashMap.Node<K,V> { LinkedHashMapEntry<K,V> before, after; // 在hashMap中并没有使用这两个节点信息 LinkedHashMapEntry(int hash, K key, V value, Node<K,V> next) { super(hash, key, value, next); } }
static final class TreeNode<K,V> extends LinkedHashMap.LinkedHashMapEntry<K,V> { TreeNode<K,V> parent; TreeNode<K,V> left; TreeNode<K,V> right; TreeNode<K,V> prev; boolean red; ............ // 省略方法代码 }
扩容原理resize
final Node<K,V>[] resize() { Node<K,V>[] 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; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } 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<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { oldTab[j] = null; 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<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; }
容器大小为2的n次方,扩容因子为m;默认n = 4也就是默认大小16, m = 0.75。 存在3种情况下会扩容
容器内数组未初始化或者大小为0 此时已存储元素个数大于2^n * m 会扩容 容器个数小于64,某个位置的单链表长度大于等于7时,这时不直接转换为树结构,而是进行扩容
插入操作put
是否为第一次加入新元素,初始化容器(也是调用扩容方法) 如果加入key的hash值对应索引位置无数据,直接插入 如果key的hash值对应索引位置有数据,且节点为红黑树结构,则查看2节中关于红黑树插入的内容 如果key的hash值对应索引位置有数据,且节点为单链表结构,则进行for循环处理: a)链表存在数据,其key与当前物理地址相同或者key的equal比较相同,则重置数据返回, b) 若是已经到链表尾部,则新增节点,加入到尾部;如果加入后数据大小>= 7 则进行树化,查看2节中树化方法 尺寸+1,判断是否达到阈值,达到则进行扩容
获取元素
获取元素比较简单,以key的hash值找到索引位置,然后根据位置的节点特点来查找元素节点为空,则无此元素 节点为红黑树节点,查看2章节中getTreeNode方法分析 节点为单链表,从头到尾进行比对,如果比对成功,则退出,否则返回null;比对依据:hash值相同且物理地址相同或者equal方法相同
删除方法
如果未加入过元素,则不处理 如果删除key的hash对应索引位置元素为空,则不处理 如果索引节点的key和要删除的key比对相同,则删除节点即为索引节点 索引节点后驱为空,则不需要处理 索引节点后驱不为空,则在链表或者红黑树中查找此节点 红黑树结构使用removeTreeNode删除元素,单链表,删除元素后,其前驱的后继为其后继;并大小减1
清空
所有索引位置置空,也即断开单链表或者双链表-红黑树的根节点,进而删除所有元素
网友评论