1.HashMap
HashMap是一个散列表,它存储的内容是键值对(key-value)映射。HashMap底层是基于数组+链表组成的,不过在jdk1.7和jdk1.8中具体实现稍有不同。
①HashMap根据键的HashCode值存储数据,具有很快的访问速度,最多只允许一条记录的键为null,允许多条记录的值为null。
②HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用Collection的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap。
③HashMap是无序的,即不会记录插入的顺序,插入的顺序和迭代输出的数组不一样。
④HashMap如果插入相同的Key,则后面的value将会覆盖前面的value。
⑤HashMap和HashTable的区别:HashMap允许空(null)键或值,而Hashtable不允许空键或值。
⑥HashMap的key与value类型可以相同也可以不同,可以是String类型的key和value,也可以是Integer的key和String类型的value。
HashMap中的元素实际上是对象,一些常见的基本类型可以使用它的包装类。基本类型对应的包装类表如下:
基本类型 | 引用类型 |
---|---|
boolean | Boolean |
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
char | Character |
2.HashMap的用法
重点看HashMap的迭代。可以使用for-each来迭代HashMap中的元素。
①如果只想获取key,可以使用keySet()方法,然后可以通过get(key)获取对应的value;如果只想获取value,可以使用values()方法。
HashMap<Integer, String> sites = new HashMap<Integer, String>();
sites.put(1, "Google");
sites.put(6, "Runoob");
// 输出key和value
for (Integer i : sites.keySet()) {
System.out.println("key: " + i + " value: " + sites.get(i));
}
// 返回所有value值
for(String value: sites.values()) {
System.out.print(value + ", ");
}
输出结果如下:
key: 1 value: Google
key: 6 value: Runoob
Google, Runoob,
HashMap 常用方法列表如下:
方法 | 描述 |
---|---|
clear() | 删除hashMap中的所有键/值对 |
clone() | 复制一份hashMap |
isEmpty() | 判断hashMap是否为空 |
size() | 计算hashMap中键/值对的数量 |
put() | 将键/值对添加到hashMap中 |
putAll() | 将所有键/值对添加到hashMap中 |
putIfAbsent() | 如果hashMap中不存在指定的键,则将指定的键/值对插入到hashMap中。 |
remove() | 删除hashMap中指定键key的映射关系 |
containsKey() | 检查hashMap中是否存在指定的key对应的映射关系。 |
containsValue() | 检查hashMap中是否存在指定的value对应的映射关系。 |
replace() | 替换hashMap中是指定的key对应的value。 |
replaceAll() | 将hashMap中的所有映射关系替换成给定的函数所执行的结果。 |
get() | 获取指定key对应对value |
getOrDefault() | 获取指定key对应的value,如果找不到key ,则返回设置的默认值 |
forEach() | 对hashMap中的每个映射执行指定的操作。 |
entrySet() | 返回hashMap中所有映射项的集合集合视图。 |
keySet() | 返回hashMap中所有key组成的集合视图。 |
values() | 返回hashMap中存在的所有 value 值。 |
merge() | 添加键值对到hashMap中 |
compute() | 对hashMap中指定key的值进行重新计算 |
computeIfAbsent() | 对hashMap中指定key的值进行重新计算,如果不存在这个key,则添加到hasMap中 |
computeIfPresent() | 对hashMap中指定key的值进行重新计算,前提是该key存在于hashMap中。 |
3.HashMap原理
HashMap实现了Map接口,继承AbstractMap。其中Map接口定义了键映射到值的规则,而AbstractMap类提供Map接口的骨干实现,以最大限度地减少实现此接口所需的工作,其实AbstractMap类已经实现了Map。
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
(1)构造函数
HashMap提供了三个构造函数:
①HashMap():构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空HashMap。
②HashMap(int initialCapacity):构造一个指定初始容量和默认加载因子 (0.75) 的空HashMap。
③HashMap(int initialCapacity, float loadFactor):构造一个指定初始容量和指定加载因子的空HashMap。
在这里提到了两个参数:初始容量、加载因子。这两个参数是影响HashMap性能的重要参数,其中容量表示哈希表中桶的数量,初始容量是创建哈希表时的容量;加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度,它衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。对于使用链表法的散列表来说,查找一个元素的平均时间是O(1+a),因此如果负载因子越大,对空间的利用更充分,然而后果是查找效率的降低;如果负载因子太小,那么散列表的数据将过于稀疏,对空间造成严重浪费。系统默认负载因子为0.75,一般情况下是无需修改的。
可能有人要问:为什么要加载因子?这里就涉及到HashMap的原理了。HashMap中存储元素的时候,首先得先通过它自己的hash算法找到存储在table数组的索引值。但是这个hash算法并不能保证每一个元素对应table数组中不同的索引值,当放入HashMap的元素过多时就容易出现相同的索引值,在算法里叫冲突,这时元素就会被加到该索引值下的链表中,这样查找的效率就会大大降低,显然违背了HashMap快速查找的初衷。所以HashMap在设计的时候就用了这样一个加载因子,如果存储的元素个数占table长度的比例大于loadFactor加载因子的时候,冲突加剧,这时就得扩容解决问题了。
所以总结影响HashMap效率的两个因素:①初始容量 ②加载因子。解决的本质就是减少hash冲突。
(2)数据结构
HashMap是一种支持快速存取的数据结构,要了解它的性能必须要了解它的数据结构。
在Java中最常用的两种结构是数组和模拟指针(引用),几乎所有的数据结构都可以利用这两种来组合实现,HashMap也是如此。实际上HashMap是一个“链表散列”,以jdk1.7为例看一下它数据结构:
HashMap底层实现还是数组,只是数组的每一项都是一条链。参数initialCapacity就代表了该数组的长度。
HashMap构造函数的源码:
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0) //初始容量不能<0
throw new IllegalArgumentException( "Illegal initial capacity: " + initialCapacity);
//初始容量不能 > 最大容量值,则设置最大容量值为2^30
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//负载因子不能 < 0
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException( "Illegal load factor: " + loadFactor);
// 计算出大于initialCapacity的最小的2的n次方值
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
this.loadFactor = loadFactor;
//设置HashMap的容量极限,当HashMap的容量达到该极限时就会进行扩容操作
threshold = (int) (capacity * loadFactor);
//初始化table数组
table = new Entry[capacity];
init();
}
每次新建一个HashMap时都会初始化一个table数组。table数组的元素为Entry节点。
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
final int hash;
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
.......
}
Entry为HashMap的内部类,它包含了键key、值value、下一个节点next(用于实现链表结构),以及hash值(当前key的hashcode),这是非常重要的,正是由于Entry才构成了table数组的项为链表。
(3)存储实现:put(key,vlaue)
下面将探讨HashMap是如何实现快速存取的。
public V put(K key, V value) {
//key为null时,调用putForNullKey方法,保存null于table第一个位置中,这是HashMap允许为null的原因
if (key == null)
return putForNullKey(value);
//计算key的hash值
int hash = hash(key.hashCode()); ------(1)
//根据key的hash值计算在table数组中的位置
int i = indexFor(hash, table.length); ------(2)
//迭代table[i]位置的链表,找到key保存的位置
for (Entry<K, V> e = table[i]; e != null; e = e.next){
Object k;
//判断该条链上是否有key相同的,若存在相同则直接覆盖value,返回旧value
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue; //返回旧值
}
}
modCount++; //修改次数增加1
//table[i]位置的链表不存在相同的key,则直接插入
addEntry(hash, key, value, i);
return null;
}
HashMap保存数据的过程为:首先判断key是否为null,若为null则直接调用putForNullKey方法。若不为null则先计算key的hash值,然后根据hash值搜索在table数组中的索引位置。如果table数组在该位置处有元素,则通过比较是否存在相同的key,若存在则覆盖原来key的value,否则将该元素保存在链头(最先保存的元素放在链尾)。若table在该处没有元素,则直接保存。这个过程看似比较简单,其实深有内幕。有如下几点:
①先看迭代处。此处迭代原因是为了防止存在相同的key值,若发现两个hash值(key)相同时,HashMap的处理方式是用新value替换旧value,这里并没有处理key,这就解释了HashMap中没有两个相同的key。
②再看(1)、(2)处。首先是hash方法,该方法为一个纯粹的数学计算,就是计算h的hash值。
static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
对于HashMap的table而言,数据分布需要均匀(最好每项都只有一个元素,这样就可以直接找到),不能太紧也不能太松,太紧会导致查询速度慢,太松则浪费空间。计算hash值后,怎么才能保证table元素分布均与呢?首先会想到取模,但是由于取模的消耗较大,HashMap是这样处理的:调用indexFor方法。
static int indexFor(int h, int length) {
return h & (length-1);
}
HashMap的底层数组table长度总是2的n次方,在构造函数中存在:capacity <<= 1;这样做总是能够保证HashMap的底层数组长度为2的n次方。当length为2的n次方时,h&(length - 1)就相当于对length取模,而且速度比直接取模快得多,这是HashMap在速度上的一个优化。
回到indexFor方法,该方法仅有一条语句:h&(length - 1),这句话除了上面的取模运算外,还有一个非常重要的责任:均匀分布table数据和充分利用空间。
假设length为16(2^n)和15,h为5、6、7。
当n=15时,6和7的结果一样,表示他们在table存储的位置是相同的,也就是产生了碰撞,6、7就会在一个位置形成链表,这样就会导致查询速度降低。诚然这里只分析三个数字不是很多,那再看0-15。
看到总共发生了8次碰撞,同时发现浪费的空间非常大,有1、3、5、7、9、11、13、15处没有记录,也就是没有存放数据。这是因为他们在与14进行&运算时,得到的结果最后一位永远都是0,即0001、0011、0101、0111、1001、1011、1101、1111位置处是不可能存储数据的,空间减少,进一步增加碰撞几率,这样就会导致查询速度慢。而当length = 16时,length – 1 = 15 即1111,那么进行低位&运算时,值总是与原来hash值相同,而进行高位运算时,其值等于其低位值。所以说当length = 2^n时,不同的hash值发生碰撞的概率比较小,这样就会使得数据在table数组中分布较均匀,查询速度也较快。
现在再来看put的流程:向HashMap中添加一对key-value时,系统首先会计算key的hash值,然后根据hash值确认在table中存储的位置。若该位置没有元素则直接插入,否则迭代该处元素链表并依此比较其key的hash值。如果两个hash值相等且key值相等(e.hash == hash && ((k = e.key) == key || key.equals(k))),则用新的Entry的value覆盖原来节点的value。如果两个hash值相等但key值不等 ,则将该节点插入该链表的链头。具体的实现过程见addEntry方法:
void addEntry(int hash, K key, V value, int bucketIndex) {
//获取bucketIndex处的Entry
Entry<K, V> e = table[bucketIndex];
//将新创建的Entry放入bucketIndex索引处,并让新的Entry指向原来的 Entry (也就是将新元素加到该元素对应table[bucketIndex]链表的表头)
table[bucketIndex] = new Entry<K, V>(hash, key, value, e);
//若HashMap中元素的个数超过极限了,则容量扩大两倍
if (size++ >= threshold)
resize(2 * table.length);
}
这个方法中有两点需要注意:
①一是链的产生。
这是一个非常优雅的设计。系统总是将新的Entry对象添加到bucketIndex处。如果bucketIndex处已经有了对象,那么新添加的Entry对象将指向原有的Entry对象,形成一条Entry链;但是若bucketIndex处没有Entry对象,也就是e==null,那么新添加的Entry对象指向null,也就不会产生Entry链了。
②扩容问题。
随着HashMap中元素的数量越来越多,发生碰撞的概率就越来越大,所产生的链表长度就会越来越长,这样势必会影响HashMap的速度,为了保证HashMap的效率,系统必须要在某个临界点进行扩容处理。该临界点是HashMap中元素的数量等于table数组长度*加载因子。
注意:扩容是一个非常耗时的过程,因为它需要重新计算这些数据在新table数组中的位置并进行复制处理。所以如果已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。
(4)扩容
在元素添加方法addEntry()中,添加完元素后,有下面两行代码:
if (size++ >= threshold)
resize(2 * table.length);
size表示的是HashMap中有多少个元素,当元素的个数超过临界值时会自动调用扩容方法,可以看出HashMap的扩容是翻番的扩2 * table.length。现在来看看resize扩容方法。
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
transfer(newTable);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
前面几行是判断扩容后是否过了最大的int值。后面几行是将原来的table中的元素重新hash放到新的扩容后的table中。重点在transfer(newTable)这个方法。
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 {
//将第一个元素e后的链表截取出来
Entry<K,V> next = e.next;
//找到e对应新table的下标索引
int i = indexFor(e.hash, newCapacity);
//将e插入到新table i下标索引链表的表头
e.next = newTable[i];
//将新table下标索引重新定位为e,这样就完成了一个元素的重新hash
newTable[i] = e;
//将截取的剩余的链表继续hash
e = next;
} while (e != null);
}
}
}
这个方法的主要作用就是,将老的table中的所有不为空的元素,重新hash放到新的table中去。在do之前就是遍历table中不为空的元素。这时候找出来的e = src[j]是一个Entry链表。所以,如果不为空,还要遍历这个链表中的每一个元素,并将这些元素重新hash到新table中。
示意图如下:
①Entry<K,V> next = e.next;
②e.next = newTable[i];
即这里的e就是Entry[j],也就是
③newTable[i] = e;
因为newTable[i]本身是一个指向浅蓝色Entry[i]的引用,这个时候再将这个引用指向红色Entry[j],这样就完成了老table中一个元素的重新hash到新table中。
(5)key为null,存到哪去了
在put方法里头,第一行就处理了key=null的情况:
if (key == null)
return putForNullKey(value);
那就看看这个putForNullKey是怎么处理的吧。
private V putForNullKey(V value) {
for (Entry<K,V> e = table[0]; e != null; e = e.next){
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0);
return null;
}
前面那个for循环是在talbe[0]链表中查找key为null的元素,如果找到就将value重新赋值给这个元素的value并返回原来的value。如果上面for循环没找到,则将这个元素添加到talbe[0]链表的表头。
(6)读取实现:get(key)
相对于HashMap的存而言,取就比较简单了。通过key的hash值找到在table数组中的索引处的Entry,然后返回该key对应的value即可。
public V get(Object key) {
// 若为null,调用getForNullKey方法返回相对应的value
if (key == null)
return getForNullKey();
// 根据key的hashCode值计算它的hash码
int hash = hash(key.hashCode());
// 取出table数组中指定索引处的值
for (Entry<K, V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
Object k;
//若搜索的key与查找的key相同,则返回相对应的value
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;
}
首先是找key为null的元素,通过putForNullKeykey方法可知,为null的元素是放在table[0]这个链表的。所以要找的话,直接到table[0]中查找就行了。如果没找到的话,则根据key的hash值找到元素所在table中下标索引,根据其再找到元素所在链表,在遍历链表,找到该元素并返回其value,否则返回null。
在这里能够根据key快速的取到value,除了和HashMap的数据结构密不可分外,还和Entry有很大的关系:HashMap在存储过程中并没有将key和value分开来存储,而是当做一个整体,这个整体就是Entry对象。同时value也只相当于key的附属而已。在存储的过程中,系统根据key的hashcode来决定Entry在table数组中的存储位置,在取的过程中同样根据key的hashcode取出相对应的Entry对象。
(7)删除元素
public V remove(Object key) {
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.value);
}
调用的还是下面的方法 :
final Entry<K,V> removeEntryForKey(Object key){
int hash = (key == null) ? 0 : hash(key.hashCode());
int i = indexFor(hash, table.length);
Entry<K,V> prev = table[i];
Entry<K,V> e = prev;
while (e != null) {
Entry<K,V> next = e.next;
Object k;
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
modCount++;
size--;
if (prev == e)
table[i] = next;
else
prev.next = next;
e.recordRemoval(this);
return e;
}
prev = e;
e = next;
}
return e;
}
while循环外面的很简单,来看看while循环里的。
Entry<K,V> next = e.next;把原有的链表截出表头元素,然后判断这个表头元素的key是不是要找的key。如果找出的第一个元素就是的话,直接将这个链表的第一个元素删除就OK。
if (prev == e)
table[i] = next;
如果不是,则遍历这个链表,下图展示了这个过程:
步骤1、初始情况
Entry<K,V> prev = table[i];
Entry<K,V> e = prev;
步骤2、没找到
Entry<K,V> next = e.next;
……..
prev = e;
e = next;
如果e这个元素不是要删除的话,则遍历下一个元素。
步骤3、找到
prev.next = next;
return e;
将prev的下一个元素指向e.next。这样就相当于删除了e
最后的结果如下:
4.线程安全性
在多线程使用场景中,应尽量避免使用线程不安全的HashMap,而使用线程安全的ConcurrentHashMap。为什么说HashMap是线程不安全的,下面举例说明在并发的多线程使用场景中使用HashMap可能造成死循环。
代码例子如下(JDK1.7的环境):
public class HashMapInfiniteLoop {
private static HashMap<Integer,String> map = new HashMap<Integer,String>(2,0.75f);
public static void main(String[] args) {
map.put(5, "C");
new Thread("Thread1") {
public void run() {
map.put(7, "B");
System.out.println(map);
};
}.start();
new Thread("Thread2") {
public void run() {
map.put(3, "A);
System.out.println(map);
};
}.start();
}
}
map初始化为一个长度为2的数组,负载因子loadFactor为0.75,threshold=2*0.75=1,也就是说当put第二个key的时候,map就需要进行resize。
通过设置断点让线程1和线程2同时debug到transfer方法的首行。注意此时两个线程已经成功添加数据。放开thread1的断点至transfer方法的“Entry next = e.next;” 这一行;然后放开线程2的的断点,让线程2进行resize。结果如下图:
注意,Thread1的 e 指向了key(3),而next指向了key(7),其在线程2rehash后,指向了线程2重组后的链表。
线程一被调度回来执行,先是执行 newTalbe[i] = e, 然后是e = next,导致了e指向了key(7),而下一次循环的next = e.next导致了next指向了key(3)。
e.next = newTable[i] 导致 key(3).next 指向了 key(7)。注意:此时的key(7).next 已经指向了key(3), 环形链表就这样出现了。
于是,当用线程一调用map.get(11)时,悲剧就出现了——Infinite Loop。
5.jdk1.7和1.8的区别
jdk1.7 中hashMap的数据结构图:
jdk 1.7 的实现中有一个很明显需要优化的地方就是:当Hash冲突严重时,在桶上形成的链表会变的越来越长,这样在查询时的效率就会越来越低;时间复杂度为 O(N)。
因此1.8中重点优化了这个查询效率。
1.8 HashMap 结构图:
先来看看几个核心的成员变量:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
static final int TREEIFY_THRESHOLD = 8;
transient Node<K,V>[] table;
transient Set<Map.Entry<K,V>> entrySet;
transient int size;
和1.7大体上都差不多,有几个重要的区别:
①TREEIFY_THRESHOLD 用于判断是否需要将链表转换为红黑树的阈值。
②HashEntry修改为Node。
Node的核心组成其实也是和1.7中的HashEntry一样,存放的都是key、value、hashcode、next等数据。
与TreeNode相关的三个参数: TREEIFY_THRESHOLD=8 和 UNTREEIFY_THRESHOLD=6 以及 MIN_TREEIFY_CAPACITY=64
TREEIFY_THRESHOLD=8 指的是链表的长度大于8的时候进行树化。
UNTREEIFY_THRESHOLD=6 指的是当元素被删除,链表的长度小于6 的时候进行退化,由红黑树退化成链表。
MIN_TREEIFY_CAPACITY=64 意思是数组中元素的个数必须大于等于64之后才能进行树化。
再来看看核心方法。
(1)put 方法
①判断当前桶是否为空,空的就需要初始化(resize 中会判断是否进行初始化)。
②根据当前 key 的 hashcode 定位到具体的桶中并判断是否为空,为空表明没有 Hash 冲突就直接在当前位置创建一个新桶即可。
③如果当前桶有值( Hash 冲突),那么就要比较当前桶中的 key、key 的 hashcode 与写入的 key 是否相等,相等就赋值给 e,在第 8 步的时候会统一进行赋值及返回。
④如果当前桶为红黑树,那就要按照红黑树的方式写入数据。
⑤如果是个链表,就需要将当前的 key、value 封装成一个新节点写入到当前桶的后面(形成链表)。
⑥接着判断当前链表的大小是否大于预设的阈值,大于时就要转换为红黑树。
⑦如果在遍历过程中找到 key 相同时直接退出遍历。
⑧如果 e != null 就相当于存在相同的 key,那就需要将值覆盖。
⑨最后判断是否需要进行扩容。
(2)get 方法
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;
if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && ((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;
}
①首先将 key hash 之后取得所定位的桶。
②如果桶为空则直接返回 null 。否则判断桶的第一个位置(有可能是链表、红黑树)的 key 是否为查询的 key,是就直接返回 value。
③如果第一个不匹配,则判断它的下一个是红黑树还是链表。红黑树就按照树的查找方式返回值,不然就按照链表的方式遍历匹配返回值。
从get/put两个核心方法可以看出 1.8 中对链表做了优化,修改为红黑树之后查询效率直接提高到了 O(logn)。
但是 HashMap 原有的问题也都存在,比如在并发场景下使用时容易出现死循环。
final HashMap<String, String> map = new HashMap<String, String>();
for (int i = 0; i < 1000; i++) {
new Thread(new Runnable() {
@Override
public void run() {
map.put(UUID.randomUUID().toString(), "");
}
}).start();
}
还有一个值得注意的是 HashMap 的遍历方式,通常有以下几种:
Iterator<Map.Entry<String, Integer>> entryIterator = map.entrySet().iterator();
while (entryIterator.hasNext()) {
Map.Entry<String, Integer> next = entryIterator.next();
System.out.println("key=" + next.getKey() + " value=" + next.getValue());
}
Iterator<String> iterator = map.keySet().iterator();
while (iterator.hasNext()){
String key = iterator.next();
System.out.println("key=" + key + " value=" + map.get(key));
}
强烈建议使用第一种 EntrySet 进行遍历。
第一种可以把 key value 同时取出,第二种还得需要通过 key 取一次 value,效率较低。
无论是 1.7 还是 1.8 其实都能看出 JDK 没有对它做任何的同步操作,所以并发会出问题,甚至 1.7 中出现死循环导致系统不可用(1.8 已经修复死循环问题)。
因此 JDK 推出了专项专用的 ConcurrentHashMap ,该类位于 java.util.concurrent 包下,专门用于解决并发问题。
6.各种Map映射
Java为数据结构中的映射定义了一个接口java.util.Map,此接口主要有四个常用的实现类,分别是HashMap、Hashtable、LinkedHashMap和TreeMap,类继承关系如下图所示:
下面针对各个实现类的特点做一些说明:
①HashMap:根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashMap最多只允许一条记录的键为null,允许多条记录的值为null。HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap。
②Hashtable:Hashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类,并且是线程安全的,任一时间只有一个线程能写Hashtable,并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。Hashtable不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换。
③LinkedHashMap:LinkedHashMap是HashMap的一个子类,保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。
④TreeMap:TreeMap实现SortedMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。如果使用排序的映射,建议使用TreeMap。在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,否则会在运行时抛出java.lang.ClassCastException类型的异常。
对于上述四种Map类型的类,要求映射中的key是不可变对象。不可变对象是该对象在创建后它的哈希值不会被改变。如果对象的哈希值发生变化,Map对象很可能就定位不到映射的位置了。