一、Map集合类思维导图
Map:使用键值对存储。Map会维护与Key有关联的值。两个Key可以引用相同的对象,但Key不能重复,典型的Key是String类型,但也可以是任何对象。
二、HashMap、Hashtable、LinkedHashMap及TreeMap区别简述
- Hashmap:散列表,存储Key-Value值,继承于AbstractMap,实现Map、Cloneable、Serializable接口,非线程安全,无序
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
- Hashtable:散列表,存储Key-Value值,继承于Dictionary,实现了Map、Cloneable、Java.io.Serializable接口,采用synchronized实现线程安全的,无序
public class Hashtable<K,V>
extends Dictionary<K,V>
implements Map<K,V>, Cloneable, java.io.Serializable
- LinkedHashMap:双向链表,继承于HashMap,非线程安全的,允许key,value为null,有序
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>
- TreeMap:红黑树(自平衡二叉查找树),存储Key-Value值,继承于AbstractMap,实现了NavigableMap、Cloneable、Java.io.Serializable接口,非线程安全,有序
public class TreeMap<K,V>
extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable
三、详解及示例说明
- HashMap
1、存储结构:Entry数组+链表+红黑树
上面左侧是Node[] table哈希桶数组,存放Node链表元素,HashMap最多只允许一条Entry的键位null(多了会覆盖),允许多条Entry的值为null。
HashMap采用哈希表存储,在HashMap中采用拉链法解决哈希冲突;拉链法就是数组+链表,每个数组元素都是一个链表,当Key.hashCode()方法得到hashCode值,然后通过哈希算法(高位运算+取模运算)来定位键值的存储位置,如果Key值相同,则产生哈希碰撞,将数据放在对应下标元素的链表上。
为了让HashMap存储高效,要减少hash碰撞,也就是要hash算法计算结果尽量均匀。如果哈希桶数组很大,较差的hash算法也会分布较均匀,Hash值的范围值是-2147483648~2147483647,前后约40亿的映射空间,基本不会出现碰撞,但是哈希桶数组不可能这么大,内存也放不下;如果hash哈希桶数组很小,较好的hash算法也会很产生较多的hahs碰撞。由此可见,好的Hash算法和好的扩容机制是节省时间&空间和减少hash碰撞的重要因素。
简单看下HashMap如何设计Hash算法,HashMap中hash源码如下
JDK1.7
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);
}
JDK1.8 Hash算法本质:取key的hashCode值、高位运算、取模运算
static final int hash(Object key) {
int h;
// key.hashCode():返回散列值也就是hashcode
// ^ :按位异或
// >>>:无符号右移,忽略符号位,空位都以0补齐
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
大家都知道位运算效率高于取余运算,所以这里采用位运算,也就决定了HashMap的长度是2的幂次方。
2、多线程操作HashMap导致死循环(JDK1.7)
Hash表如果有新的数据要插入时,会检查容量是否超过设定门限值,如果超过就要对Hash表进行扩容,用新的数组代替原有数组,这里需要对表中所有元素进行rehash计算,所以扩容是一个特别损耗性能的操作,HashMap默认容量是16,我们在初始定义的时候要根据需要确定合适的容量大小,减少不必要的性能损耗。
详细介绍多线程操作导致死锁循环问题
正常的rehash过程如下
首先哈希桶数组初始大小是2,由于hash冲突,三个元素都在table[1] 链表元素里了,Node<k,v>分别为(3,A)、(7,B)和(5,C),哈希桶数组大小不够需要扩容2到4。
下面有Thread1和Thread2两个线程同时操作HashMap进行扩容,Thread1执行到下图代码注释处,Thread2已经扩容完成
void transfer(Entry[] newTable)
{
Entry[] src = table;
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;
do {
Entry<K,V> next = e.next;//Thread1挂起
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}
此时,Thread1挂起,e指向3,next指向7,Thread2扩容完成,链表由上图的Thread1链表变成了Thread2的链表,当再次回到Thread1时采用头插法,e.next = newTable[i]//null,即节点3指向空,newTable[i] = e,即节点3插入到table[3] 中第一个位置,此时,next值依然为节点7,执行e=next,e值指向节点7如下图
此时,由于链表变化,不再是扩容前size=2时的链表顺序,而是Thread2扩容之后的链表顺序,导致next=e.next,即next为节点3而不是节点5,这时候再执行e.next = newTable[i]//节点3,就造成了下图的环形链表,造成死循环。
JDK1.8之后采用尾插法替代了头插法,同时增加了红黑树,极大优化了HashMap的性能,避免了死循环的问题,但还是会造成put元素丢失数据的情况,所以HashMap就是线程不安全的,如果想用线程安全的,JDK建议使用ConcurrentHashMap。
3、HashMap构造方法
- HashMap():它是默认的构造函数,它创建一个初始容量为16,加载因子为0.75的HashMap实例。
- HashMap(int initial capacity):它创建一个具有指定初始容量和加载因子0.75的HashMap实例。
- HashMap(int initial capacity,float loadFactor):它创建一个具有指定初始容量和指定加载因子的HashMap实例。
- HashMap(Map map):使用与指定映射相同的映射创建HashMap的实例。
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
// 包含另一个“Map”的构造函数
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);//下面会分析到这个方法
}
// 指定“容量大小”的构造函数
public HashMap(int initialCapacity) {
this(initialCapacity, 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);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
loadFactor加载因子用来控制数组存放数据疏密程度,太大会导致数组利用率低,太小导致存放数据分散,数组利用率低,官方默认值0.75f是一个比较合适的临界值。上面提到的扩容门限值threshold=capacity*loadFactor,当size>=threshold就要扩容。负载因子的值在0和1之间变化。初始容量&负载因数的设定极大程度上决定了HashMap的使用性能
4、HashMap常用方法代码示例
package Map;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
public class Hashmapdemo1 {
public static void main(String[] args) {
HashMap<Integer, String> map = new HashMap<>();
//key不能重复,value值可以,key重复会被覆盖
map.put(1, "sss");
map.put(1, "Java");
map.put(2, "is");
map.put(3, "beautiful");
System.out.println("------直接输出hashmap: -------");
System.out.println(map);
//遍历输出hashmap
//1、通过keySet()遍历
System.out.println("------遍历keySet()遍历hashmap: -------");
for (int key : map.keySet()) {
System.out.println("key: " + key + "; value: " + map.get(key));
}
//2、通过entrySet()遍历
System.out.println("------遍历entrySet()遍历hashmap: -------");
for (Map.Entry<Integer, String> entry : map.entrySet()) {
System.out.println("key: " + entry.getKey() + "; value: " + entry.getValue());
}
//3、通过values遍历值
System.out.println("------遍历values遍历hashmap值: -------");
for (String value : map.values()) {
System.out.println("values: " + value);
}
//4、通过迭代器遍历entrySet()
System.out.println("------遍历entrySet().iterator遍历hashmap: -------");
Iterator<Map.Entry<Integer, String>> it = map.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<Integer, String> entryit = it.next();
System.out.println("key: " + entryit.getKey() + "; value: " + entryit.getValue());
}
System.out.println("map大小:" + map.size());
System.out.println("map是否为空:" + map.isEmpty());
System.out.println("map移除key为2的值:" + map.remove(2));
System.out.println("map移除key为2之后:" + map);
System.out.println("map取key为1的值:" + map.get(1));
System.out.println("map是否包含key为2的元素:" + map.containsKey(2));
System.out.println("map是否包含value为Java的元素:" + map.containsValue("Java"));
System.out.println("map替换key为1的值为 C++:" + map.replace(1, "C++"));
System.out.println("map替换key为1的值为 C++之后map:" + map);
}
}
注:使用entrySet()遍历方式比keySet()快,entrySet是直接从Entry对象中获得,时间复杂度o(1),keySet遍历要从Map中重新获取,时间复杂度为o(n),keySet比entrySet多遍历了一次table。
- Hashtable
Hashtable与HashMap类似,不同的地方在于Hashtable不允许键值为null;同时Hashtable是线程安全的,采用synchronized修饰,但是由于synchronized的修饰的是全表,导致同步性能非常差,总体上速度比HashMap慢,现在Hashtable基本没怎么用,这里就不深入探索了。
- ConcurrentHashMap
底层采用分段的数组+链表/红黑树,JDK1.7时,ConcurrentHashMap对整个哈希桶数组进行分割分段(Segment),每一把锁只锁其中一部分数据,提高并发访问效率,JDK1.8时,ConcurrentHashMap没有Segment概念,通过synchronized+CAS来实现线程安全。对比Hashtable对整个存储结构用同一把锁,效率提高很多,所以涉及线程安全的map建议使用concurrentHashMap。
- LinkedHashMap
HashMap是集合Map中最为常用的,但是HashMap是无序的,LinkedHashMap为我们解决了这个问题,作为HashMap的子类,虽然增加了时间&空间的开销,但是它通过额外的双向链表保证了迭代顺序。迭代顺序可以是插入时的顺序,也可以是访问的顺序。LinkedHashMap结构图如下
这些Node<k,v>中,出去橙色绿色的线,就是上面HashMap的结构,单独的橙色绿色的线就是一个双向链表,由此可见LinkedHashMap结构其实就是HashMap&LinkedeList融合体。
默认的是按照插入顺序迭代输出的,示例代码:
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
public class MapDemo {
public static void main(String[] args) {
HashMap<Character, String> map = new HashMap<>();
map.put('a', "Java1");
map.put('z', "Java2");
map.put('b', "Java5");
map.put('h', "Java3");
map.put('s', "Java4");
Iterator<Map.Entry<Character, String>> it1 = map.entrySet().iterator();
while (it1.hasNext()) {
// 按照hash顺序输出(对比插入顺序是“无序”的)
Map.Entry<Character, String> entry = it1.next();
System.out.println(entry.getKey()+" "+entry.getValue());
}
System.out.println();
LinkedHashMap<Character, String> linkedMap = new LinkedHashMap<>();
linkedMap.put('a', "Java1");
linkedMap.put('z', "Java2");
linkedMap.put('b', "Java5");
linkedMap.put('h', "Java3");
linkedMap.put('s', "Java4");
Iterator<Map.Entry<Character, String>> it2 = linkedMap.entrySet().iterator();
while (it2.hasNext()) {
// 按照插入顺序输出结果
Map.Entry<Character, String> entry = it2.next();
System.out.println(entry.getKey()+" "+entry.getValue());
}
}
}
这里需要注意的是:LinkedHashMap遍历速度与实际数据有关和容量无关,因为它通过双向链表遍历,而HashMap遍历速度和其容量有关。
- TreeMap
TreeMap是一个有序的key-value集合,通过红黑树实现的,能够把它保存的记录根据键排序,默认按键值升序排序,或者根据Comparator进行排序,取决于使用的构造方法,非线程安全
默认自然排序示例代码如下
import java.util.*;
public class MapDemo {
public static void main(String[] args) {
HashMap<Character, String> map = new HashMap<>();
map.put('a', "Java1");
map.put('z', "Java2");
map.put('b', "Java5");
map.put('h', "Java3");
map.put('s', "Java4");
Iterator<Map.Entry<Character, String>> it1 = map.entrySet().iterator();
while (it1.hasNext()) {
// 按照hash顺序输出(对比插入顺序是“无序”的)
Map.Entry<Character, String> entry = it1.next();
System.out.println(entry.getKey() + " " + entry.getValue());
}
System.out.println();
TreeMap<Character, String> treeMap = new TreeMap<>();
treeMap.put('a', "Java1");
treeMap.put('z', "Java2");
treeMap.put('b', "Java5");
treeMap.put('h', "Java3");
treeMap.put('s', "Java4");
Iterator<Map.Entry<Character, String>> it2 = treeMap.entrySet().iterator();
while (it2.hasNext()) {
// 按照key值排序
Map.Entry<Character, String> entry = it2.next();
System.out.println(entry.getKey() + " " + entry.getValue());
}
}
}
这里key为什么能够自然排序,其实是Integer类继承了Comparable接口
public final class Integer extends Number implements Comparable<Integer>
如果要排序的key值类型没有实现排序,那么程序就会运行报错,如下图会报错
package Map;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;
public class TreeMapDemo {
static class bird {
int age;
public bird(int age) {
this.age = age;
}
}
public static void main(String[] args) {
TreeMap<bird, Integer> map = new TreeMap<>();
map.put(new bird(1), 1);
map.put(new bird(2), 2);
map.put(new bird(66), 66);
map.put(new bird(8), 8);
Iterator<Map.Entry<bird, Integer>> iterator = map.entrySet().iterator();
//这里key值是bird类,本身是无序的,虽然编译没错,但是TreeMap是有序的
//所以这样运行时会报错的bird cannot be cast to java.lang.Comparable
while (iterator.hasNext()) {
Map.Entry<bird, Integer> entry = iterator.next();
System.out.println(entry.getKey() + " : " + entry.getValue());
}
}
}
Comparable接口排序示例代码如下
package Map;
import java.util.Comparator;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;
public class TreeMapDemo {
static class bird implements Comparable<bird> {
int age;
public bird(int age) {
this.age = age;
}
@Override
public int compareTo(bird b) {
if (this.age > b.age)
return 1;
else if (this.age < b.age)
return -1;
return age;
}
}
public static void main(String[] args) {
TreeMap<bird, Integer> map = new TreeMap<>();
map.put(new bird(1), 1);
map.put(new bird(2), 2);
map.put(new bird(66), 66);
map.put(new bird(8), 8);
Iterator<Map.Entry<bird, Integer>> iterator = map.entrySet().iterator();
//这里key值是bird类,本身是无序的,但实现了Comparable的compareTo方法
while (iterator.hasNext()) {
Map.Entry<bird, Integer> entry = iterator.next();
System.out.println(entry.getValue());
}
}
}
未完待续......
本博客是个人学习总结,参考学习了很多知识(网络&源码&书),如果内容有误或者有资料参考未列举出来,欢迎交流~
---------------------------------------------------------------------------------------------------------------------------------------------------------------------
2019.12.14 更新
// 如果map里没有key为“name”的value值,就test
String name = map.getOrDefault("name", "test")
// 如果map里没有key为“name”的value值,就test 再加test
map.put(name,map.getOrDefault(name,"test")+"test");