一起来看下
定义:
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
集成AbstractMap类,实现了Map、Cloneable/
常量定义:
<span style="white-space:pre"> </span> <pre name="code" class="html">//存储数据的Entry数组,它的大小必须是2的幂
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
//map中保存的键值对的数量
transient int size;
//需要调整大小的极限值
int threshold;
//装载因子
final float loadFactor;
//map修改的次数
transient int modCount;
//默认的map大小
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
//哈希因子
transient int hashSeed = 0;
//默认初始大小 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 Entry<?,?>[] EMPTY_TABLE = {}; 在HashMap中,使用Entry这一对象来存储元素结构,它在Map接口中定义:
<pre name="code" class="html"> <span style="white-space:pre"> </span> 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;
threshold = initialCapacity;
init();
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
//构造方法很好理解,其中init()函数为空
//使用一个Map来构造新的map的构造函数
public HashMap(Map<? extends K, ? extends V> m) {
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
//扩张表
inflateTable(threshold);
//将老的map中的元素全部放入到新的map之中
putAllForCreate(m);
}
对构造方法分析下:构造方法一共四个,第一个也就是主要用的,它的参数传入了两个参数,初始容量和负载因子;并且将扩展阈值的大小变为初始容量;最后一个构造函数,使用一个Map对象作为参数,来构建一个新的Map;
这个函数里面有两个新的函数,分别是inflateTable和putAllForCreate,下来看看实现:
private void inflateTable(int toSize) {
// 前面提到了,table的长度一定是2的幂,这个函数是计算大于且最接近toSize的数的;这里是将容量扩大到大于toSize的最小的2的幂
int capacity = roundUpToPowerOf2(toSize);
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
//初始化哈希的掩码值
initHashSeedAsNeeded(capacity);
}
在inflateTable里面,我们看见一个roundUpToPowerOf2()的函数,它的作用我在上面已经谢了,看下具体实现:
//此函数返回大于等于最接近number的2的冪数:如果number>MAXIMUM_CAPACITY,返回MAXIMUM_CAPACITY;Integer.highestOneBit(number)返回小于等于最接近number的2的冪数,比如5是101,对5调用次函数,返回1000
//Integer.bitCount()是返回number中2进制中1的个数;因为number的最高位为1,所以当二进制中1的个数多余1,就说明最number大鱼Integer.highestOneBit(number),小于这个数字的2倍;因此让他扩大一倍,就是最接近大于等于number的数字了
private static int roundUpToPowerOf2(int number) {
// assert number >= 0 : "number must be non-negative";
int rounded = number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (rounded = Integer.highestOneBit(number)) != 0
? (Integer.bitCount(number) > 1) ? rounded << 1 : rounded
: 1;
return rounded;
}
private void putAllForCreate(Map<? extends K, ? extends V> m) {
for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
putForCreate(e.getKey(), e.getValue());
}
对旧map中的每一个元素进行putForCreate()的操作,
private void putForCreate(K key, V value) {
int hash = null == key ? 0 : hash(key);
int i = indexFor(hash, table.length);
/**
* <span style="font-family: Verdana, Arial, Helvetica, sans-serif; font-size: 13.9200000762939px; line-height: 20.8800010681152px;">该方法先计算需要添加的元素的hash值和在table数组中的索引i。接着遍历table[i]的链表,若有元素的key值与传入key值相等,则替换value,结束方法。若不存在key值相同的元素,则调用createEntry创建并添加元素。</span>
*/
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
e.value = value;
return;
}
}
createEntry(hash, key, value, i);
}
第一步显示计算hash的值,如果为返回0,否则根据hash函数返回一个值;看下Hash()函数:
// 这个方法的主要作用是防止质量较差的哈希函数带来过多的冲突(碰撞)问题。对hashCode再次哈希的原因是减少哈希冲突
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
下来,根据hash的值,找到在table中的位置:indexFor()函数:
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
}
这里,
它通过
h & (table.length -1)
来得到该对象的保存位,而
HashMap
底层数组的长度总是
2
的
n
次方,这是
HashMap
在速度上的优化。
当length总是 2 的n次方时,h& (length-1)运算等价于对length取模,也就是h%length,但是&比%具有更高的效率。
这看上去很简单,其实比较有玄机的,我们举个例子来说明:
假设数组长度分别为15和16,优化后的hash码分别为8和9,那么&运算后的结果如下:
h & (table.length-1) hash table.length-1
8 & (15-1): 0100 & 1110 = 0100
9 & (15-1): 0101 & 1110 = 0100
-----------------------------------------------------------------------------------------------------------------------
8 & (16-1): 0100 & 1111 = 0100
9 & (16-1): 0101 & 1111 = 0101
从上面的例子中可以看出:当它们和15-1(1110)“与”的时候,产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,这就产生了碰撞,8和9会被放到数组中的同一个位置上形成链表,那么查询的时候就需要遍历这个链 表,得到8或者9,这样就降低了查询的效率。同时,我们也可以发现,当数组长度为15的时候,hash值会与15-1(1110)进行“与”,那么 最后一位永远是0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!而当数组长度为16时,即为2的n次方时,2n-1得到的二进制数的每个位上的值都为1,这使得在低位上&时,得到的和原hash的低位相同,加之hash(int h)方法对key的hashCode的进一步优化,加入了高位计算,就使得只有相同的hash值的两个值才会被放到数组中的同一个位置上形成链表。
所以说,当数组长度为2的n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。
最后的创建新的Entry对象函数:
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
到此,与HashMap相关的方法就已经分析完毕,下来看下HashMap常用的几个方法。
常用方法:
首先,看下HashMap中的put方法:
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
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;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
put函数先判断table是否为空表,如果是空表则先扩张整个表,inflateTable上面已经写过~~;然后判断key的值是不是为null,如果为null,存key为null的entry元素;否则,找出其hash值和在table中的下标,然后判断将存的元素的key值时候已经在map中有,如果存在,需改value值,返回此entry~,如果没有,添加新的,返回null;
private V putForNullKey(V value) {
<span style="white-space:pre"> </span>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;
}
这个函数先遍历table,如果table中有entry的key为null,则修改它的value,否则,创建新的entry添加到table中~~其中e.recordAccess方法的作用记录当调用put函数时,所存的entry元素的key已经存在,覆盖value的时间,这个函数是个空函数~~。
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);
}
这个函数是添加新的entry的,再添加的时候,我们肯定会遇到这样一个情况,如果table的大小已经满了,且带添加的这个key需要新的table空间,则需要扩展原有的table了;这里判断如果大小大于或者等于阈值且当前添加的元素部位null,扩充table,调用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, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
resize函数先判断原来table的大小,如果达到最大值,不再扩充,阈值设为最大值;否则,将table的长度翻倍,再讲以前的元素全部放入到新的table中:
<pre name="code" class="html"> void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> 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;//这里又将链表倒序了一次。
}
}
}
<span style="font-family: Verdana, Arial, Helvetica, sans-serif; font-size: 13.9200000762939px; line-height: 20.8800010681152px;"> 从上面的代码可以看出,HashMap之所以不能保持元素的顺序有以下几点原因:第一,插入元素的时候对元素进行哈希处理,不同元素分配到table的不同位置;第二,容量拓展的时候又进行了hash处理;第三,复制原表内容的时候链表被倒置。</span>
在来看看get方法:
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
首先判断key是不是为null,如果为null,去找到key为null的entry,否则,根据key的值去找,函数很好理解~
private V getForNullKey() {
if (size == 0) {
return null;
}
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
再来看看删除remove();
public V remove(Object key) {
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.value);
}
final Entry<K,V> removeEntryForKey(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
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;
}
上面的这个过程就是先找到table数组中对应的索引,接着就类似于一般的链表的删除操作,而且是单向链表删除节点,很简单。在C语言中就是修改指针,这个例子中就是将要删除节点的前一节点的next指向删除被删除节点的next即可。
在看看clear方法:
public void clear() {
modCount++;
Arrays.fill(table, null);
size = 0;
}
直接将所有的元素变为null;
在看看两个新增的函数(相比起hashtable)
containskey()
public boolean containsKey(Object key) {
return getEntry(key) != null;
}
直接利用getEntry函数进行判断
containsValue()
private boolean containsNullValue() {
Entry[] tab = table;
for (int i = 0; i < tab.length ; i++)
for (Entry e = tab[i] ; e != null ; e = e.next)
if (e.value == null)
return true;
return false;
}
没什么好说的~~
其余的基本上都是很少用的api了,大家可以自己分析分析;
我们都知道hatshable与hashmap的区别一个是同步的,另一个是不同步的,但是hashmap还有一个ConcurrentHashMap是同步的,他和hashtable有什么区别呢?下来分析hashtable与ConcurrentHashMap