1、Map接口
1.1、Map接口概述
Map与Collection并列存在,用于保存具有映射关系的数据:key-value
- Map 中的 key 和 value 都可以是任何引用类型的数据
- Map 中的 key 用
Set来存放,不允许重复,即Key所对应的类,须重写hashCode()和equals()方法 - 常用String类型的对象作为Map的“key” ,即通过指定的 key 总能找到唯一的、确定的value
- Map接口的常用实现类:HashMap、TreeMap、LinkedHashMap和 Properties。其中,
HashMap是 Map 接口使用频率最高的实现类HashMap: 作为Map的主要实现类,线程不安全的,效率高,可以存储null的key-value,即map.put(null,null)操作是可以的。LinkedHashMap:作为HashMap的子类,保证在遍历map元素时,可以按照添加的顺序实现遍历。因为它在原有的HashMap底层结构基础上。添加了一对指针,指向前一个和后一个元素。对于频繁的遍历操作,此类执行效率高于HashMapTreeMap:保证按照添加的key-value对进行排序,实现接序遍历。此时考虑key的自然排序或定制排序,底层使用红照树Hashtable:作为古老的实现类,线程安全的,效率低,不能存null的key-valueProperties:常用来处理配置文件。key-value都是String类型
图解

Map中的key:无序的、不可重复的,使用Set存储所有的key ,即key所在的类要重写equals()和hashCode() (以HashMap为例)
Map中的value:无序的、可重复的,使用Collection存储所有的value,即value所在的类要重写equals()
一个键值对:key-value 构成了一个Entry对象。
Map中的entry:无序的、不可重复的,使用Set存储所有的entry,我们执行Map.put(a,"BB")操作都是往Map里面放置一个一个的Entry对象
继承体系

1.2、Map中的常用方法
1.2.1、添加、删除、修改操作
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中的所有数据- 修改也由put来体现
实例:
public class Test {
public static void main(String[] args) {
Map map=new HashMap();
//map=new LinkedHashMap();
map.put("AA",123);//体现的是添加操作
map.put(45,123);
map.put("BB",56);
map.put("AA",87);//体现出来的是修改
System.out.println(map);//{AA=87, 45=123, BB=56},输出的格式是:key=value
Map map1=new HashMap();
map1.put("CC",123);
map1.put("DD",123);
map.putAll(map1);
System.out.println(map);//{AA=87, 45=123, BB=56, CC=123, DD=123}
Object obj=map.remove("CC");//返回要移除的键值对的value
System.out.println(obj);//123
System.out.println(map);//{AA=87, 45=123, BB=56, DD=123}
Object obj1=map.remove("CCC");//这个key是不存在的
System.out.println(obj1);//返回的是null
map.clear();//clear是清空map中的数据,不等同于map=null
System.out.println(map);//{}
System.out.println(map.size());//0
}
}
1.2.2、元素查询的操作
- Object get(Object key):获取指定key对应的value
- boolean containsKey(Object key):是否包含指定的key
- boolean containsValue(Object value):是否包含指定的value
- int size():返回map中key-value对的个数
- boolean isEmpty():判断当前map是否为空
- boolean equals(Object obj):判断当前map和参数对象obj是否相等
代码实现:
public class Test {
public static void main(String[] args) {
Map map=new HashMap();
map.put("AA",123);
map.put(45,123);
map.put("BB",56);
System.out.println(map.get(45));//123
System.out.println(map.get(455));//null,455这个key不存在返回的结果就是null
boolean isExist = map.containsKey("BB");//containsKey会去找哈希值,再根据哈希值判断数组中的位置,再看谁和它equals,看是不是包含这个key
System.out.println(isExist);//true
boolean containsValue = map.containsValue(123);//因为现在有两个123,所以只要找到一个就不会继续往下找了
System.out.println(containsValue);//true
map.clear();//把数组元素改成了null,把size改成了0
System.out.println(map.isEmpty());//true,这个方法如果size为0就会返回true
//要想equals方法的结果是true,参数也要是Map,同时里面存的数据也要一样
}
}
1.2.3、元视图操作的方法
- Set keySet():返回所有key构成的Set集合
- Collection values():返回所有value构成的Collection集合
- Set entrySet():返回所有key-value对构成的Set集合
涉及到如何去遍历Map中的key,value以及key-value
Map接口并没有继承Iterable接口,因此实现了Map接口的类不能直接通过map.iterator()得到迭代器,需要将Map类通过map.entrySet()转化为Set集合类(实现了Collection接口),再通过set.iterator()使用迭代器。
Map是由key和value构成的,所有的key使用Set存储,要想遍历所有key,则通过keySet()找到拿到key的Set,再用Set去iterator即可。value也一样,只要能够拿到所有的value,value的存在形式是Collection,再用Collection去调iterator方法即可,键值对整体构成了一个Set,要拿到所有的Entry构成的Set去iterator也可以
- 通过keySet()找到key
- values()找到value
- entrySet()找到(key,value)
遍历Map中的key
public class Test {
public static void main(String[] args) {
Map map=new HashMap();
map.put("AA",123);
map.put(45,123);
map.put("BB",56);
//遍历所有的key集:keySet()
Set set = map.keySet();//得到的set是由所有的key构成的,下一步就可以用迭代器了
Iterator iterator = set.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
}
}
遍历所有的value
//遍历所有的value集:values()
Collection values = map.values();
//方式一
Iterator iterator1 = values.iterator();
while(iterator1.hasNext()){
System.out.println(iterator1.next());
}
//方式二
Collection values = map.values();
for(Object obj:values){
System.out.println(obj);
}
遍历输出key和输出value的顺序是一致的,因为底层是Entry或者叫Node,是找到Node以后,如果要key就去拿key,如果拿value就去拿value
遍历所有的key-value
//方式一:entrySet(),返回的是Set,Set中放的是一个一个的Entry
Set entrySet = map.entrySet();
Iterator iterator1 = entrySet.iterator();
while(iterator1.hasNext()){
Object obj = iterator1.next();//其实这个obj是Entry,可以用Entry进行强制转换
Map.Entry entry = (Map.Entry) obj;
//在Entry中有getKey和getValue方法
System.out.println(entry.getKey()+"-->"+entry.getValue());
}
//方式二:用上面涉及到的方法拼凑出来:找到key之后使用get方法可以得到value
Set keySet =map.keySet();//得到的set是由所有的key构成的,下一步就可以用迭代器了
Iterator iterator2 = keySet.iterator();
while(iterator2.hasNext()){
Object key = iterator2.next();
Object value = map.get(key);
System.out.println(key+"-->"+value);
}
1.3、练习:遍历 Map
import java.util.*;
public class Test{
public static void main(String[] args) {
Map<String, String> map = new HashMap<String, String>();
map.put("1", "value1");
map.put("2", "value2");
map.put("3", "value3");
//第一种:普遍使用,二次取值
System.out.println("通过Map.keySet遍历key和value:");
for (String key : map.keySet()) {
System.out.println("key= "+ key + " and value= " + map.get(key));
}
//第二种
System.out.println("通过Map.entrySet使用iterator遍历key和value:");
Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, String> entry = it.next();
System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
}
//第三种:推荐,尤其是容量大时
System.out.println("通过Map.entrySet遍历key和value");
for (Map.Entry<String, String> entry : map.entrySet()) {
System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
}
//第四种
System.out.println("通过Map.values()遍历所有的value,但不能遍历key");
for (String v : map.values()) {
System.out.println("value= " + v);
}
}
}
2、HashMap实现类
2.1、HashMap概述
HashMap是Map接口的实现类,键值对存储(基于哈希表的映射:根据指定的键,可以获取对应的值),并允许null作为键和值,线程不安全,即方法为非同步方法。根据键的 HashCode 值存储数据,具有很快的访问速度,最多允许一条记录的键和值为 null,不支持线程同步
- HashMap是 Map 接口使用频率最高的实现类。
- 允许使用null键和null值,与HashSet一样,不保证映射的顺序。
- 所有的key构成的集合是Set,无序的、不可重复的。所以,key所在的类要重写: equals()和hashCode()
- 所有的value构成的集合是Collection,无序的、可以重复的。所以,value所在的类要重写:equals()
- 一个key-value构成一个entry
- 所有的entry构成的集合是Set,无序的、不可重复的 ,即不会记录插入的顺序
- HashMap 判断两个 key 相等的标准是:两个 key 通过 equals() 方法返回 true, hashCode 值也相等。
- HashMap 判断两个 value相等的标准是:两个 value 通过 equals() 方法返回 true。
继承体系
HashMap 继承于AbstractMap,实现了 Map、Cloneable、java.io.Serializable 接口。

HashMap 的 key 与 value 类型可以相同也可以不同,可以是字符串(String)类型的 key 和 value,也可以是整型(Integer)的 key 和字符串(String)类型的 value。
HashMap<Integer, String> Sites = new HashMap<Integer, String>();//泛型
2.2、HashMap的数据结构
下面内容参考:blog.csdn.net/qq_35080796…
2.2.1、JDK7
Java编程语言中,最基本的两种结构:数组和链表(引用模拟指针),所有的数据结构都可以用这两种基本结构进行构建。
- 数组的特点:寻址容易,插入和删除难;
- 链表的特点是:寻址困难,插入和删除容易。
综合数组和链表两者的特点,在jdk7.0里面,HashMap(直译为散列表,音译为哈希表)采用数组+链表的存储方式,(即为链地址法) 。底层结构是一个数组(默认长度为16),而数组的每一个元素是一个单向的链表,实际上是一个由Entry组成的链表,新加入的Entry放在链头,最先加入的放在链尾。每一个数组存储的元素代表的是每一个链表的头结点

2.2.2、jDK8
JDK1.8的HashMap源码实现和1.7是不一样的,有很大不同,其底层数据结构也不一样,引入了红黑树结构。有网友测试过,JDK1.8HashMap的性能要高于JDK1.7 15%以上,在某些size的区域上,甚至高于100%。随着size的变大,JDK1.7的花费时间是增长的趋势,而JDK1.8是明显的降低趋势,并且呈现对数增长稳定。当一个链表长度大于8的时候,HashMap会动态的将它替换成一个红黑树(JDK1.8引入红黑树大程度优化了HashMap的性能),这会将时间复杂度从O(n)降为O(logn)。
HashMap的内部存储结构其实是数组+链表+红黑树的结合。当实例化一个 HashMap时,会初始化initialCapacity 和 loadFactor,在put第一对映射关系 时,系统会创建一个长度为initialCapacity的Node数组,这个长度在哈希表中被称为容量(Capacity),在这个数组中可以存放元素的位置我们称之为 “桶”(bucket),每个bucket都有自己的索引,系统可以根据索引快速的查找bucket中的元素。
每个bucket中存储一个元素,即一个Node对象,但每一个Node对象可以带 一个引用变量next,用于指向下一个元素,因此,在一个桶中,就有可能 生成一个Node链。也可能是一个一个TreeNode对象,每一个TreeNode对象 可以有两个叶子结点left和right,因此,在一个桶中,就有可能生成一个 TreeNode树。而新添加的元素作为链表的last,或树的叶子结点。

2.3、HashMap的底层实现原理--源码分析
2.3.1、JDK7的底层源码分析
下面内容来自:blog.csdn.net/qq_35080796…
以下面代码出发开始从源码分析底层原理实现
HashMap map = new HashMap();
map.put(1,"value1"); //key = 1是自动装箱
1、成员变量
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable{
/** 初始容量,默认16,1 << 4 代表将1左移4位:2^4 = 16 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/** HashMap的最大支持容量,2^30 */
static final int MAXIMUM_CAPACITY = 1 << 30;
/** HashMap的默认负载因子,默认0.75,负载因子越小,hash冲突机率越低 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/** 初始化一个Entry类型的空数组 */
static final Entry<?,?>[] EMPTY_TABLE = {};
/** 将初始化好的空数组赋值给table,table数组是HashMap实际存储数据的地方,
并不在EMPTY_TABLE数组中,长度总是2的n次幂
*/
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
/** HashMap实际存储的元素--键值对Entry个数 */
transient int size;
/** 扩容的临界值(HashMap实际能存储的大小),公式为(threshold = 数组容量capacity * 填充因子loadFactor) 当size>threshold的时候就会扩容。*/
int threshold;
/** 填充因子 */
final float loadFactor;
/** HashMap的结构被修改的次数,用于迭代器 */
transient int modCount;
DEFAULT_INITIAL_CAPACITY:HashMap的默认容量,16MAXIMUM_CAPACITY: HashMap的最大支持容量,2^30DEFAULT_LOAD_FACTOR:HashMap的默认加载因子,负载因子表示一个散列空间的使用程度。 当向集合容器中添加元素的时候,会判断当前容器的个数: 如果当前容器的个数 > 阈(yu)值:即底层数组长度*负载因子就会扩容TREEIFY_THRESHOLD:Bucket中链表长度大于该默认值,转化为红黑树UNTREEIFY_THRESHOLD:Bucket中红黑树存储的Node小于该默认值,转化为链表MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量。(当桶中Node的 数量大到需要变红黑树时,若hash表容量小于MIN_TREEIFY_CAPACITY时,此时应执行 resize扩容操作这个MIN_TREEIFY_CAPACITY的值至少是TREEIFY_THRESHOLD的4 倍。)table:存储元素的数组,总是2的n次幂entrySet:存储具体元素的集size:HashMap中存储的键值对的数量modCount:HashMap扩容和结构改变的次数。threshold:扩容的临界值,=底层数组容量*填充因子loadFactor:填充因子
2、table数组、Entry类
从源码中可以看到Entry<K,V>是HashMap类中的一个静态内部类,它既是HashMap底层数组的组成元素,又是每一个单向链表的组成元素,并且它包含了元素的key和value,以及链表所需要的指向下一个节点的地址区next,存储的具体结构如下:
/**
* The table, resized as necessary. Length MUST Always be a power of two.
*/
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
// 此静态内部类和内部的属性和方法都是包级访问权限
static class Entry<K,V> implements Map.Entry<K,V> {
final K key; // 存储的键
V value; // 存储的值
Entry<K,V> next; // 链表中指向下一个元素的指针
final int hash; // hash值,用来确定链表上元素存储或取出的位置
……
}
可以看出,Entry 就是数组中的元素,每个数组中的元素 Map.Entry 其实就是一个 key-value 对,它持有一个指向下一个元素的引用,这就构成了链表。对于数组,扩容是必不可少的,长度每次扩大两倍
3、HashMap的构造方法
public HashMap() { //默认初始容量和负载因子:16和0.75
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
//该构造器可以指定数组长度(容量)和负载因子
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);
// Find a power of 2 >= initialCapacity
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
// 设置填充因子为默认负载因子0.75
this.loadFactor = loadFactor;
threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
init();
}
通过有参构造器可以指定数组长度(容量)和负载因子,经过了这段构造器初始化代码,最终的结果就是初始化了一个长度为16的Entry类型的table空数组
4、put方法
public V put(K key, V value) {
// HashMap 允许存放 null 键和 null 值。
// 当 key 为 null 时,调用 putForNullKey 方法,将 value 放置在数组第一个位置。
if (key == null)
return putForNullKey(value);
// 根据 key 的 keyCode 重新计算 hash 值。
int hash = hash(key.hashCode());
// 搜索指定 hash 值在对应 table 中的索引。
int i = indexFor(hash, table.length);
// 如果 i 索引处的 Entry 不为 null,通过循环不断遍历 e 元素的下一个元素。
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
// 如果 i 索引处的 Entry 为 null,表明此处还没有 Entry。
modCount++;
// 将 key、value 添加到 i 索引处。
addEntry(hash, key, value, i);
return null;
}
从上面的源代码中可以看出:当我们往 HashMap 中 put 元素的时候,先根据 key 的 hashCode 利用hash方法重新计算 hash 值,根据 hash 值得到这个元素在数组中的位置(即下标),如果数组该位置上已经存放有其他元素了,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置(bucket )上。
5、addEntry方法
addEntry(hash, key, value, i)方法根据计算出的 hash 值,将 key-value 对放在数组 table
的 i 索引处。这个方法就是形成单链表的方法,addEntry 是 HashMap 提供的一个包访问权限的方法,代码如下:
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;//hash方法
bucketIndex = indexFor(hash, table.length);//indexFor方法
}
createEntry(hash, key, value, bucketIndex);
}
当系统决定存储 HashMap 中的 key-value 对时,完全没有考虑 Entry 中的 value,仅仅只是根据key来计算并决定每个Entry的存储位置。我们完全可以把 Map 集合中的 value 当成 key 的附属,当系统决定了 key 的存储位置之后,value 随之保存在那里即可。
6、hash方法
hash(int h)方法根据 key 的 hashCode 重新计算一次散列。此算法加入了高位计算,防止低位不变,高位变化时,造成的 hash 冲突。
static int hash(int h) {
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
7、indexFor方法
我们可以看到在 HashMap 中 要找到某个元素,需要根据 key 的 hash 值来求得对应数组中的位置。如何计算这个位置就是 hash 算法。前面说过 HashMap 的数据结构是数组和链表的结合,所以我们当然希望这个 HashMap 里面的元素位置尽量的分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用 hash 算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,而不用再去遍历链表,这样就大大优化了查询的效率。
对于任意给定的对象,只要它的 hashCode() 返回值相同,那么程序调用 hash(int h) 方法所计算得到的 hash 码值总是相同的。我们首先想到的就是把 hash 值对数组长度取模运算,这样一来, 元素的分布相对来说是比较均匀的。但是,“模”运算的消耗还是比较大的,在 HashMap 中是这样做的:调用 indexFor(int h, int length) 方法来计算该对象应该保存在 table 数组的哪个索引处。
indexFor(int h, int length) 方法的代码如下:
static int indexFor(int h, int length) {
return h & (length-1);
}
这个方法非常巧妙,它通过 h & (table.length -1) 来得到该对象的保存位,而 HashMap底层数组的长度总是 2 的 n 次方,这是 HashMap 在速度上的优化。
2.3.2、JDK8中底层源码分析
1、get方法
思路如下:
- bucket里的第一个节点,直接命中;
- 如果有冲突,则通过key.equals(k)去查找对应的entry
- 若为树,则在树中通过key.equals(k)查找,O(logn);
- 若为链表,则在链表中通过key.equals(k)查找,O(n)。
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;
// first指向hash值对应数组位置中的Node节点
if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
// 如果first节点对应的hash和key的hash相等(在数组相同位置,只是说明 hash&(n-1) 操作结果相等, 说明hash值的部分低位相等,
// 并不代表整个hash值相等), 并且first对应的key也相等的话, first节点就是要查找的
if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) { // 说明存在hash冲突
if (first instanceof TreeNode) // 说明由红黑树对hash值冲突进行管理
return ((TreeNode<K, V>) first).getTreeNode(hash, key); // 查找红黑树
do { // 说明hash值冲突是由链表进行管理
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null); // 对链表进行遍历
}
}
return null;
}
2、put方法
大致的思路为:
- 对key的hashCode()做hash,然后再计算index;
- 如果没碰撞直接放到bucket里;
- 如果碰撞了,以链表的形式存在buckets后;
- 如果碰撞导致链表过长(大于等于TREEIFY_THRESHOLD),就把链表转换成红黑树;
- 如果节点已经存在就替换old value(保证key的唯一性)
- 如果bucket满了(超过load factor*current capacity),就要resize。
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) {
Node<K, V>[] tab;
Node<K, V> p;
int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length; // 当数组table为null时, 调用resize生成数组table, 并令tab指向数组table
if ((p = tab[i = (n - 1) & hash]) == null) // 如果新存放的hash值没有冲突
tab[i] = newNode(hash, key, value, null); // 则只需要生成新的Node节点并存放到table数组中即可
else { // 否则就是产生了hash冲突
Node<K, V> e;
K k;
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p; // 如果hash值相等且key值相等, 则令e指向冲突的头节点
else if (p instanceof TreeNode) // 如果头节点的key值与新插入的key值不等, 并且头结点是TreeNode类型,说明该hash值冲突是采用红黑树进行处理.
e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value); // 向红黑树中插入新的Node节点
else { // 否则就是采用链表处理hash值冲突
for (int binCount = 0;; ++binCount) { // 遍历冲突链表, binCount记录hash值冲突链表中节点个数
if ((e = p.next) == null) { // 当遍历到冲突链表的尾部时
p.next = newNode(hash, key, value, null); // 生成新节点添加到链表末尾
if (binCount >= TREEIFY_THRESHOLD - 1) // 如果binCount即冲突节点的个数大于等于 (TREEIFY_THRESHOLD(=8) - 1),便将冲突链表改为红黑树结构, 对冲突进行管理,
// 否则不需要改为红黑树结构
treeifyBin(tab, hash);
break;
}
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) // 如果在冲突链表中找到相同key值的节点, 则直接用新的value覆盖原来的value值即可
break;
p = e;
}
}
if (e != null) { // 说明原来已经存在相同key的键值对
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null) // onlyIfAbsent为true表示仅当<key,value>不存在时进行插入, 为false表示强制覆盖;
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount; // 修改次数自增
if (++size > threshold) // 当键值对数量size达到临界值threhold后, 需要进行扩容操作.
resize();
afterNodeInsertion(evict);
return null;
}
3、treeifyBin
jdk1.8中对hashmap有着非常棒的扩容机制,当链表长度大于某个值的时候,hashmap中的链表会变成红黑树结构,但是实际上真的是这样么?我们来看一下树化的函数是怎样进行的:
//该方法的主要作用是将冲突链表改为红黑树
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
//当数组的长度< MIN_TREEIFY_CAPACITY(64) 时,只是单纯将数组扩容, 而没有直接将链表改为红黑树.
//因为hash数组长度还太小时导致多冲突的主要原因, 增大hash数组长度可以改善冲突情况
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
我们从第一个判断语句就发现,如果hashmap中table的长度小于64(MIN_TREEIFY_CAPACITY)的时候,其实是不会进行树化的,而是对这个hashmap进行扩容。所以我们发现,扩容不仅仅用于node的个数超过threshold的时候。
2.4、HashMap 的扩容机制
2.4.1、jdk7
当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的长度是固定的。所以为了提高查询的效率,就要对HashMap的数组进行扩容,而在 HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是 resize。 那么HashMap什么时候进行扩容呢?
那么HashMap什么时候进行扩容呢?当 HashMap 中的元素个数超过 数组大小 * loadFactor 时,就会进行数组扩容,loadFactor 的默认值为 0.75 ,这是一个折中的取值,符合泊淞分布(如果loadFactor过大,说明table数组存数据非常多,密集,可能出现某一位置链表非常长,但数组上始终达不到存储的个数,从而不会触发扩容;反之,存数据很少,稀疏,导致扩容频繁,影响性能)。扩容时通过调用resize方法重新创建一个原来HashMap大小的两倍的newTable数组,最大扩容到2^30+1,并将原先table的元素全部移到newTable里面,重新计算hash,然后再重新根据hash分配位置。这个过程叫作rehash,因为它调用hash方法找到新的bucket位置。
也就是说,默认情况下,数组大小为 16 ,那么当 HashMap 中元素个数超过 16 * 0.75 = 12的时候,就把数组的大小扩展为 2 * 16 = 32 ,即扩大一倍,然后重新计算每个元素在新数组中的位置(rehash),而这是一个非常消耗性能的操作,所以如果我们已经预知 HashMap 中元素的个数,那么预设元素的个数能够有效的提高HashMap 的性能。
2.4.2、jdk8
那么HashMap什么时候进行扩容和链表的树形化呢?
jdk1.8的table长度一定是2的幂
当HashMap中的元素个数超过 数组大小(数组总大小length,不是数组中个数 size)*loadFactor 时 , 就会进行数组扩容 , loadFactor 的默认 值 (DEFAULT_LOAD_FACTOR)为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小(DEFAULT_INITIAL_CAPACITY)为16,那么当HashMap中 元素个数超过16*0.75=12(这个值就是代码中的threshold值,也叫做临界值) 的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元 素在数组中的位置,而这是一个非常消耗性能的操作
所以如果我们已经预知 HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。 当HashMap中的其中一个链的对象个数如果达到了8个,此时如果capacity没有 达到64,那么HashMap会先扩容解决,如果已经达到了64,那么这个链会变成树,结点类型由Node变成TreeNode类型。当然,如果当映射关系被移除后, 下次resize方法时判断树的结点个数低于6个,也会把树再转为链表。
2.5、总结
2.5.1、jdk7
//以jdk7为例说明:
HashMap map = new HashMap()//在实例化以后,底层创建了长度是16的一维数组Entry[] table。
...可能已经执行过多次put...
map.put(key1,value1):
首先,调用key1所在类的hashCode()计算key1哈希值,此哈希值经过某种算法计算以后,得到在Entry数组中的存放位置
- 如果此位置上的数据为空,此时的
key1-value1添加成功。 ----情况1 - 如果此位置上的数据不为空,(意味着此位置上存在一个或多个数据(以链表形式存在)),比较
key1和已经存在的一个或多个数据的哈希值:- 如果
key1的哈希值与已经存在的数据的哈希值都不相同,此时key1-value1添加成功。----情况2 - 如果
key1的哈希值和已经存在的某一个数据(key2-value2)的哈希值相同,继续比较:- 调用
key1所在类的equals(key2)方法,比较:- 如果
equals()返回false:此时key1-value1添加成功。----情况3 - 如果
equals()返回true:使用value1替换value2
- 如果
- 调用
- 如果
补充:
- 关于情况2和情况3:此时
key1-value1和原来的数据以链表的方式存储。 - 在不断的添加过程中,会涉及到扩容问题,当超出临界值(且要存放的位置非空)时,扩容。默认的扩容方式:扩容为原来容量的2倍,并将原有的数据复制过来。
2.5.2、jdk8
jdk8 相较于jdk7在底层实现方面的不同:
- new HashMap():底层没有创建一个长度为16的数组
- jdk 8底层的数组是:Node[],而非Entry[]
- 首次调用put()方法时,底层创建长度为16的数组
- jdk7底层结构只有:数组+链表。jdk8中底层结构:数组+链表+红黑树。
- 形成链表时,七上八下(jdk7:新的元素指向旧的元素。jdk8:旧的元素指向新的元素)
- 当数组的某一个索引位置上的元素以链表形式存在的数据个数 > 8 且当前数组的长度 > 64时,此时此索引位置上的所数据改为使用红黑树存储。
哈希冲突
通过红黑树的方式来处理哈希冲突是我第一次看见!学过哈希,学过红黑树,就是 从来没想到两个可以结合到一起这么用! 按照原来的拉链法来解决冲突,如果一个桶上的冲突很严重的话,是会导致哈希表 的效率降低至 O(n),而通过红黑树的方式,可以把效率改进至 O(logn)。相比 链式结构的节点,树型结构的节点会占用比较多的空间,所以这是一种以空间换时 间的改进方式
源码:
//遍历旧哈希表的每个桶,重新计算桶里元素的新位置
for (int j = 0; j < oldCap; ++j) {
Node 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)e).split(this, newTab, j, oldCap);
//如果采用链式处理冲突
else {
// preserve order Node loHead = null,
loTail = null;
Node hiHead = null,
hiTail = null;
Node next;
2.6、性能问题
HashMap有两个参数影响其性能:初始容量和负载因子。均可以通过构造方法指定大小。
容量capacity是HashMap中bucket哈希桶(Entry的链表)的数量,初始容量只是HashMap在创建时的容量,最大设置初始容量是2^30,默认初始容量是16(必须为2的幂),解释一下,当数组长度为2的n次幂的时候,不同的key通过indexFor()方法算得的数组位置相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,get()的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。
负载因子loadFactor是HashMap在其容量自动增加之前可以达到多满的一种尺度,默认值是0.75。
2.7、线程安全
HashMap是线程不安全的,在多线程情况下直接使用HashMap会出现一些莫名其妙不可预知的问题。在多线程下使用HashMap,有几种方案:
- 在外部包装HashMap,实现同步机制
- 使用
Map m = Collections.synchronizedMap(new HashMap(...));实现同步(官方参考方案,但不建议使用,使用迭代器遍历的时候修改映射结构容易出错) - 使用
java.util.HashTable,效率最低(几乎被淘汰了) - 使用
java.util.concurrent.ConcurrentHashMap,相对安全,效率高(建议使用)
注意一个小问题,HashMap所有集合类视图所返回迭代器都是快速失败的(fail-fast),在迭代器创建之后,如果从结构上对映射进行修改,除非通过迭代器自身的 remove 或 add 方法,其他任何时间任何方式的修改,迭代器都将抛出 ConcurrentModificationException。因此,面对并发的修改,迭代器很快就会完全失败。
2.8、面试题
谈谈你对HashMap中put/get方法的认识?如果了解再谈谈 HashMap的扩容机制?默认大小是多少?什么是负载因子(或填充比)?什么是吞吐临界值(或阈值、threshold)?
答:
- DEFAULT_INITIAL_CAPACITY : HashMap的默认容量,16
- DEFAULT_LOAD_FACTOR:HashMap的默认加载因子:0.75
- threshold:扩容的临界值,=容量*填充因子:16 * 0.75 => 12
- TREEIFY_THRESHOLD:Bucket中链表长度大于该默认值,转化为红黑树:8
- MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量:64
负载因子loadfactor值的大小,对HashMap有什么影响
负载因子 loadFactor 定义为:散列表的实际元素数目(n)/ 散列表的容量(m)。 负载因子衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越 高,反之愈小。对于使用链表法的散列表来说,查找一个元素的平均时间是 O(1+a),因此如果负载因子越大,对空间的利用更充分,然而后果是查找效率的降低;如果负载因子太小, 那么散列表的数据将过于稀疏,对空间造成严重浪费。
- 负载因子的大小决定了
HashMap的数据密度。 - 负载因子越大密度越大,发生碰撞的几率越高,数组中的链表越容易长, 造成查询或插入时的比较次数增多,性能会下降。
- 负载因子越小,就越容易触发扩容,数据密度也越小,意味着发生碰撞的 几率越小,数组中的链表也就越短,查询和插入时比较的次数也越小,性能会更高。但是会浪费一定的内容空间。而且经常扩容也会影响性能,建议初始化预设大一点的空间。
- 按照其他语言的参考及研究经验,会考虑将负载因子设置为
0.7~0.75,此时平均检索长度接近于常数
threshold 字段
HashMap 的实现中,通过 threshold 字段来判断 HashMap 的最大容量:
threshold = (int)(capacity * loadFactor);
结合负载因子的定义公式可知,threshold 就是在此 loadFactor 和 capacity 对应下允许的 最大元素数目,超过这个数目就重新 resize,以降低实际的负载因子。默认的的负载因子 0.75是对空间和时间效率的一个平衡选择。当容量超出此最大容量时,resize后的HashMap 容量是容量的两倍:
if (size++ >= threshold)
resize(2 * table.length);
3、LinkedHashMap实现类(了解)
3.1、概述
LinkedHashMap 是 HashMap 的子类 ,在HashMap存储结构的基础上,使用了一对双向链表来记录添加元素的顺序,与LinkedHashSet类似,LinkedHashMap 可以维护 Map 的迭代顺序:迭代顺序与 Key-Value 对的插入顺序一致
LinkedHashMap的特点
- key和value都允许为空
- key重复会覆盖,value可以重复
- 有序的
- LinkedHashMap是非线程安全的
LinkedHashMap的基本结构
- LinkedHashMap可以认为是HashMap+LinkedList,也就是说,它使用HashMap操作数据结构,也用LinkedList维护插入元素的先后顺序.
- LinkedHashMap的实现思想就是多态,理解LinkedHashMap能帮助我们加深对多态的理解.
LinkedHashMap 的实现主要分两部分,一部分是哈希表,另外一部分是双向链表。哈希 表部分继承了 HashMap,拥有了 HashMap 那一套高效的操作,所以我们要看的就是 LinkedHashMap 中链表的部分,了解它是如何来维护有序性的。 LinkedHashMap 的大致实现如下图所示,当然链表和哈希表中相同的键值对都是指 向同一个对象,这里把它们分开来画只是为了呈现出比较清晰的结构

3.2、源码简析
LinkedHashMap的定义:
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>{
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
//继承了HashMap的Node类
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);
}
}
我在 LinkedHashMap 的源码中没有找到 put 方法,这就说明了它并没有重写 put 方 法,所以我们调用的 put 方法其实是 HashMap 的 put 方法。因为 HashMap 的 put 方 法中调用了 afterNodeAccess 方法和 afterNodeInsertion 方法,已经足够保证链表的有 序性了,所以它也就没有重写 put 方法了。remove 方法也是如此
4、TreeMap 实现类
4.1、概述
TreeMap存储 Key-Value 对时,需要根据 key-value 对进行排序。 TreeMap 可以保证所有的 Key-Value 对处于有序状态。 向TreeMap中添加key-value,要求key必须是由同一个类创建的对象,因为要按照key进行排序:自然排序 、定制排序。
- TreeSet底层使用红黑树结构存储数据
- TreeMap 的 Key 的排序:
- 自然排序:TreeMap 的所有的 Key 必须实现 Comparable 接口,而且所有 的 Key 应该是同一个类的对象,否则将会抛出 ClasssCastException
- 定制排序:创建 TreeMap 时,传入一个 Comparator 对象,该对象负责对 TreeMap 中的所有 key 进行排序。此时不需要 Map 的 Key 实现 Comparable 接口
- TreeMap判断两个key相等的标准:两个key通过compareTo()方法或 者compare()方法返回0。
4.2、自然排序
User类
package com.lemon.java;
import java.util.Objects;
public class User implements Comparable{
private String name;
private int age;
public User() {
}
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return age == user.age &&
Objects.equals(name, user.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
//按照姓名从大到小排列,年龄从小到大排列
@Override
public int compareTo(Object o) {
if (o instanceof User){
User user = (User) o;
int compare = -this.name.compareTo(user.name); //前面加负号就是降序排列
if (compare != 0){
return compare;
} else {
return Integer.compare(this.age,user.age); //如果姓名一样就比较年龄升序排列
}
} else {
throw new RuntimeException("输入的类型不匹配");
}
}
}
测试
public class TreeMapeTest {
//自然排序
@Test
public void test(){
TreeMap map = new TreeMap();
User u1 = new User("Tom",11);
User u2 = new User("Team",30);
User u3 = new User("John",23);
User u4 = new User("Jerry",18);
map.put(u1,11);
map.put(u2,11);
map.put(u3,22);
map.put(u4,33);
Set entrySet = map.entrySet();
Iterator iterator = entrySet.iterator();
while (iterator.hasNext()){
Object obj = iterator.next();
Map.Entry entry = (Map.Entry) obj;
System.out.println(entry.getKey() + "------>" + entry.getValue());
}
}
}
结果
User{name='Tom', age=11}------>11
User{name='Team', age=30}------>11
User{name='John', age=23}------>22
User{name='Jerry', age=18}------>33
4.3、定制排序
//定制排序
@Test
public void test2(){
TreeMap map = new TreeMap(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
if (o1 instanceof User && o2 instanceof User){
User u1 = (User) o1;
User u2 = (User) o2;
return Integer.compare(u1.getAge(),u2.getAge());
} else {
throw new RuntimeException("输入的类型不匹配");
}
}
});
User u1 = new User("Tom",11);
User u2 = new User("Team",30);
User u3 = new User("John",23);
User u4 = new User("Jerry",18);
map.put(u1,11);
map.put(u2,11);
map.put(u3,22);
map.put(u4,33);
Set entrySet = map.entrySet();
Iterator iterator = entrySet.iterator();
while (iterator.hasNext()){
Object obj = iterator.next();
Map.Entry entry = (Map.Entry) obj;
System.out.println(entry.getKey() + "------>" + entry.getValue());
}
}
结果:
User{name='Tom', age=11}------>11
User{name='Jerry', age=18}------>33
User{name='John', age=23}------>22
User{name='Team', age=30}------>11
5、Hashtable实现类
Hashtable 可以说已经具有一定的历史了,现在也很少使用到 Hashtable 了,更多的是使 用 HashMap 或 ConcurrentHashMap。HashTable 是一个线程安全的哈希表,它通过使用 synchronized 关键字来对方法进行加锁,从而保证了线程安全。但这也导致了在单线程 环境中效率低下等问题。Hashtable 与 HashMap 不同,它不允许插入 null 值和 null 键。
- Hashtable实现原理和HashMap相同,功能相同。底层都使用哈希表结构,查询 速度快,很多情况下可以互用。
- 与HashMap不同,Hashtable 不允许使用 null 作为 key 和 value
- 与HashMap一样,Hashtable 也不能保证其中 Key-Value 对的顺序
- Hashtable判断两个key相等、两个value相等的标准,与HashMap一致。
//哈希表
private transient Entry[] table;
//记录哈希表中键值对的个数
private transient int count;
//扩容的阈值
private int threshold;
//负载因子
private float loadFactor;
6、Properties实现类
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);
实例
准备配置文件:默认情况会识别在当前工程下,所以新建一个jdbc.properties文件
#最好字段前加前缀
name=小米
age=13
程序读取
package com.lemon.java;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;
/**
* @Author Lemons
* @create 2022-02-25-20:04
*/
public class PropertiesTest {
public static void main(String[] args) throws Exception{
FileInputStream fis =null;
try {
Properties pros = new Properties();
fis = new FileInputStream("jdbc.properties");
pros.load(fis); //加载流对应的文件
String name = pros.getProperty("name");
String age = pros.getProperty("age");
System.out.println("name = " + name +",age = " + age);
} catch (IOException e) {
e.printStackTrace();
} finally {
if(fis != null){
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
结果
name = 小米,age = 13