一、 HashMap概述:
Map 集合类用于存储元素对(称作“键”和“值”),其中每个键映射到一个值。
二、 HashMap内部存储
使用实现Map接口的内部类存储元素,Map接口定义如下:
interface Entry<K,V> {
K getKey();
V getValue();
V setValue(V value);
boolean equals(Object o);
int hashCode();
}
内部类如下:
static class Entry<K,V> implements Map.Entry<K,V> {
final K key; // 本元素键引用
V value; // 本元素值引用
Entry<K,V> next; // 指向相同hash值的下一个元素
int hash; // 将通过key对象hashCode()方法获取的哈希值传入HashMap类 // 中的final int hash(Object k)方法获取的值
……
}
一个简单的HashMap对象的内部存储图如下,对于Entry e元素,存放在index数组的第e.hash & (index.length-1)个位置的链表中。使用hash()函数重新计算得出的哈希值是为了减少碰撞冲突的发生。
三、 HashMap重要API
initial capacity 初始容量,新建该HashMap对象使用的数组大小,
load factor 装载因子,用来度量该HashMap对象使用的数组所能容纳的元素个数:当尝试添加一个元素之前,如果发现当前已存储元素个数为index.length*load factor,则先新建一个容量为index.length*2的数组,并把原来数组中的元素重新计算索引值,放到对应的数组链表项中。然后再添加该元素。
构造函数:
public HashMap(int initialCapacity, float loadFactor)
public HashMap(int initialCapacity)
public HashMap()
// 初始时新建存储原来Map接口对应的类对象的键和值的Entry元素添加到该
// HashMap对象
public HashMap(Map<? extends K, ? extends V> m)
// 获取该HashMap对象指定键对应的key,如果不存在则返回null。但是当返回null时候,不表示不存在对应的Entry元素,也可能该元素存储的时候value就为空。查找过程如下:
首先将通过key对象hashCode()方法获取的哈希值传入HashMap类中的final int hash(Object k)方法获取一个哈希值hash,在index数组的第hash & (index.length-1)个位置的链表中查找某个Entry e元素,并满足(k = e.key) == key || (key != null && key.equals(k)),即引用同一个元素,或者通过equals方法判断相等。
public V get(Object key)
// 判断该HashMap对象中是否包含key指定的项
public boolean containsKey(Object key) {
return getEntry(key) != null;
}
// 如果该HashMap对象中存在key对应的Entry e,则把e的值设为value;否则新建一个Entry并添加。同样,尝试添加一个元素之前,如果发现当前已存储元素个数为index.length*load factor,则先新建一个容量为index.length*2的数组,并把原来数组中的元素重新计算索引值,放到对应的数组链表项中。然后再添加该元素。
public V put(K key, V value)
// 删除该HashMap对象中key对应的Entry e并返回e;如果不存在对应的e则返回空。该操作不会改变内部存储的数组大小。
public V remove(Object key)
四、 tips
1. 键的不变性
l 你向HashMap中插入一个对象,它的键就是“1”。HashMap从键(即“1”)的散列码中生成哈希值。Map在新创建的记录中存储这个哈希值。
l 你改动键的内部值,将其变为“2”。键的哈希值发生了改变,但是HashMap并不知道这一点(因为存储的是旧的哈希值)。
l 你试着通过修改后的键获取相应的对象。Map会计算新的键(即“2”)的哈希值,从而找到Entry对象所在的链表(桶)。既然你已经修改了键,Map会试着在错误的桶中寻找Entry对象,很可能找不到。
2. 非线性安全:可通过将HashMap方法操作放入synchronized块中实现线性安全。
3. 性能问题
initial capacity 初始容量
load factor 装载因子
如果你需要存储大量数据,你应该在创建HashMap时指定一个初始的容量,这个容量应该接近你期望的大小。
如果你不这样做,Map会使用默认的大小,即16,factorLoad的值是0.75。前11次调用put()方法会非常快,但是第12次(16*0.75)调用时会创建一个新的长度为32的内部数组(以及对应的链表/树),第13次到第22次调用put()方法会很快,但是第23次(32*0.75)调用时会重新创建(再一次)一个新的内部数组,数组的长度翻倍。然后内部调整大小的操作会在第48次、96次、192次…..调用put()方法时触发。如果数据量不大,重建内部数组的操作会很快,但是数据量很大时,花费的时间可能会从秒级到分钟级。通过初始化时指定Map期望的大小,你可以避免调整大小操作带来的消耗。
但这里也有一个缺点:如果你将数组设置的非常大,例如2^28,但你只是用了数组中的2^26个桶,那么你将会浪费大量的内存(在这个示例中大约是2^30字节)。
key对象哈希函数hashCode()
在最好的情况下,get()和put()方法都只有O(1)的复杂度。但是,如果你不去关心键的哈希函数,那么你的put()和get()方法可能会执行非常慢。put()和get()方法的高效执行,取决于数据被分配到内部数组(桶)的不同的索引上。如果键的哈希函数设计不合理,你会得到一个非对称的分区(不管内部数据的是多大)。所有的put()和get()方法会使用最大的链表,这样就会执行很慢,因为它需要迭代链表中的全部记录。在最坏的情况下(如果大部分数据都在同一个桶上),那么你的时间复杂度就会变为O(n)。
下面是一个可视化的示例。
4. Java 8 中的变化
和Java 7相比,Node可以被扩展成TreeNode。TreeNode是一个红黑树的数据结构,它可以存储更多的信息,这样我们可以在O(log(n))的复杂度下添加、删除或者获取一个元素。
参考文章:
Java HashMap工作原理. http://www.importnew.com/16599.html
Java Map 集合类简介.
http://www.oracle.com/technetwork/cn/articles/maps1-100947-zhs.html
Technorati 标签: JDK7,HashMap,源码学习,数据结构