突飞猛进九段斗之气--Map接口
修炼完这一篇秘籍,你就可以成为九段斗之气战士了,连升三段,成为初学武者里面中的佼佼者,距离斗者也只有一步之遥,冲鸭道友们。我们的目标是:拳打北山幼儿园,脚踢南海敬老院。
该系列文章上一篇: 六段斗之气–Collection的子接口:Set接口.
该系列文章下一篇: 突破!斗者–Collections工具类.
文章目录
- 初识Map接口
- Map是个啥玩意
- Map接口常用方法
- Map实现类之一:HashMap
- HashMap的存储结构
- HashMap的源码分析
- 类的声明
- 类的属性
- 构造方法
- 增加方法put(K key, V value)方法与扩容
- 插入元素到红黑树中的方法与树化
- 两种删除方法
- 更新与查询方法
- HashMap的小结
- Map实现类之二:LinkedHashMap
- Map实现类之三:TreeMap
- 跟HashTable说再见,挽留一下Properties
- 参考文献
- 更多
初识Map接口
Map是个啥玩意
- Map与Collection并列存在。用于保存具有映射关系的数据:key-value
- Map中的key和value都可以是任何引用类型的数据
- Map中的key用Set来存放,不允许重复,即同一个Map对象所对应的类,必须重写hashCode()方法和equals()方法。
- 常用String类作为Map的“键”,key和value之间存在单向一对一关系,即通过指定的key总能找到唯一的、确定的value
- Map接口的常用实现类:HashMap、TreeMap、LinkedHashMap和Properties。其中HashMap是Map接口使用频率最高的实现类。

结合上面的接口继承图,先大概说一下每个实现类的特点和它们之间的区别。
- 不同于Collection,Map用来存储双列数据,存储key-value对的数据,类似于高中的函数:y = f(x);
- HashMap:作为Map的主要实现类;线程不安全的,效率高;可以存储null的key和value。
- LinkedHashMap:保证在遍历map元素时,可以按照添加的顺序实现遍历。原因是因为在原有的HashMap底层结构基础上,添加了一对指针,指向前一个和后一个元素。对于频繁的遍历操作,此类执行效率高于HashMap。
- TreeMap保证按照添加的key-value对进行排序,实现排序遍历。此时考虑key的自然排序或定制排序
底层使用红黑树。 - Hashtable:作为古老的实现类;线程安全的,效率低;不能存储null的key和value。类似于ArrayList和Vector的关系。
- Properties:常用来处理配置文件。key和value都是String类型,比如配置数据库连接信息的时候经常使用的properties文件。
- 特别提一嘴,后面在源码中再细说,HashMap的底层JDK7及之前是用数组+链表实现的,在JDK8之后是由数组+链表+红黑树实现的,是不是很神奇,总之它变来变去,无非就是处于时间和空间的考虑,时间要短,空间要小,折衷处理,性能优化是最终目标。
Map接口常用方法
先看看Map接口有哪些行为规范,并对这些常用的方法进行代码测试

package com.learnjiawa.jihe;import org.junit.Test;import java.util.*;/*** Map中定义的方法:添加、删除、修改操作:Object put(Object key,Object value):将指定key-value添加到(或修改)当前map对象中void putAll(Map m):将m中的所有key-value对存放到当前map中Object remove(Object key):移除指定key的key-value对,并返回valuevoid clear():清空当前map中的所有数据元素查询的操作:Object get(Object key):获取指定key对应的valueboolean containsKey(Object key):是否包含指定的keyboolean containsValue(Object value):是否包含指定的valueint size():返回map中key-value对的个数boolean isEmpty():判断当前map是否为空boolean equals(Object obj):判断当前map和参数对象obj是否相等元视图操作的方法:Set keySet():返回所有key构成的Set集合Collection values():返回所有value构成的Collection集合Set entrySet():返回所有key-value对构成的Set集合*总结:常用方法:* 添加:put(Object key,Object value)* 删除:remove(Object key)* 修改:put(Object key,Object value)* 查询:get(Object key)* 长度:size()* 遍历:keySet() / values() / entrySet()*/
public class MapTest {/*添加、删除、修改操作:Object put(Object key,Object value):将指定key-value添加到(或修改)当前map对象中void putAll(Map m):将m中的所有key-value对存放到当前map中Object remove(Object key):移除指定key的key-value对,并返回valuevoid clear():清空当前map中的所有数据*/@Testpublic void test1(){Map map = new HashMap();
// map = new Hashtable();map.put(null,123);}@Testpublic void test2(){Map map = new HashMap();map = new LinkedHashMap();map.put(123,"AA");map.put(345,"BB");map.put(12,"CC");System.out.println(map);}@Testpublic void test3(){Map map = new HashMap();//添加map.put("AA",123);map.put(45,123);map.put("BB",56);//修改map.put("AA",87);System.out.println(map);Map map1 = new HashMap();map1.put("CC",123);map1.put("DD",123);map.putAll(map1);System.out.println(map);//remove(Object key)Object value = map.remove("CC");System.out.println(value);System.out.println(map);//clear()map.clear();//与map = null操作不同System.out.println(map.size());System.out.println(map);}/*元素查询的操作:Object get(Object key):获取指定key对应的valueboolean containsKey(Object key):是否包含指定的keyboolean containsValue(Object value):是否包含指定的valueint size():返回map中key-value对的个数boolean isEmpty():判断当前map是否为空boolean equals(Object obj):判断当前map和参数对象obj是否相等*/@Testpublic void test4(){Map map = new HashMap();map.put("AA",123);map.put(45,123);map.put("BB",56);// Object get(Object key)System.out.println(map.get(45));//containsKey(Object key)boolean isExist = map.containsKey("BB");System.out.println(isExist);isExist = map.containsValue(123);System.out.println(isExist);map.clear();System.out.println(map.isEmpty());}/*元视图操作的方法:Set keySet():返回所有key构成的Set集合Collection values():返回所有value构成的Collection集合Set entrySet():返回所有key-value对构成的Set集合*/@Testpublic void test5(){Map map = new HashMap();map.put("AA",123);map.put(45,1234);map.put("BB",56);//遍历所有的key集:keySet()Set set = map.keySet();Iterator iterator = set.iterator();while(iterator.hasNext()){System.out.println(iterator.next());}System.out.println();//遍历所有的value集:values()Collection values = map.values();for(Object obj : values){System.out.println(obj);}System.out.println();//遍历所有的key-value//方式一:entrySet()Set entrySet = map.entrySet();Iterator iterator1 = entrySet.iterator();while (iterator1.hasNext()){Object obj = iterator1.next();//entrySet集合中的元素都是entryMap.Entry entry = (Map.Entry) obj;System.out.println(entry.getKey() + "---->" + entry.getValue());}System.out.println();//方式二:Set keySet = map.keySet();Iterator iterator2 = keySet.iterator();while(iterator2.hasNext()){Object key = iterator2.next();Object value = map.get(key);System.out.println(key + "=====" + value);}}
}
Map实现类之一:HashMap
HashMap是Map接口使用频率最高的实现类,也是集合中的重中之重。Map允许使用null键和null值,与HashSet一样不保证映射的顺序。HashMap的所有键构成的集合是一个Set, 是无序的,不可重复的。所以key所在的类 要重写equals方法和hashCode方法。HashMap的所有value值构成的集合是一个Collection是无序的,可以重复的。所以value所在的类要重写equals方法。一个key,和一个value,在HashMap中构成一个实体entry,HashMap中所有entry构成的集合是Set。这些点从Map接口提供的元视图获得方法就可以看出来,这也是理解HashMap存储结构和底层源码的关键所在。
HashMap的存储结构
先结合下面两幅图,直观对比一下JDK7和JDK8之间的结构差别。


- JDK7中HashMap的内部存储结构其实是数组和链表的结合。当实例化一个HashMap时,系统会创建一个长度为Capacity的Entry数组,这个长度在哈希表中被称为容量(Capacity),在这个数组中可以存放元素的位置我们称之为“桶”(bucket),每个bucket都有自己的索引,系统可以根据索引快速的查找bucket中的元素。每个bucket中存储一个元素,即一个Entry对象,但每一个Entry对象可以带一个引用变量,用于指向下一个元素,因此,在一个桶中,就有可能生成一个Entry链。而且新添加的元素作为链表的head。
- JDK8中HashMap的内部存储结构其实是数组+链表+树的结合。当实例化一个HashMap时,会初始化initialCapacity和loadFactor,在put第一对映射关系时,系统会创建一个长度为initialCapacity的Node数组,这个长度在哈希表中被称为容量(Capacity),在这个数组中可以存放元素的位置我们称之为“桶”(bucket),每个bucket都有自己的索引,系统可以根据索引快速的查找bucket中的元素。每个bucket中存储一个元素,即一个Node对象,但每一个Node对象可以带一个引用变量next,用于指向下一个元素,因此,在一个桶中,就有可能生成一个Node链。也可能是一个一个TreeNode对象,每一个TreeNode对象可以有两个叶子结点left和right,因此,在一个桶中,就有可能生成一个TreeNode树。而新添加的元素作为链表的last,或树的叶子结点。
- 画重点,这两段要背,这是无上修炼口诀,背下来了再去源码里面验证这个过程,注意加粗部分,一个是创建数组的时机不同,另一个是添加链表元素时的位置不同。
HashMap的源码分析
类的声明
先来观察暗中观察一波HashMap类的声明。

- HashMap实现了Cloneable,可以被克隆。
- HashMap实现了Serializable,可以被序列化。
- HashMap继承自AbstractMap,实现了Map接口,具有Map的所有功能。
类的属性
在分析源码之前,需要对HashMap源码中的重要常量先了解一下,免得看得一头雾水。
| 常量名 | 作用 |
|---|---|
| DEFAULT_INITIAL_CAPACITY | HashMap的默认容量,16 |
| MAXIMUM_CAPACITY | HashMap的最大支持容量,2^30 |
| DEFAULT_LOAD_FACTOR | HashMap的默认加载因子 |
| TREEIFY_THRESHOLD | Bucket中链表长度大于该默认值,转化为红黑树 |
| UNTREEIFY_THRESHOLD | Bucket中红黑树存储的Node小于该默认值,转化为链表 |
| table | 存储元素的数组,总是2的n次幂 |
| entrySet | 存储具体元素的集 |
| size | HashMap中存储的键值对的数量 |
| modCount | HashMap扩容和结构改变的次数 |
| threshold | 扩容的临界值=容量*填充因子 |
| loadFactor | 填充因子 |
| MIN_TREEIFY_CAPACITY | 桶中的Node被树化时最小的hash表容量。(当桶中Node的 数量大到需要变红黑树时,若hash表容量小于MIN_TREEIFY_CAPACITY时,此时 应执行resize扩容操作这个MIN_TREEIFY_CAPACITY的值至少是TREEIFY_THRESHOLD的4倍。) |
构造方法
/*** Constructs an empty HashMap with the specified initial* capacity and load factor.* 判断传入的初始容量和装载因子是否合法,并计算扩容门槛,* 扩容门槛为传入的初始容量往上取最近的2的n次方。* @param initialCapacity the initial capacity* @param loadFactor the load factor* @throws IllegalArgumentException if the initial capacity is negative* or the load factor is nonpositive*/public HashMap(int initialCapacity, float loadFactor) {// 检查传入的初始容量是否合法if (initialCapacity < 0)throw new IllegalArgumentException("Illegal initial capacity: " +initialCapacity);if (initialCapacity > MAXIMUM_CAPACITY)initialCapacity = MAXIMUM_CAPACITY;// 检查装载因子是否合法if (loadFactor <= 0 || Float.isNaN(loadFactor))throw new IllegalArgumentException("Illegal load factor: " +loadFactor);this.loadFactor = loadFactor;// 计算扩容门槛this.threshold = tableSizeFor(initialCapacity);}static final int tableSizeFor(int cap) {// 扩容门槛为传入的初始容量往上取最近的2的n次方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;}/*** Constructs an empty HashMap with the specified initial* capacity and the default load factor (0.75).* 传入默认装载因子。* @param initialCapacity the initial capacity.* @throws IllegalArgumentException if the initial capacity is negative.*/public HashMap(int initialCapacity) {this(initialCapacity, DEFAULT_LOAD_FACTOR);}/*** Constructs an empty HashMap with the default initial capacity* 空参构造方法,全部使用默认值。* (16) and the default load factor (0.75).*/public HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted}/*** Constructs a new HashMap with the same mappings as the* specified Map. The HashMap is created with* default load factor (0.75) and an initial capacity sufficient to* hold the mappings in the specified Map.* 通过既有的Map创建一个新的Map* @param m the map whose mappings are to be placed in this map* @throws NullPointerException if the specified map is null*/public HashMap(Map<? extends K, ? extends V> m) {this.loadFactor = DEFAULT_LOAD_FACTOR;putMapEntries(m, false);}
增加方法put(K key, V value)方法与扩容
/*** HashMap中最主要的方法之一,总体流程:* (1)计算key的hash值;* (2)如果桶(数组)数量为0,则初始化桶;* (3)如果key所在的桶没有元素,则直接插入;* (4)如果key所在的桶中的第一个元素的key与待插入的key相同,说明找到了元素,转后续流程(9)处理;* (5)如果第一个元素是树节点,则调用树节点的putTreeVal()寻找元素或插入树节点;* (6)如果不是以上三种情况,则遍历桶对应的链表查找key是否存在于链表中;* (7)如果找到了对应key的元素,则转后续流程(9)处理;* (8)如果没找到对应key的元素,则在链表最后插入一个新节点并判断是否需要树化;* (9)如果找到了对应key的元素,则判断是否需要替换旧值,并直接返回旧值;* (10)如果插入了元素,则数量加1并判断是否需要扩容;*/
public V put(K key, V value) {// 调用hash(key)计算出key的hash值return putVal(hash(key), key, value, false, true);
}static final int hash(Object key) {int h;// 如果key为null,则hash值为0,否则调用key的hashCode()方法// 并让高16位与整个hash异或,这样做是为了使计算出的hash更分散return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K, V>[] tab;Node<K, V> p;int n, i;// 如果桶的数量为0,则初始化if ((tab = table) == null || (n = tab.length) == 0)// 调用resize()初始化n = (tab = resize()).length;// (n - 1) & hash 计算元素在哪个桶中// 如果这个桶中还没有元素,则把这个元素放在桶中的第一个位置if ((p = tab[i = (n - 1) & hash]) == null)// 新建一个节点放在桶中tab[i] = newNode(hash, key, value, null);else {// 如果桶中已经有元素存在了Node<K, V> e;K k;// 如果桶中第一个元素的key与待插入元素的key相同,保存到e中用于后续修改value值if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p;else if (p instanceof TreeNode)// 如果第一个元素是树节点,则调用树节点的putTreeVal插入元素e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);else {// 遍历这个桶对应的链表,binCount用于存储链表中元素的个数for (int binCount = 0; ; ++binCount) {// 如果链表遍历完了都没有找到相同key的元素,说明该key对应的元素不存在,则在链表最后插入一个新节点if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);// 如果插入新节点后链表长度大于8,则判断是否需要树化,因为第一个元素没有加到binCount中,所以这里-1if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);break;}// 如果待插入的key在链表中找到了,则退出循环if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}// 如果找到了对应key的元素if (e != null) { // existing mapping for key// 记录下旧值V oldValue = e.value;// 判断是否需要替换旧值if (!onlyIfAbsent || oldValue == null)// 替换旧值为新值e.value = value;// 在节点被访问后做点什么事,在LinkedHashMap中用到afterNodeAccess(e);// 返回旧值return oldValue;}}// 到这里了说明没有找到元素// 修改次数加1++modCount;// 元素数量加1,判断是否需要扩容if (++size > threshold)// 扩容resize();// 在节点插入后做点什么事,在LinkedHashMap中用到afterNodeInsertion(evict);// 没找到元素返回nullreturn null;
}
/**
* 扩容方法
*/
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)// 如果旧容量的两倍小于最大容量并且旧容量大于默认初始容量(16),则容量扩大为两部,扩容门槛也扩大为两倍newThr = oldThr << 1; // double threshold} else if (oldThr > 0) // initial capacity was placed in threshold// 使用非默认构造方法创建的map,第一次插入元素会走到这里// 如果旧容量为0且旧扩容门槛大于0,则把新容量赋值为旧门槛newCap = oldThr;else { // zero initial threshold signifies using defaults// 调用默认构造方法创建的map,第一次插入元素会走到这里// 如果旧容量旧扩容门槛都是0,说明还未初始化过,则初始化容量为默认容量,扩容门槛为默认容量*默认装载因子newCap = DEFAULT_INITIAL_CAPACITY;newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}if (newThr == 0) {// 如果新扩容门槛为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;// 如果桶中第一个元素不为空,赋值给eif ((e = oldTab[j]) != null) {// 清空旧桶,便于GC回收 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// 如果这个链表不止一个元素且不是一颗树// 则分化成两个链表插入到新的桶中去// 比如,假如原来容量为4,3、7、11、15这四个元素都在三号桶中// 现在扩容到8,则3和11还是在三号桶,7和15要搬移到七号桶中去// 也就是分化成了两个链表Node<K, V> loHead = null, loTail = null;Node<K, V> hiHead = null, hiTail = null;Node<K, V> next;do {next = e.next;// (e.hash & oldCap) == 0的元素放在低位链表中// 比如,3 & 4 == 0if ((e.hash & oldCap) == 0) {if (loTail == null)loHead = e;elseloTail.next = e;loTail = e;} else {// (e.hash & oldCap) != 0的元素放在高位链表中// 比如,7 & 4 != 0if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);// 遍历完成分化成两个链表了// 低位链表在新桶中的位置与旧桶一样(即3和11还在三号桶中)if (loTail != null) {loTail.next = null;newTab[j] = loHead;}// 高位链表在新桶中的位置正好是原来的位置加上旧容量(即7和15搬移到七号桶了)if (hiTail != null) {hiTail.next = null;newTab[j + oldCap] = hiHead;}}}}}return newTab;
}
插入元素到红黑树中的方法与树化
/**
*(1)寻找根节点;
*(2)从根节点开始查找;
*(3)比较hash值及key值,如果都相同,直接返回,在putVal()方法中决定是否要替换value值;
*(4)根据hash值及key值确定在树的左子树还是右子树查找,找到了直接返回;
*(5)如果最后没有找到则在树的相应位置插入元素,并做平衡;
*/
final TreeNode<K, V> putTreeVal(HashMap<K, V> map, Node<K, V>[] tab,int h, K k, V v) {Class<?> kc = null;// 标记是否找到这个key的节点boolean searched = false;// 找到树的根节点TreeNode<K, V> root = (parent != null) ? root() : this;// 从树的根节点开始遍历for (TreeNode<K, V> p = root; ; ) {// dir=direction,标记是在左边还是右边// ph=p.hash,当前节点的hash值int dir, ph;// pk=p.key,当前节点的key值K pk;if ((ph = p.hash) > h) {// 当前hash比目标hash大,说明在左边dir = -1;}else if (ph < h)// 当前hash比目标hash小,说明在右边dir = 1;else if ((pk = p.key) == k || (k != null && k.equals(pk)))// 两者hash相同且key相等,说明找到了节点,直接返回该节点// 回到putVal()中判断是否需要修改其value值return p;else if ((kc == null &&// 如果k是Comparable的子类则返回其真实的类,否则返回null(kc = comparableClassFor(k)) == null) ||// 如果k和pk不是同样的类型则返回0,否则返回两者比较的结果(dir = compareComparables(kc, k, pk)) == 0) {// 这个条件表示两者hash相同但是其中一个不是Comparable类型或者两者类型不同// 比如key是Object类型,这时可以传String也可以传Integer,两者hash值可能相同// 在红黑树中把同样hash值的元素存储在同一颗子树,这里相当于找到了这颗子树的顶点// 从这个顶点分别遍历其左右子树去寻找有没有跟待插入的key相同的元素if (!searched) {TreeNode<K, V> q, ch;searched = true;// 遍历左右子树找到了直接返回if (((ch = p.left) != null &&(q = ch.find(h, k, kc)) != null) ||((ch = p.right) != null &&(q = ch.find(h, k, kc)) != null))return q;}// 如果两者类型相同,再根据它们的内存地址计算hash值进行比较dir = tieBreakOrder(k, pk);}TreeNode<K, V> xp = p;if ((p = (dir <= 0) ? p.left : p.right) == null) {// 如果最后确实没找到对应key的元素,则新建一个节点Node<K, V> xpn = xp.next;TreeNode<K, V> x = map.newTreeNode(h, k, v, xpn);if (dir <= 0)xp.left = x;elsexp.right = x;xp.next = x;x.parent = x.prev = xp;if (xpn != null)((TreeNode<K, V>) xpn).prev = x;// 插入树节点后平衡// 把root节点移动到链表的第一个节点moveRootToFront(tab, balanceInsertion(root, x));return null;}}
}/**
树化方法:
*(1)从链表的第一个元素开始遍历;
*(2)将第一个元素作为根节点;
*(3)其它元素依次插入到红黑树中,再做平衡;
*(4)将根节点移到链表第一元素的位置(因为平衡的时候根节点会改变);
*/
final void treeify(Node<K, V>[] tab) {TreeNode<K, V> root = null;for (TreeNode<K, V> x = this, next; x != null; x = next) {next = (TreeNode<K, V>) x.next;x.left = x.right = null;// 第一个元素作为根节点且为黑节点,其它元素依次插入到树中再做平衡if (root == null) {x.parent = null;x.red = false;root = x;} else {K k = x.key;int h = x.hash;Class<?> kc = null;// 从根节点查找元素插入的位置for (TreeNode<K, V> p = root; ; ) {int dir, ph;K pk = p.key;if ((ph = p.hash) > h)dir = -1;else if (ph < h)dir = 1;else if ((kc == null &&(kc = comparableClassFor(k)) == null) ||(dir = compareComparables(kc, k, pk)) == 0)dir = tieBreakOrder(k, pk);// 如果最后没找到元素,则插入TreeNode<K, V> xp = p;if ((p = (dir <= 0) ? p.left : p.right) == null) {x.parent = xp;if (dir <= 0)xp.left = x;elsexp.right = x;// 插入后平衡,默认插入的是红节点,在balanceInsertion()方法里root = balanceInsertion(root, x);break;}}}}// 把根节点移动到链表的头节点,因为经过平衡之后原来的第一个元素不一定是根节点了moveRootToFront(tab, root);
}
两种删除方法
/**
*(1)先查找元素所在的节点;
*(2)如果找到的节点是树节点,则按树的移除节点处理;
*(3)如果找到的节点是桶中的第一个节点,则把第二个节点移到第一的位置;
*(4)否则按链表删除节点处理;
*(5)修改size,调用移除节点后置处理等;
*/
public V remove(Object key) {Node<K, V> e;return (e = removeNode(hash(key), key, null, false, true)) == null ?null : e.value;
}final Node<K, V> removeNode(int hash, Object key, Object value,boolean matchValue, boolean movable) {Node<K, V>[] tab;Node<K, V> p;int n, index;// 如果桶的数量大于0且待删除的元素所在的桶的第一个元素不为空if ((tab = table) != null && (n = tab.length) > 0 &&(p = tab[index = (n - 1) & hash]) != null) {Node<K, V> node = null, e;K k;V v;if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))// 如果第一个元素正好就是要找的元素,赋值给node变量后续删除使用node = p;else if ((e = p.next) != null) {if (p instanceof TreeNode)// 如果第一个元素是树节点,则以树的方式查找节点node = ((TreeNode<K, V>) p).getTreeNode(hash, key);else {// 否则遍历整个链表查找元素do {if (e.hash == hash &&((k = e.key) == key ||(key != null && key.equals(k)))) {node = e;break;}p = e;} while ((e = e.next) != null);}}
/**
如果找到了元素,则看参数是否需要匹配value值,如果不需要匹配直接删除,如果需要匹配则看value值是否与传入的value相等
*/if (node != null && (!matchValue || (v = node.value) == value ||(value != null && value.equals(v)))) {if (node instanceof TreeNode)// 如果是树节点,调用树的删除方法(以node调用的,是删除自己)((TreeNode<K, V>) node).removeTreeNode(this, tab, movable);else if (node == p)// 如果待删除的元素是第一个元素,则把第二个元素移到第一的位置tab[index] = node.next;else// 否则删除node节点p.next = node.next;++modCount;--size;// 删除节点后置处理afterNodeRemoval(node);return node;}}return null;
}/**
*(1)TreeNode本身既是链表节点也是红黑树节点;
*(2)先删除链表节点;
*(3)再删除红黑树节点并做平衡;
*/
final void removeTreeNode(HashMap<K, V> map, Node<K, V>[] tab,boolean movable) {int n;// 如果桶的数量为0直接返回if (tab == null || (n = tab.length) == 0)return;// 节点在桶中的索引int index = (n - 1) & hash;// 第一个节点,根节点,根左子节点TreeNode<K, V> first = (TreeNode<K, V>) tab[index], root = first, rl;// 后继节点,前置节点TreeNode<K, V> succ = (TreeNode<K, V>) next, pred = prev;if (pred == null)// 如果前置节点为空,说明当前节点是根节点,则把后继节点赋值到第一个节点的位置,相当于删除了当前节点tab[index] = first = succ;else// 否则把前置节点的下个节点设置为当前节点的后继节点,相当于删除了当前节点pred.next = succ;// 如果后继节点不为空,则让后继节点的前置节点指向当前节点的前置节点,相当于删除了当前节点if (succ != null)succ.prev = pred;// 如果第一个节点为空,说明没有后继节点了,直接返回if (first == null)return;// 如果根节点的父节点不为空,则重新查找父节点if (root.parent != null)root = root.root();// 如果根节点为空,则需要反树化(将树转化为链表)// 如果需要移动节点且树的高度比较小,则需要反树化if (root == null|| (movable&& (root.right == null|| (rl = root.left) == null|| rl.left == null))) {tab[index] = first.untreeify(map); // too smallreturn;}// 分割线,以上都是删除链表中的节点,下面才是直接删除红黑树的节点(因为TreeNode本身即是链表节点又是树节点)// 删除红黑树节点的大致过程是寻找右子树中最小的节点放到删除节点的位置,然后做平衡,此处不过多注释TreeNode<K, V> p = this, pl = left, pr = right, replacement;if (pl != null && pr != null) {TreeNode<K, V> s = pr, sl;while ((sl = s.left) != null) // find successors = sl;boolean c = s.red;s.red = p.red;p.red = c; // swap colorsTreeNode<K, V> sr = s.right;TreeNode<K, V> pp = p.parent;if (s == pr) { // p was s's direct parentp.parent = s;s.right = p;} else {TreeNode<K, V> sp = s.parent;if ((p.parent = sp) != null) {if (s == sp.left)sp.left = p;elsesp.right = p;}if ((s.right = pr) != null)pr.parent = s;}p.left = null;if ((p.right = sr) != null)sr.parent = p;if ((s.left = pl) != null)pl.parent = s;if ((s.parent = pp) == null)root = s;else if (p == pp.left)pp.left = s;elsepp.right = s;if (sr != null)replacement = sr;elsereplacement = p;} else if (pl != null)replacement = pl;else if (pr != null)replacement = pr;elsereplacement = p;if (replacement != p) {TreeNode<K, V> pp = replacement.parent = p.parent;if (pp == null)root = replacement;else if (p == pp.left)pp.left = replacement;elsepp.right = replacement;p.left = p.right = p.parent = null;}TreeNode<K, V> r = p.red ? root : balanceDeletion(root, replacement);if (replacement == p) { // detachTreeNode<K, V> pp = p.parent;p.parent = null;if (pp != null) {if (p == pp.left)pp.left = null;else if (p == pp.right)pp.right = null;}}if (movable)moveRootToFront(tab, r);
}
更新与查询方法
@Override//更新方法public V replace(K key, V value) {Node<K,V> e;if ((e = getNode(hash(key), key)) != null) {V oldValue = e.value;e.value = value;afterNodeAccess(e);return oldValue;}return null;}/***(1)计算key的hash值;*(2)找到key所在的桶及其第一个元素;*(3)如果第一个元素的key等于待查找的key,直接返回;*(4)如果第一个元素是树节点就按树的方式来查找,否则按链表方式查找;*/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;// 如果桶的数量大于0并且待查找的key所在的桶的第一个元素不为空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;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;}/***经典二叉查找树的查找过程,先根据hash值比较,再根据key值比较决定是查左子树还是右子树。*/final TreeNode<K, V> getTreeNode(int h, Object k) {// 从树的根节点开始查找return ((parent != null) ? root() : this).find(h, k, null);}final TreeNode<K, V> find(int h, Object k, Class<?> kc) {TreeNode<K, V> p = this;do {int ph, dir;K pk;TreeNode<K, V> pl = p.left, pr = p.right, q;if ((ph = p.hash) > h)// 左子树p = pl;else if (ph < h)// 右子树p = pr;else if ((pk = p.key) == k || (k != null && k.equals(pk)))// 找到了直接返回return p;else if (pl == null)// hash相同但key不同,左子树为空查右子树p = pr;else if (pr == null)// 右子树为空查左子树p = pl;else if ((kc != null ||(kc = comparableClassFor(k)) != null) &&(dir = compareComparables(kc, k, pk)) != 0)// 通过compare方法比较key值的大小决定使用左子树还是右子树p = (dir < 0) ? pl : pr;else if ((q = pr.find(h, k, kc)) != null)// 如果以上条件都不通过,则尝试在右子树查找return q;else// 都没找到就在左子树查找p = pl;} while (p != null);return null;}
HashMap源码中最重要的无非是添加元素、获取元素、以及扩容的过程,JDK8不同于JDK7的是多了一个树形化的过程。
- JDK7中向HashMap中添加entry1(key,value),需要首先计算entry1中key的哈希值(根据key所在类的hashCode()计算得到),此哈希值经过处理以后,得到在底层Entry[]数组中要存储的位置i。如果位置i上没有元素,则entry1直接添加成功。如果位置i上已经存在entry2(或还有链表存在的entry3,entry4),则需要通过循环的方法,依次比较entry1中key和其他的entry。如果彼此hash值不同,则直接添加成功。如果hash值相同,继续比较二者是否equals。如果返回值为true,则使用entry1的value去替换equals为true的entry的value。如果遍历一遍以后,发现所有的equals返回都为false,则entry1仍可添加成功。entry1指向原有的entry元素。
- 当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的长度是固定的。所以为了提高查询的效率,就要对HashMap的数组进行扩容,而在HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。那么HashMap什么时候进行扩容呢? 当HashMap中的元素个数超过数组大小(数组总大小length,不是数组中个数size)loadFactor 时 , 就 会 进 行 数 组 扩 容 , loadFactor 的默认值(DEFAULT_LOAD_FACTOR)为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小(DEFAULT_INITIAL_CAPACITY)为16,那么当HashMap中元素个数超过160.75=12(这个值就是代码中的threshold值,也叫做临界值)的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。
- JDK8中,当HashMap中的其中一个链的对象个数如果达到了8个,此时如果capacity没有达到64,那么HashMap会先扩容解决,如果已经达到了64,那么这个链会变成树,结点类型由Node变成TreeNode类型。当然,如果当映射关系被移除后,下次resize方法时判断树的结点个数低于6个,也会把树再转为链表。
HashMap的小结
经过上面的修炼,你能闭着眼睛说一下HashMap中put/get方法的大体过程吗?你能聊一聊HashMap的扩容机制吗?HashMap的默认初始容量是多少?负载因子是什么意思?临界值是什么意思?总结一下JDK1.8相较于之前的变化:
- HashMap map = new HashMap();//默认情况下,先不创建长度为16的数组。
- 当首次调用map.put()时,再创建长度为16的数组。
- 数组为Node类型,在jdk7中称为Entry类型。
- 形成链表结构时,新添加的key-value对在链表的尾部(七上八下)。
- 当数组指定索引位置的链表长度>8时,且map中的数组的长度> 64时,同时满足两项条件时此索引位置上的所有key-value对使用红黑树进行存储。

Map实现类之二:LinkedHashMap
LinkedHashMap是HashMap的子类,在HashMap存储结构的基础上,使用了一对双向链表来记录添加元素的顺序,与LinkedHashSet类似,LinkedHashMap可以维护Map的迭代顺序:迭代顺序与Key-Value对的插入顺序一致。比较一下HashMap中的内部类和LinkedHashMap中的内部类,可以很清楚的看到这一点。
//HashMap中的内部类:Node
static class Node<K,V> implements Map.Entry<K,V>{final int hash;final K key;V value;Node<K,V> next;
}
//LinkedHashMap中的内部类:Entry
static class Entry<K,V> extends HashMap.Node<K,V>{Entry<K,V> before, after;Entry(int hash, K key, V value, Node<K,V> next){super(hash, key, value, next);}
}
Map实现类之三:TreeMap
- TreeMap存储Key-Value对时,需要根据key-value对进行排序。TreeMap可以保证所有的Key-Value对处于有序状态。
- TreeSet底层使用红黑树结构存储数据
- TreeMap的Key的排序:
自然排序----TreeMap的所有的Key必须实现Comparable接口,而且所有的Key应该是同一个类的对象。否则将会抛出ClassCastException。
定制排序----创建TreeMap时,传入一个Comparator对象,该对象负责对TreeMap中的所有的key进行排序。此时不需要Map的Key实现Comparable接口。
TreeMap判断两个key相等的标准:两个key通过compareTo()或者compare()返回0。
跟HashTable说再见,挽留一下Properties
- HashTable是个古老的Map实现类,JDK1.0就提供了。不同于HashMap,HashTable时线程安全的。HashTable实现原理和HashMap相同,功能相同。底层都使用哈希表结构,查询速度快,同样不能保证其中Key-Value对的顺序,判断两个key、两个value相等的标准也一致很多情况下可以互用。与HashMap不同,HashTable不允许使用null作为key和value,
- Properties是Hashtable的子类,该对象用于处理属性文件。由于属性文件里面的key/value都是字符串类型,所以Properties里面的key/value都是字符串类型。存取数据时,建议使用setProperty(String key, String value)方法和getProperty(String key)方法。
Properties pros = new Properties();
pros.load(new FileInputStream("jdbc.properties"));
String user = pros.getProperty("user");
System.out.println(user);
去看该系列文章下一篇: 突破!斗者–Collections工具类.
参考文献
[1]Bruce Eckel.Java编程思想(第4版)[M].机械工业出版社,2008:459-524.
更多
对我的文章感兴趣,持续更新中…
- Github.
- 个人博客
- 网站开发
- 简书
- Email: zouhuayu96@qq.com
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
