Map接口
基本概念
Map有键和值的概念,一个键映射到一个值,Map按照键存储和访问值,键不能重复,即一个键只会存储一份,给同一个键重复设值会覆盖原来的值。使用Map可以方便地处理需要根据键访问对象的场景,比如:
- 一个词典应用,键可以为单词,值可以为单词信息类,包括含义、发音、例句等。
- 统计和记录一本书中所有单词出现的次数,可以以单词为键,出现次数为值。
- 管理配置文件中的配置项,配置项是典型的键值对。
- 根据身份证号查询人员信息,身份证号为键,人员信息为值。
数组、ArrayList、LinkedList可以视为一种特殊的Map,键为索引,值为对象。
接口定义
Map接口的定义为:
public interface Map {
V put(K key, V value);//保存键值对
V get(Object key);
V remove(Object key);
int size();
boolean isEmpty();
boolean containsKey(Object key);
boolean containsValue(Object value);
void putAll(Map extends K, ? extends V> m);//批量保存,保存参数m中的所有键值对到当前Map
void clear();
Set keySet();//获取Map中键的集合,Set没有重复的元素集合
Collection values();//获取Map中所有值的集合,可以重复
Set> entrySet();//获取Map中的所有键值对
interface Entry {
K getKey();
V getValue();
V setValue(V value);
boolean equals(Object o);
int hashCode();
}
boolean equals(Object o);
int hashCode();
}
HashMap
构造方法
除了默认构造方法,HashMap还有如下构造方法:
public HashMap(int initialCapacity)
public HashMap(int initialCapacity, float loadFactor)
public HashMap(Map extends K, ? extends V> m)
实现原理
内部组成
HashMap内部有如下几个主要的实例变量:
//被transient关键字修饰的变量不会被序列化
transient Entry[] table = (Entry[]) EMPTY_TABLE;//一个Entry类型的数组,其中的每个元素指向一个单向链表,链表中的每个节点表示一个键值对,Entry是一个内部类
transient int size;//实际键值对的个数
int threshold;//阈值,当键值对个数size大于等于threshold时考虑进行扩展
final float loadFactor;//负载因子,表示整体上table被占用的程度,是一个浮点数,默认为0.75,可以通过构造方法进行修改
static class Entry implements Map.Entry {
final K key;
V value;
Entry next//指向下一个Entry节点
int hash;//key的哈希值,直接存储hash值是为了在比较的时候加快计算
Entry(int h, K k, V v, Entry n) {
value = v;
next = n;
key = k;
hash = h;
}
}
table的初始值为EMPTY_TABLE,是一个空表,具体定义为:
static final Entry,?>[] EMPTY_TABLE = {};
当添加键值对后,table就不是空表了,它会随着键值对的添加进行扩展,扩展的策略类似于ArrayList,添加第一个元素时,默认分配的大小为16,不过,并不是size大于16时再进行扩展,下次什么时候扩展与threshold有关。一般而言,threshold等于table.length乘以loadFactor,比如,如果table.length为16,loadFactor为0.75,则threshold为12。
默认构造方法
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);//(16,0.75)
}
public HashMap(int initialCapacity, float loadFactor) {
this.loadFactor = loadFactor;
threshold = initialCapacity;
}
保存键值对
下面,我们来看HashMap是如何把一个键值对保存起来的,代码为:
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);//计算key的哈希值
int i = indexFor(hash, table.length);//计算应该将这个键值对放到table的哪个位置
for (Entry e = table[i]; e != null; e = e.next) {//保存位置i,table[i]指向一个单向链表,在这个链表中逐个查找是否已经有这个键了
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;//如果找到了已经存储的key,直接修改后返回
}
}
modCount++;//记录修改次数,方便在迭代中检测结构性变化
addEntry(hash, key, value, i);//未存储过,则新增
return null;
}
//如果是第一次保存,首先会调用inflateTable()方法给table分配实际的空间
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize
int capacity = roundUpToPowerOf2(toSize);
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
//默认情况下,capacity的值为16,threshold会变为12,table会分配一个长度为16的Entry数组
}
static int indexFor(int h, int length) {
return h & (length-1);
//HashMap中,length为2的幂次方,h&(length-1)等同于求模运算:h%length
}
//在给定的位置添加一条
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;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);//如果空间是够的,不需要resize,调用createEntry添加一条数据
}
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);//这里实际上是相当于添加到了链表头
size++;
}
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
Entry[] newTable = new Entry[newCapacity];
transfer(newTable, initHashSeedAsNeeded(newCapacity));//将原来的键值对移植过来
table = newTable;//更新内部的table变量
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
//遍历原来的每个键值对,计算新位置,并保存到新位置
void transfer(Entry[] newTable, boolean rehash) {//rehash一般为false
int newCapacity = newTable.length;
for (Entry e : table) {
while(null != e) {
Entry next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
以上,就是保存键值对的主要代码,简单总结一下,基本步骤为:
- 计算键的哈希值
- 根据哈希值得到保存位置(取模)
- 插到对应位置的链表头部或更新已有值
- 根据需要扩展table大小
图示说明
以上描述可能比较抽象,我们通过一个例子,用图示的方式说明,代码是:
Map countMap = new HashMap<>();
countMap.put("hello", 1);
countMap.put("world", 3);
countMap.put("position", 4);
在通过new HashMap()创建一个对象后,内存中的图示结构大概是:
接下来执行countMap.put("hello", 1);
"hello"的hash值为96207088,模16的结果为0,所以插入table[0]指向的链表头部,内存结构会变为:
"world"的hash值为111207038,模16结果为15,所以保存完"world"后,内存结构会变为:
"position"的hash值为771782464,模16结果也为0,table[0]已经有节点了,新节点会插到链表头部,内存结构会变为:
实现原理小结
以上就是HashMap的基本实现原理,内部有一个数组table,每个元素table[i]指向一个单向链表,根据键存取值,用键算出hash,取模得到数组中的索引位置buketIndex
,然后操作table[buketIndex]指向的单向链表。
存取的时候依据键的hash值,只在对应的链表中操作,不会访问别的链表,在对应链表操作时也是先比较hash值,相同的话才用equals方法比较,这就要求,相同的对象其hashCode()返回值必须相同,如果键是自定义的类,就特别需要注意这一点。这也是hashCode和equals方法的一个关键约束。
HashMap特点分析
HashMap实现了Map接口,内部使用数组链表和哈希的方式进行实现,这决定了它有如下特点:
- 根据键保存和获取值的效率都很高,为O(1),每个单向链表往往只有一个或少数几个节点,根据hash值就可以直接快速定位。
- HashMap中的键值对没有顺序,因为hash值是随机的。
如果经常需要根据键存取值,而且不要求顺序,那HashMap就是理想的选择。
不过HashMap没有顺序,如果要保持添加的顺序,可以使用HashMap的一个子类LinkedHashMap,Map还有一个重要的实现类TreeMap,它可以排序。
ref:Java编程的逻辑 (40) - 剖析HashMap