目录
一、HashMap简介
(1)HashMap实现了Map接口,存储的是键值对的数据(key-value)。
(2)HashMap的key和value都允许为null,键唯一,值可重复。
(3)存储的数据是无序的。
(4)由数组,链表和红黑树组成。默认初始容量是16,装载因子为0.75。
(5)非同步的。如果需要同步操作,可使用Map m = Collections.synchronizedMap(new HashMap(...));
二、HashMap的构造方法
HashMap有四个构造方法。
HashMap()
: 构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空 HashMap。
Map<String,String> hashMap = new HashMap<String,String>();
HashMap的默认构造函数就是对下面的几个字段进行初始化。
int threshold; // 所能容纳key-value对的极限,默认为12(16 * 0.75),超过它,hashmap就会进行扩容,扩容后容量为原来的两倍。
final float loadFactor; // 负载因子,默认等于0.75。这里说下length,length代表HashMap中数组的长度,threshold = length * loadFactor。
int size; // size代表HashMap中实际存储的key-value对的个数,注意和length的区别。
int modCount; // modCount字段主要用来记录HashMap内部结构发生变化的次数,主要用于迭代的快速失败。强调一点,内部结构发生变化指的是结构发生变化,例如put新键值对,但是某个key对应的value值被覆盖不属于结构变化。
HashMap(int initialCapacity)
: 构造一个带指定初始容量 (initialCapacity) 和默认加载因子 (0.75) 的空 HashMap。
Map<String,String> hashMap = new HashMap<String,String>(32);
-
HashMap(int initialCapacity, float loadFactor)
: 构造一个带指定初始容量和加载因子的空 HashMap。注意:虽然可以传入initialCapacity这个指定的初始容量,但HashMap底层还是会自动给我们将initialCapacity这个数,转变成比它大的那个最小的2的幂次方的数。其中涉及方法
tableSizeFor
,举个例子:如果传入6,那HashMap的初始值为8,如果是14那对应的初始值是16。所以并不是想传入多少他的初始容量就为多少。 -
HashMap(Map<? extends K,? extends V> m)
: 构造一个映射关系与指定 Map 相同的新 HashMap。
三、HashMap的常用方法
1、添加功能
- V put(K key,V value) :
如果键是第一次存储,就直接存储元素,返回null
如果键不是第一次存在,就用值把以前的值替换掉,返回以前的值
2、删除功能
- void clear() :移除所有的键值对元素
- V remove(Object key) :根据键删除键值对元素,并把值返回
3、判断功能
- boolean containsKey(Object key) :判断集合是否包含指定的键
- boolean containsValue(Object value) :判断集合是否包含指定的值
- boolean isEmpty() :判断集合是否为空
4、获取功能
- Set<Map.Entry<K,V>> entrySet() :返回此映射所包含的映射关系的 Set 视图
- V get(Object key) :根据键获取值
- Set keySet() :获取集合中所有键的集合
- Collection values() :获取集合中所有值的集合
5、长度
- int size() :返回集合中的键值对的对数
四、HashMap的遍历
Java中所有的Map都实现了Map接口,那么一下的遍历方法都使用任何Map实现(HashMap、TreeMap、LinkedHashMap等等)
方法1:通过键找值遍历,其中需要用到方法
Set keySet()
V get(Object key)
import java.util.HashMap;
import java.util.Map;
public class HashMapDemo {
public static void main(String[] args) {
Map<Integer, String> map = new HashMap<Integer, String>();
map.put(1,"luffy");
map.put(2,"nami");
map.put(3,"zoro");
for (Integer key : map.keySet()) {
String value = map.get(key);
System.out.println("Key = " + key + ", Value = " + value);
}
}
}
/*
输出结果:
Key = 1, Value = luffy
Key = 2, Value = nami
Key = 3, Value = zoro
*/
方法2:通过entrySet()
方法获取map集合的键值对对象,然后通过for-each及该对象的getKey()
和getValue()
进行遍历
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class HashMapDemo {
public static void main(String[] args) {
Map<Integer, String> map = new HashMap<Integer, String>();
map.put(1,"luffy");
map.put(2,"nami");
map.put(3,"zoro");
Set<Map.Entry<Integer,String>> set = map.entrySet();
for (Map.Entry<Integer,String> me : set) {
// 根据键值对对象获取键和值
Integer key = me.getKey();
String value = me.getValue();
System.out.println("Key = " + key + ", Value = " + value);
}
}
}
方法3:通过Iterator的hasNext()
和next()
方法进行遍历
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
public class HashMapDemo {
public static void main(String[] args) {
Map<Integer, String> map = new HashMap<Integer, String>();
map.put(1,"luffy");
map.put(2,"nami");
map.put(3,"zoro");
Iterator<Map.Entry<Integer, String>> entries = map.entrySet().iterator();
while (entries.hasNext()) {
Map.Entry<Integer, String> entry = entries.next();
// 这里同样需要借助getKey()和getValue()方法
System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue());
}
}
}
方法4:通过Map的keySet()和values()直接对它的键或值遍历
import java.util.HashMap;
import java.util.Map;
public class HashMapDemo {
public static void main(String[] args) {
Map<Integer, String> map = new HashMap<Integer, String>();
map.put(1,"luffy");
map.put(2,"nami");
map.put(3,"zoro");
//遍历map中的键
for (Integer key : map.keySet()) {
System.out.println("Key = " + key);
}
//遍历map中的值
for (String value : map.values()) {
System.out.println("Value = " + value);
}
}
}
/*
输出结果:
Key = 1
Key = 2
Key = 3
Value = luffy
Value = nami
Value = zoro
*/
五、HashMap获取哈希桶数组索引
不管增加、删除、查找键值对,定位到哈希桶数组的位置都是很关键的第一步。而HashMap的底层是由数组+链表+红黑树(1.8以后新加了红黑树)构成,对于数组的索引,也就是哈希桶数组的索引,HashMap里的源码是这样实现计算的:
方法一:
static final int hash(Object key) { //jdk1.8 & jdk1.7
int h;
// h = key.hashCode() 为第一步 取hashCode值
// h ^ (h >>> 16) 为第二步 高位参与运算
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
方法二:
static int indexFor(int h, int length) { //jdk1.7的源码,jdk1.8没有这个方法,但是实现原理一样的
return h & (length-1); //第三步 取模运算
}
也就是根据key,采用hash算法对key进行哈希计算,从而获得一个int型的哈希桶索引,一共有三步:取key的hashCode值,对hashCode值的高低16位进行异或,hashCode值与length-1取模计算下标。
六、HashMap的put方法
JDK1.8HashMap的put方法源码如下:
public V put(K key, V value) {
// 对key的hashCode()做hash
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;
// 步骤①:tab为空则创建
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 步骤②:计算index,并对null做处理
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 步骤③:节点key存在,直接覆盖value
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 步骤④:判断该链为红黑树
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 步骤⑤:该链为链表
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key,value,null);
//链表长度大于8转换为红黑树进行处理
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// key已经存在直接覆盖value
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
48 ++modCount;
49 // 步骤⑥:超过最大容量 就扩容
50 if (++size > threshold)
51 resize();
52 afterNodeInsertion(evict);
53 return null;
54 }
注意几点:
① 1.7之前,HashMap在执行put方法时,如果发生了哈希冲突,则在哈希桶对应的链表采用头插法进行添加元素;而1.8之后采用的是尾插法,目的是为了保证hashmap扩容之后,相同链表上的元素还在一起。
② 1.8之后,当链表的长度大于TREEIFY_THRESHOLD (8)时,是会自动将链表转成红黑树的,这是一个优化,在红黑树上对数据进行查询等操作会比在链表上更高效。
七、HashMap的扩容机制
HashMap的扩容涉及到几个值
- size:HashMap中实际存储的键值对对数
- length:HashMap的capacity,也就是其目前规定的总容量,默认为16
- threshold:扩容的临界值,它的计算为 => threshold = length * loadFactor,所以默认为12(16*0.75)
- loadFactor: 负载因子,默认为0.75
HashMap的扩容机制是这样实现的,当size大于threshold时,就会进行扩容,1.8之后通过数组拷贝、重新计算哈希桶索引的方法将HashMap扩充为原来的2倍。默认情况下,当size大于12时进行扩容。