Hashmap学习

 

如下这个博文是基于JDK1.7的,html

https://www.cnblogs.com/xrq730/p/5030920.htmljava

主要是粘贴了上篇博文的程序员

这个博文很详细,可是没有图只有代码,看的有点吃力面试

https://www.cnblogs.com/skywang12345/p/3310835.html#a24算法

https://www.cnblogs.com/xiaoxi/p/7233201.html数据库

 

仍是没有吃透,看了个大概吧。数组

 

 

 

初识HashMap安全

以前的List,讲了ArrayList、LinkedList,最后讲到了CopyOnWriteArrayList,就前二者而言,反映的是两种思想:数据结构

(1)ArrayList以数组形式实现,顺序插入、查找快,插入、删除较慢app

(2)LinkedList以链表形式实现,顺序插入、查找较慢,插入、删除方便

那么是否有一种数据结构可以结合上面两种的优势呢?有,答案就是HashMap。

HashMap是一种很是常见、方便和有用的集合,是一种键值对(K-V)形式的存储结构,下面将仍是用图示的方式解读HashMap的实现原理,

 

四个关注点在HashMap上的答案

关  注  点 结      论
HashMap是否容许空 Key和Value都容许为空
HashMap是否容许重复数据 Key重复会覆盖、Value容许重复
HashMap是否有序 无序,特别说明这个无序指的是遍历HashMap的时候,获得的元素的顺序基本不多是put的顺序
HashMap是否线程安全 非线程安全

 

添加数据

首先看一下HashMap的一个存储单元Entry:

复制代码
static class Entry<K,V> implements Map.Entry<K,V> {
    final K key;
    V value;
    Entry<K,V> next;
    int hash;
    ...
}
复制代码

以前一篇写LinkedList的文章,里面写到LinkedList是一个双向链表,从HashMap的Entry看得出,Entry组成的是一个单向链表,由于里面只有Entry的后继Entry,而没有Entry的前驱Entry。用图表示应该是这么一个数据结构:

接下来,假设我有这么一段代码:

1 public static void main(String[] args)
2 {
3     Map<String, String> map = new HashMap<String, String>();
4     map.put("111", "111");
5     map.put("222", "222");
6 }

看一下作了什么。首先从第3行开始,new了一个HashMap出来:

1 public HashMap() {
2     this.loadFactor = DEFAULT_LOAD_FACTOR;
3     threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
4     table = new Entry[DEFAULT_INITIAL_CAPACITY];
5     init();
6 }

注意一下第5行的init()是个空方法,它是HashMap的子类好比LinkedHashMap构造的时候使用的。DEFAULT_INITIAL_CAPACITY为16,也就是说,HashMap在new的时候构造出了一个大小为16的Entry数组,Entry内全部数据都取默认值,如图示为:

看到new出了一个大小为16的Entry数组来。接着第4行,put了一个Key和Value同为111的字符串,看一下put的时候底层作了什么:

复制代码
 1 public V put(K key, V value) {
 2     if (key == null)
 3         return putForNullKey(value);
 4     int hash = hash(key.hashCode());
 5     int i = indexFor(hash, table.length);
 6     for (Entry<K,V> e = table[i]; e != null; e = e.next) {
 7        Object k;
 8         if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
 9             V oldValue = e.value;
10             e.value = value;
11             e.recordAccess(this);
12             return oldValue;
13         }
14     }
15 
16     modCount++;
17     addEntry(hash, key, value, i);
18     return null;
19 }
复制代码
复制代码
1 static int hash(int h) {
2     // This function ensures that hashCodes that differ only by
3     // constant multiples at each bit position have a bounded
4     // number of collisions (approximately 8 at default load factor).
5     h ^= (h >>> 20) ^ (h >>> 12);
6     return h ^ (h >>> 7) ^ (h >>> 4);
7 }
复制代码
 1 static int indexFor(int h, int length) {
 2     return h & (length-1);
 3 }

看一下put方法的几个步骤:

一、第2行~第3行就是HashMap容许Key值为空的缘由,空的Key会默认放在第0位的数组位置上

二、第4行拿到Key值的HashCode,因为HashCode是Object的方法,所以每一个对象都有一个HashCode,对这个HashCode作一次hash计算。按照JDK源码注释的说法,此次hash的做用是根据给定的HashCode对它作一次打乱的操做,防止一些糟糕的Hash算法产生的糟糕的Hash值,至于为何要防止糟糕的Hash值,HashMap添加元素的最后会讲到

三、第5行根据从新计算的HashCode,对Entry数组的大小取模获得一个Entry数组的位置。看到这里使用了&,移位加快一点代码运行效率。另外,这个取模操做的正确性依赖于length必须是2的N次幂,这个熟悉二进制的朋友必定理解,所以注意HashMap构造函数中,若是你指定HashMap初始数组的大小initialCapacity,若是initialCapacity不是2的N次幂,HashMap会算出大于initialCapacity的最小2的N次幂的值,做为Entry数组的初始化大小。这里为了讲解方便,咱们假定字符串111和字符串222算出来的i都是1

四、第6行~第14行会先判断一下原数据结构中是否存在相同的Key值,存在则覆盖并返回,不执行后面的代码。注意一下recordAccess这个方法,它也是HashMap的子类好比LinkedHashMap用的,HashMap中这个方法为空。另外,注意一点,对比Key是否相同,是先比HashCode是否相同,HashCode相同再判断equals是否为true,这样大大增长了HashMap的效率,对HashCode不熟悉的朋友能够看一下个人这篇文章讲讲HashCode的做用

五、第16行的modeCount++是用于fail-fast机制的,每次修改HashMap数据结构的时候都会自增一次这个值

而后就到了关键的addEntry方法了:

void addEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
    if (size++ >= threshold)
        resize(2 * table.length);
}
Entry(int h, K k, V v, Entry<K,V> n) {
    value = v;
    next = n;
    key = k;
    hash = h;
}

假设new出来的Entry地址为0x00000001,那么,put("111", "111")用图表示应该是这样的:

每个新增的Entry都位于table[1]上,另外,里面的hash是rehash以后的hash而不是Key最原始的hash。看到table[1]上存放了111---->111这个键值对,它持有原table[1]的引用地址,所以能够寻址到原table[1],这就是单向链表。 再看一下put("222", "222")作了什么,一张图就能够理解了:

新的Entry再次占据table[1]的位置,而且持有原table[1],也就是111---->111这个键值对。

至此,HashMap进行put数据的过程就呈现清楚了。不过还有一个问题,就是HashMap如何进行扩容,再看一下addEntry方法:

 1 void addEntry(int hash, K key, V value, int bucketIndex) {
 2 Entry<K,V> e = table[bucketIndex];
 3     table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
 4     if (size++ >= threshold)
 5         resize(2 * table.length);
 6 }

看到第4行~第5行,也就是说在每次放置完Entry以后都会判断是否须要扩容。这里不讲扩容是由于HashMap扩容在不正确的使用场景下将会致使死循环,这是一个值得探讨的话题,也是我工做中实际遇到过的一个问题,所以下一篇文章将会详细说明为何不正确地使用HashMap会致使死循环。

 

删除数据

有一段代码:

复制代码
1 public static void main(String[] args)
2 {
3     Map<String, String> map = new HashMap<String, String>();
4     map.put("111", "111");
5     map.put("222", "222");
6     map.remove("111");
7 }
复制代码

第6行删除元素,看一下删除元素的时候作了什么,第4行~第5行添加了两个键值对就沿用上面的图,HashMap删除指定键值对的源代码是:

 1 public V remove(Object key) {
 2     Entry<K,V> e = removeEntryForKey(key);
 3     return (e == null ? null : e.value);
 4 }
复制代码
 1 final Entry<K,V> removeEntryForKey(Object key) {
 2     int hash = (key == null) ? 0 : hash(key.hashCode());
 3     int i = indexFor(hash, table.length);
 4     Entry<K,V> prev = table[i];
 5     Entry<K,V> e = prev;
 6 
 7     while (e != null) {
 8         Entry<K,V> next = e.next;
 9         Object k;
10         if (e.hash == hash &&
11             ((k = e.key) == key || (key != null && key.equals(k)))) {
12             modCount++;
13             size--;
14             if (prev == e)
15                 table[i] = next;
16             else
17                 prev.next = next;
18             e.recordRemoval(this);
19             return e;
20         }
21         prev = e;
22         e = next;
23     }
24 
25     return e;
26 }
复制代码

分析一下remove元素的时候作了几步:

一、根据key的hash找到待删除的键值对位于table的哪一个位置上

二、记录一个prev表示待删除的Entry的前一个位置Entry,e能够认为是当前位置

三、从table[i]开始遍历链表,假如找到了匹配的Entry,要作一个判断,这个Entry是否是table[i]:

(1)是的话,也就是第14行~第15行,table[i]就直接是table[i]的下一个节点,后面的都不须要动

(2)不是的话,也就是第16行~第17行,e的前一个Entry也就是prev,prev的next指向e的后一个节点,也就是next,这样,e所表明的Entry就被踢出了,e的先后Entry就连起来了

remove("111")用图表示就是:

整个过程只须要修改一个节点的next的值便可,很是方便。

 

修改数据

修改元素也是put,看一下源代码:

复制代码
 1 public V put(K key, V value) {
 2     if (key == null)
 3         return putForNullKey(value);
 4     int hash = hash(key.hashCode());
 5     int i = indexFor(hash, table.length);
 6     for (Entry<K,V> e = table[i]; e != null; e = e.next) {
 7         Object k;
 8         if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
 9             V oldValue = e.value;
10             e.value = value;
11             e.recordAccess(this);
12             return oldValue;
13         }
14     }
15     modCount++;
16     addEntry(hash, key, value, i);
17     return null;
18 }
复制代码

这个其实前面已经提到过了,第6行~第14行就是修改元素的逻辑,若是某个Key已经在数据结构中存在的话,那么就会覆盖原value,也就是第10行的代码。

 

插入数据

所谓"插入元素",在个人理解里,必定是基于数据结构是有序的前提下的。像ArrayList、LinkedList,再远点说就是数据库,一条一条都是有序的。

而HashMap,它的顺序是基于HashCode,HashCode是一个随机性很强的数字,因此HashMap中的Entry彻底是随机存放的。HashMap又不像LinkedHashMap这样维护了插入元素的顺序,因此对HashMap这个数据结构谈插入元素是没有意义的。

因此,HashMap并无插入的概念。

 

再谈HashCode的重要性

前面讲到了,HashMap中对Key的HashCode要作一次rehash,防止一些糟糕的Hash算法生成的糟糕的HashCode,那么为何要防止糟糕的HashCode?

糟糕的HashCode意味着的是Hash冲突,即多个不一样的Key可能获得的是同一个HashCode,糟糕的Hash算法意味着的就是Hash冲突的几率增大,这意味着HashMap的性能将降低,表如今两方面:

一、有10个Key,可能6个Key的HashCode都相同,另外四个Key所在的Entry均匀分布在table的位置上,而某一个位置上却链接了6个Entry。这就失去了HashMap的意义,HashMap这种数据结构性高性能的前提是,Entry均匀地分布在table位置上,但如今确是1 1 1 1 6的分布。因此,咱们要求HashCode有很强的随机性,这样就尽量地能够保证了Entry分布的随机性,提高了HashMap的效率。

二、HashMap在一个某个table位置上遍历链表的时候的代码:

if (e.hash == hash && ((k = e.key) == key || key.equals(k)))

看到,因为采用了"&&"运算符,所以先比较HashCode,HashCode都不相同就直接pass了,不会再进行equals比较了。HashCode由于是int值,比较速度很是快,而equals方法每每会对比一系列的内容,速度会慢一些。Hash冲突的几率大,意味着equals比较的次数势必增多,必然下降了HashMap的效率了。

 

HashMap的table为何是transient的

一个很是细节的地方:

transient Entry[] table;

看到table用了transient修饰,也就是说table里面的内容全都不会被序列化,不知道你们有没有想过这么写的缘由?

在我看来,这么写是很是必要的。由于HashMap是基于HashCode的,HashCode做为Object的方法,是native的:

public native int hashCode();

这意味着的是:HashCode和底层实现相关,不一样的虚拟机可能有不一样的HashCode算法。再进一步说得明白些就是,可能同一个Key在虚拟机A上的HashCode=1,在虚拟机B上的HashCode=2,在虚拟机C上的HashCode=3。

这就有问题了,Java自诞生以来,就以跨平台性做为最大卖点,好了,若是table不被transient修饰,在虚拟机A上能够用的程序到虚拟机B上能够用的程序就不能用了,失去了跨平台性,由于:

一、Key在虚拟机A上的HashCode=100,连在table[4]上

二、Key在虚拟机B上的HashCode=101,这样,就去table[5]上找Key,明显找不到

整个代码就出问题了。所以,为了不这一点,Java采起了重写本身序列化table的方法,在writeObject选择将key和value追加到序列化的文件最后面:

复制代码
private void writeObject(java.io.ObjectOutputStream s)
        throws IOException
{
Iterator<Map.Entry<K,V>> i =
    (size > 0) ? entrySet0().iterator() : null;

// Write out the threshold, loadfactor, and any hidden stuff
s.defaultWriteObject();

// Write out number of buckets
s.writeInt(table.length);

// Write out size (number of Mappings)
s.writeInt(size);

    // Write out keys and values (alternating)
if (i != null) {
 while (i.hasNext()) {
    Map.Entry<K,V> e = i.next();
    s.writeObject(e.getKey());
    s.writeObject(e.getValue());
    }
    }
}
复制代码

而在readObject的时候重构HashMap数据结构:

复制代码
private void readObject(java.io.ObjectInputStream s)
         throws IOException, ClassNotFoundException
{
// Read in the threshold, loadfactor, and any hidden stuff
s.defaultReadObject();

// Read in number of buckets and allocate the bucket array;
int numBuckets = s.readInt();
table = new Entry[numBuckets];

    init();  // Give subclass a chance to do its thing.

// Read in size (number of Mappings)
int size = s.readInt();

// Read the keys and values, and put the mappings in the HashMap
for (int i=0; i<size; i++) {
    K key = (K) s.readObject();
    V value = (V) s.readObject();
    putForCreate(key, value);
}
}
复制代码

一种麻烦的方式,但却保证了跨平台性。

这个例子也告诉了咱们:尽管使用的虚拟机大多数状况下都是HotSpot,可是也不能对其它虚拟机无论不顾,有跨平台的思想是一件好事。

 

HashMap和Hashtable的区别

HashMap和Hashtable是一组类似的键值对集合,它们的区别也是面试常被问的问题之一,我这里简单总结一下HashMap和Hashtable的区别:

一、Hashtable是线程安全的,Hashtable全部对外提供的方法都使用了synchronized,也就是同步,而HashMap则是线程非安全的

二、Hashtable不容许空的value,空的value将致使空指针异常,而HashMap则无所谓,没有这方面的限制

三、上面两个缺点是最主要的区别,另一个区别可有可无,我只是提一下,就是两个的rehash算法不一样,Hashtable的是:

private int hash(Object k) {
    // hashSeed will be zero if alternative hashing is disabled.
    return hashSeed ^ k.hashCode();
}

这个hashSeed是使用sun.misc.Hashing类的randomHashSeed方法产生的。HashMap的rehash算法上面看过了,也就是:

复制代码
static int hash(int h) {
    // This function ensures that hashCodes that differ only by
    // constant multiples at each bit position have a bounded
    // number of collisions (approximately 8 at default load factor).
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}
复制代码
================================================================================== 

我不能保证写的每一个地方都是对的,可是至少能保证不复制、不黏贴,保证每一句话、每一行代码都通过了认真的推敲、仔细的斟酌。每一篇文章的背后,但愿都能看到本身对于技术、对于生活的态度。

我相信乔布斯说的,只有那些疯狂到认为本身能够改变世界的人才能真正地改变世界。面对压力,我能够挑灯夜战、不眠不休;面对困难,我愿意迎难而上、永不退缩。

其实我想说的是,我只是一个程序员,这就是我如今纯粹人生的所有。
相关文章
相关标签/搜索