java集合-HashMap(三)

版权声明:本文为博主原创文章,转载需注明出处. https://blog.csdn.net/piaoslowly/article/details/81870697

java集合-HashMap(三)

哈希算法

什么是哈希算法?

百度百科给出的解释:哈希算法将任意长度的二进制值映射为较短的固定长度的二进制值,这个小的二进制值称为哈希值。哈希值是一段数据唯一且极其紧凑的数值表示形式。如果散列一段明文而且哪怕只更改该段落的一个字母,随后的哈希都将产生不同的值。要找到散列为同一个值的两个不同的输入,在计算上是不可能的,所以数据的哈希值可以检验数据的完整性。一般用于快速查找和加密算法。

另外一种说明:
Hash,一般翻译做“散列”,也有直接音译为“哈希”的,就是把任意长度的输入(又叫做预映射, pre-image),通过散列算法,变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,而不可能从散列值来唯一的确定输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。

哈希常使用的散列函数的方法

散列函数能使对一个数据序列的訪问过程更加迅速有效,通过散列函数,数据元素将被更快地定位:

  1. 直接寻址法:取keyword或keyword的某个线性函数值为散列地址。即H(key)=key或H(key) = a•key + b,当中a和b为常数(这样的散列函数叫做自身函数)
  2. 数字分析法:分析一组数据,比方一组员工的出生年月日,这时我们发现出生年月日的前几位数字大体同样,这种话,出现冲突的几率就会非常大,可是我们发现年月日的后几位表示月份和详细日期的数字区别非常大,假设用后面的数字来构成散列地址,则冲突的几率会明显减少。因此数字分析法就是找出数字的规律,尽可能利用这些数据来构造冲突几率较低的散列地址。
  3. 平方取中法:取keyword平方后的中间几位作为散列地址。
  4. 折叠法:将keyword切割成位数同样的几部分,最后一部分位数能够不同,然后取这几部分的叠加和(去除进位)作为散列地址。
  5. 随机数法:选择一随机函数,取keyword的随机值作为散列地址,通经常使用于keyword长度不同的场合。
  6. 除留余数法:取keyword被某个不大于散列表表长m的数p除后所得的余数为散列地址。即 H(key) = key MOD p, p<=m。不仅能够对keyword直接取模,也可在折叠、平方取中等运算之后取模。对p的选择非常重要,一般取素数或m,若p选的不好,easy产生同义词。

哈希算法的应用

  • MD4:(RFC 1320)是 MIT 的 Ronald L. Rivest 在 1990 年设计的,MD 是 Message Digest 的缩写。它适用在32位字长的处理器上用快速软件实现–它是基于 32 位操作数的位操作来实现的。
  • MD5:(RFC 1321)是 Rivest 于1991年对MD4的改进版本号。它对输入仍以512位分组,其输出是4个32位字的级联,与 MD4 同样。MD5比MD4来得复杂,而且速度较之要慢一点,但更安全,在抗分析和抗差分方面表现更好
  • SHA-1 及其它:SHA1是由NIST NSA设计为同DSA一起使用的,它对长度小于264的输入,产生长度为160bit的散列值,因此抗穷举(brute-force)性更好。SHA-1 设计时基于和MD4同样原理,而且模仿了该算法。

哈希值的特点

哈希值是二进制值;
- 哈希值具有一定的唯一性;
- 哈希值极其紧凑;
- 要找到生成同一个哈希值的2个不同输入,在一定时间范围内,是不可能的。
- 希算法的不可逆性。
- 同一个key产生出来的哈希值一定相等。

哈希碰撞冲突解决

哈希算法得出来的值在某种极端的情况下也是会冲突的。即key1≠key2,而hash(key1)=hash(key2),这就是哈希冲突,因此在建造哈希表时不仅要设定一个好的哈希函数,并且要设定一种处理冲突的方法。下面就是哈希冲突解决的方法:

开放地址法

开放地执法有一个公式:Hi=(H(key)+di) MOD m i=1,2,…,k(k<=m-1)
其中,m为哈希表的表长。di 是产生冲突的时候的增量序列。如果di值可能为1,2,3,…m-1,称线性探测再散列。
如果di取1,则每次冲突之后,向后移动1个位置.如果di取值可能为1,-1,2,-2,4,-4,9,-9,16,-16,…k*k,-k*k(k<=m/2),称二次探测再散列。
如果di取值可能为伪随机数列。称伪随机探测再散列。

再哈希法

当发生冲突时,使用第二个、第三个、哈希函数计算地址,直到无冲突时。缺点:计算时间增加。
比如上面第一次按照姓首字母进行哈希,如果产生冲突可以按照姓字母首字母第二位进行哈希,再冲突,第三位,直到不冲突为止

建立一个公共溢出区

假设哈希函数的值域为[0,m-1],则设向量HashTable[0..m-1]为基本表,另外设立存储空间向量OverTable[0..v]用以存储发生冲突的记录。

链地址法(拉链法)

将所有关键字为同义词的记录存储在同一线性链表中。如下:


从上图我们可以发现哈希表是由数组+链表组成的,一个长度为16的数组中,每个元素存储的是一个链表的头结点。那么这些元素是按照什么样的规则存储到数组中呢。一般情况是通过hash(key)%len获得,也就是元素的key的哈希值对数组长度取模得到。

拉链法的优缺点:
优点:

  • 拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;
  • 由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;
  • 开放定址法为减少冲突,要求装填因子α较小,故当结点规模较大时会浪费很多空间。而拉链法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间;
  • 在用拉链法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。而对开放地址法构造的散列表,删除结点不能简单地将被删结 点的空间置为空,否则将截断在它之后填人散列表的同义词结点的查找路径。这是因为各种开放地址法中,空地址单元(即开放地址)都是查找失败的条件。因此在 用开放地址法处理冲突的散列表上执行删除操作,只能在被删结点上做删除标记,而不能真正删除结点。

缺点:

  • 指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间,而若将节省的指针空间用来扩大散列表的规模,可使装填因子变小,这又减少了开放定址法中的冲突,从而提高平均查找速度。

什么是哈希表(散列表)?

散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。

说白了散列表就是一个数组,只不过数组里面里面a[i],i是通过散列函数来生成的,我们就叫这个数组为散列表或者哈希表。

解说:之前一直没看懂上面的定义,现在终于明白了。
1. 哈希表其实就是一个数组而已,没了。
2. 为什么要叫哈希表呢?
一般来说定义一个数组

int a[] = new a[10];
a[1]=10;
a[2]=30;
a[3]=50;

看到a[i]里面的i了吗?普通的数组i我们自己手动设置的,我们需要通过i顺序存储。
3. 我们来看看hash表有什么特殊的地方呢?
在hash表中也就是a[i]数组中啦,只不过i不是我们顺序1,2,3这样来设置的。hash表中的i是通过一个算法来的出来的,我们编写一个方法叫f(key)这个f方法呢就叫做哈希方法。给数组赋值是也不是直接调用a[1]=10了,而是使用key来存数据了a[f(key)]=10,对于key来说,我们可以定义任何数据类型了。存储的时候可以不用顺序存储,只需要运算key的哈希值就可以直接存储了。

HashMap与ArrayList和LinkedList在数据复杂度上有什么区别

可以看出HashMap整体上性能都非常不错,但是不稳定,为O(N/Buckets),N就是以数组中没有发生碰撞的元素,Buckets是因碰撞产生的链表长度。
注意:实际发生碰撞时非常罕见的。所以通常来说:N/Buckets=1;

散列总结

  1. 先有了一个散列算法,通过散列算法可以将任意长度二进制转换为固定长度值,这个值叫做散列值。
  2. 有一个数组,数组的下标是0,1,2,3…,只不过这个0,1,2,3是通过一个key值进行散列计算后得到的,然后通过这个散列值把value存到相应的数组下标下面,我们就叫这个数组为散列表。
  3. 散列在某些情况下也会产生冲突,所以通过链表来解决冲突(拉链发)这就得到了一个HashMap。

HashMap

HashMap介绍

HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。
HashMap 继承于AbstractMap,实现了Map、Cloneable、java.io.Serializable接口。
HashMap 的实现是不同步的,这意味着它不是线程安全的。它的key、value都可以为null。此外,HashMap中的映射不是有序的。

HashMap 的实例有两个参数影响其性能:“初始容量” 和 “加载因子”。容量是哈希表中桶的数量,初始容量只是哈希表在创建时的容量。加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度。
当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。
通常,默认加载因子是 0.75, 这是在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数 HashMap 类的操作中,包括 get 和 put 操作,都反映了这一点)。在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少 rehash 操作次数。如果初始容量大于最大条目数除以加载因子,则不会发生 rehash 操作。

HashMap的数据结构

Java中数据存储方式最底层的两种结构,一种是数组,另一种就是链表,数组的特点:连续空间,寻址迅速,但是在删除或者添加元素的时候需要有较大幅度的移动,所以查询速度快,增删较慢。而链表正好相反,由于空间不连续,寻址困难,增删元素只需修改指针,所以查询慢、增删快。有没有一种数据结构来综合一下数组和链表,以便发挥他们各自的优势?答案是肯定的!就是:哈希表。哈希表具有较快(常量级)的查询速度,及相对较快的增删速度,所以很适合在海量数据的环境中使用。一般实现哈希表的方法采用“拉链法”,我们可以理解为“链表的数组”.

HashMap成员变量

它包括几个重要的成员变量:table, size, threshold, loadFactor, modCount。
- table是一个Entry[]数组类型,而Entry实际上就是一个单向链表。哈希表的”key-value键值对”都是存储在Entry数组中的。
- size是HashMap的大小,它是HashMap保存的键值对的数量。
- threshold是HashMap的阈值,用于判断是否需要调整HashMap的容量。threshold的值=”容量*加载因子”,当HashMap中存储数据的数量达到threshold时,就需要将HashMap的容量加倍。
- loadFactor就是加载因子。
- modCount是用来实现fail-fast机制的(快速失败机制)。

看源码前奏

  1. 假如HashMap的key为String类型,我们来看下String是怎么生成hashCode的。
    String str = “bao”;
public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

str.hashCode();
char [] value={‘b’,’a’,’o’};
第一步:h=(int)’b’;
第二步:h= 31* (int)’b’ + (int)’a’;
第三部:h= 31* (31* (int)’b’ + (int)’a’)+(int)’o’;
最后得出结果。

Integer是没有hashCode值的,它的hashCode就是它自己。想象一下也是,Integer本身就可以作为hashCode值来操作。

  1. hashMap中的hash方法
static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

解说一下:位运算符,与(&)、非(~)、或(|)、异或(^)位运算符主要针对两个二进制数的位进行逻辑运算。
&运算符:只有两个位都是1,结果才是1。
int a=129;
int b=128;
c = a&b。
c结果为128.
“a”的值是129,转换成二进制就是10000001,而“b”的值是128,转换成二进制就是10000000。根据与运算符的运算规律,只有两个位都是1,结果才是1,可以知道结果就是10000000,即128。

|运算符:两个位只要有一个为1,那么结果就是1,否则就为0
c= a|b
结果为129.

~运算符:如果位为0,结果是1,如果位为1,结果是0,
int a=3;
~a
0000 0011
1111 1100

^运算符:两个操作数的位中,相同则结果为0,不同则结果为1。
int a=15;
int b=2;
a 与 b 异或的结果是:13
15: 1111
2 : 0010
结果:1101

jdk1.7源码分析

初始化

//初始化数组大小默认为16,其他时候也必须是2的幂
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//数组的最大值,2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;
//增长因子,默认0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;

public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }

//这里表格还没有初始化呢,只是设置一些默认值。
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();
    }

put方法

//数组的大小
int threshold;

//求Integer.highestOneBit(?)的2的幂最近的一个值
//number先<<1(相当于number*2)然后再求2的幂
//这样做的目的就是不管用户手动初始化HashMap的大小为多少,最后都能得实际的一个2的幂次方
private static int roundUpToPowerOf2(int number) {
        // assert number >= 0 : "number must be non-negative";
        return number >= MAXIMUM_CAPACITY
                ? MAXIMUM_CAPACITY
                : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}

private void inflateTable(int toSize) {
        // Find a power of 2 >= toSize
        //找到一个<=2的冥的一个值来做作为数组的大小。
        int capacity = roundUpToPowerOf2(toSize);
         //MAXIMUM_CAPACITY = 1<<30
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        table = new Entry[capacity];
        //初始化hashSeed,单独测试了一下,基本都为0,这里主要是看系统里面有没有设置过threshold初始值
        initHashSeedAsNeeded(capacity);
    }

//求数组的下标,hash值与数组长度做&运算,这样就保证了的出来的index值不会超出数组的长度   
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);
} 

public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        if (key == null)
            return putForNullKey(value);//如果key为空,则把它存到数组的0号索引里面。第二个null进来则使用第二个替换第一个。这里也说明了hashMap只能存一个null值。
        //求key的hash值
        int hash = hash(key);
        //hash值与数组长度做&运算,得到小与数组长度的一个索引值。
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            //如果hash值相同,并且key值也相同,说明是同一个key值,则新值替换老的值。
            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;
    }

void addEntry(int hash, K key, V value, int bucketIndex) {
        //检查元素总数是否达到阀值threshold=容量*增长因子,如果达到了就重建内部数据结构
        //注意:元素总数不等于数组的长度哦!
        if ((size >= threshold) && (null != table[bucketIndex])) {
            //扩展数据长度为当前的2倍
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
    }  

//真正的开始创建节点并添加到数组里面了。    
void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        //添加一个元素+1,这个也说明了数组长度不等于size的大小。因为假如table[2]里面存了3个node,这样table[2]里面是不是就有三个元素了
        size++;
    }      

数组扩容

//当old数组达到阀值后,就开始扩容
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);
    }

//把老表拷贝到新表    
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);
                //这里会反转链表哦!假如老表节点里面存的是C->B->A->null;C为数组的头节点,存入新的之后变为A->B->C-null
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }    

reHash

final boolean initHashSeedAsNeeded(int capacity) {
    boolean currentAltHashing = hashSeed != 0;
    boolean useAltHashing = sun.misc.VM.isBooted() &&
            (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
    boolean switching = currentAltHashing ^ useAltHashing;
    if (switching) {
        hashSeed = useAltHashing
            ? sun.misc.Hashing.randomHashSeed(this)
            : 0;
    }
    return switching;
}

hashMap在扩容时,会做一个判断,是否需要重新做一次hash值.在测试过程中这里一直返回false就是说不需要重新做一次hash操作.那问题来了,它什么时候会重新计算key的hash值呢?
看条件:capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD 当扩容数组大小大于等于2的32次方时为true.
hashSeed :为0表示不需要做散列了.不然将等于一个随机数值,这样可以加大hash是的散列,减小hash冲突.

get方法

final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }

        int hash = (key == null) ? 0 : hash(key);
        //获取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;
    }

移除

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;
    }

遍历

//这里只是铺垫
public Set<Map.Entry<K,V>> entrySet() {
        return entrySet0();
    }

    private Set<Map.Entry<K,V>> entrySet0() {
        Set<Map.Entry<K,V>> es = entrySet;
        return es != null ? es : (entrySet = new EntrySet());
    }

    private final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
        public Iterator<Map.Entry<K,V>> iterator() {
            //遍历数组
            return newEntryIterator();
        }
        public boolean contains(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry<K,V> e = (Map.Entry<K,V>) o;
            Entry<K,V> candidate = getEntry(e.getKey());
            return candidate != null && candidate.equals(e);
        }
        public boolean remove(Object o) {
            return removeMapping(o) != null;
        }
        public int size() {
            return size;
        }
        public void clear() {
            HashMap.this.clear();
        }
    }
 //这里才是真正的开始遍历Map的
 private abstract class HashIterator<E> implements Iterator<E> {
        Entry<K,V> next;        // next entry to return
        int expectedModCount;   // For fast-fail
        int index;              // current slot
        Entry<K,V> current;     // current entry

        HashIterator() {
            expectedModCount = modCount;
            if (size > 0) { // advance to first entry
                Entry[] t = table;
                //注意它的写法:next=t[index++];然后在判断next是否为 null。
                //这里是找到t这个数组中不为null的第一个元素。
                while (index < t.length && (next = t[index++]) == null)
                    ;
            }
        }

        public final boolean hasNext() {
            return next != null;
        }

        final Entry<K,V> nextEntry() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            Entry<K,V> e = next;
            if (e == null)
                throw new NoSuchElementException();

                //next=e.next;
               //这里判断数组中的entry,有没有下一个节点,如果没有,则进入数组的下一个索引序号,index++。
               if ((next = e.next) == null) {
                Entry[] t = table;
                while (index < t.length && (next = t[index++]) == null)
                    ;
            }
            current = e;
            return e;
        }

        public void remove() {
            if (current == null)
                throw new IllegalStateException();
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            Object k = current.key;
            current = null;
            HashMap.this.removeEntryForKey(k);
            expectedModCount = modCount;
        }
    }   


解说:整个遍历思路就是,先找到不为null的数组t[index++],再然后就是遍历这个节点有没有下一个节点,有则继续遍历下一个节点,没有则重新找
下一个不为null的数组。

注意:普通数组都是0,1,2,3这样按照序号填充数组的。但是hashMap是根据hash值来填充数组的,就是说0,1,2,3也许是2里面存了值,0,1里面没值。

Map的遍历实战

        //方法一,推荐使用,这个里面可以直接修改m的值。
        for (Map.Entry<String, Integer> m : map.entrySet()) {
            System.out.println(m.getKey() + "," + m.getValue());
        }

        //方法二
        Iterator<Map.Entry<String, Integer>> it = map.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry<String, Integer> entry = it.next();
            System.out.println(entry.getKey() + "," + entry.getValue());
        }

        //方法三
        for (String key : map.keySet()) {
            Integer value = map.get(key);
        }

        //方法四:只能看value,不能看key
        for (Integer i : map.values()) {

        }

jdk1.8源码分析

不同版本1.7与1.8

JDK1.7中
使用一个Entry数组来存储数据,用key的hashcode取模来决定key会被放到数组里的位置,如果hashcode相同,或者hashcode取模后的结果相同(hash collision),那么这些key会被定位到Entry数组的同一个格子里,这些key会形成一个链表。
在hashcode特别差的情况下,比方说所有key的hashcode都相同,这个链表可能会很长,那么put/get操作都可能需要遍历这个链表
也就是说时间复杂度在最差情况下会退化到O(n)

JDK1.8中
使用一个Node数组来存储数据,但这个Node可能是链表结构,也可能是红黑树结构
如果插入的key的hashcode相同,那么这些key也会被定位到Node数组的同一个格子里。
如果同一个格子里的key不超过8个,使用链表结构存储。如果超过了8个,那么会调用treeifyBin函数,将链表转换为红黑树。
那么即使hashcode完全相同,由于红黑树的特点,查找某个特定元素,也只需要O(log n)的开销
也就是说put/get的操作的时间复杂度最差只有O(log n)

总结

  1. HashMap默认数组大小为16.但是存储的元素总数并不等于16哦,它有一个阀值,阀值一般是‘数组’.length*‘加载因子’0.75f,当数组长度达到阀值后数组就会动态增加一倍哦。16,32,64这样增长哦!16的old数组废弃,然后新建一个长度为32的数组,老的元素拷贝到新的数组里面,依此类推。
  2. 数组扩容的时候,原有的元素所在的索引值会重新计算存入到新的数组里面。
  3. HashMap可以存入null的key,这个null的key会被存到table[0]里面,如果第二个null的key来的时候,会替换第一个null的key的值哦。
  4. HashMap是多线程不安全的。
  5. HashMap的key最好使用String或者Integer类型,String类型的key是因为String.hashCode方法是自己实现的,hash冲突会更小,Integer的hash值就是它自己,所以冲突不存在(这里并不代表HashMap中不存在哦看上面的解说)。

疑问解答

HashMap与哈希表中的“拉链法”

眨一看,HashMap与哈希表中解决冲突的“拉链方法”一毛一样,但是别被误导了,它还是有不一样的地方哦。
要存入的valu值,key通过哈希运算后可以存入到数组中的任何位置,而哈希本身就冲突就微乎其微,按道理说数组中的链表应该很少有下一个节点,是这样的吗?
还有“加载因子”到底是干什么的呢?
来看一段代码示例:

public class HashMapDome { 

    public static void main(String args[]) {
        Map<String, Integer> map = new HashMap<String, Integer>();
        map.put("k1", 1);
        map.put("k2", 2);
        map.put("k3", 3);
        map.put("k4", 4);
        map.put("k5", 5);
        map.put("k6", 5);
        map.put("k7", 5);
        map.put("k8", 5);
        map.put("k9", 5);
        map.put("k10", 5);
        map.put("k11", 5); 

        for (Map.Entry<String, Integer> m : map.entrySet()) {
            int hash = hash(m.getKey());
            int i = indexFor(hash, 16);
            System.out.println("m.key=" + m.getKey() + ",hash=" + hash + ",index=" + i);
        }



    }

    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);
    }

    static int hash(Object k) {
        int h = 0;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }

        h ^= k.hashCode();

        // 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);
    }



}
//运算结果
m.key=k11,hash=101760,index=0
m.key=k3,hash=3552,index=0
m.key=k10,hash=101761,index=1
m.key=k4,hash=3553,index=1
m.key=k5,hash=3554,index=2
m.key=k6,hash=3555,index=3
m.key=k7,hash=3556,index=4
m.key=k8,hash=3557,index=5
m.key=k9,hash=3558,index=6
m.key=k1,hash=3566,index=14
m.key=k2,hash=3567,index=15

解说:这里的数组长度为16,加载因子为0.75f,就是说map的阀值为12.如果超过12个元素就扩展一倍数组。
可以看到key值不同,hash值也不同,但是他们还是有可能存到了同一个数组下标中,index就是实际map存入的数组下标。
这里只存入11个值,已经有2对开始产生链表了。
k11,k3他们都存到了t[0],就是说k11与k3已经形成链表了。

加载因子是什么?

哈希值假设不冲突,但是运算出来的hash值一般都比较大,并且没有规律,那么数组的长度则无法确定,这样就是理想是美好的,现实是很残酷的。
我们怎么把任意hash值存到我们规定的数组大小里面呢?hash值和数组的长度求一次”与运算”,这样就可以将任意哈希值塞到我们设置的大小数组中了。但是这样就有一个问题了,数组越小,运算之后的冲突越大,产生的链表就越多,这样遍历的时候就会很慢了啊。如果数组长度设置的很大,但是存的值又很少,浪费了大量空间啊,所以就有了一个”加载因子“啊。
注意:存入的元素越多冲突的可能性就越大,数组长度为16,存入了8个元素之后冲突就开始大大增加,冲突就会产生链表,它并不是当存满了之后才开始冲突的,所以我们需要设置一个加载因子,当16*0.75=12个元素之后就开始赶紧扩容吧,不然后面冲突增多了,降低了查询效率,反而得不偿失了。

不要冲突太多,也不要空间浪费,所以就折中一下吧0.75. 当数组中存入的元素个数是table.length*0.75的时候就开始扩充表一倍,虽然量费了0.25的内存空间,但是现在内存都白菜价了,也就无所谓了。

为什么HashMap的数组长度必须是2的幂?

在看源码过程中,数组的长度必须是2的幂。

private static int roundUpToPowerOf2(int number) {
        // assert number >= 0 : "number must be non-negative";
        return number >= MAXIMUM_CAPACITY
                ? MAXIMUM_CAPACITY
                : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
    }

这样求数组大小的,得到的一定是2的幂。

就奇怪了,为什么数组长度一定要为2的幂呢?
找到2中说法
第一:数组索引的时候:index = h & (length-1); length是数组的长度。2的幂减1得到的二进制都为….11111,这样的出来的index分布比较均匀。
通过把源码粘贴出来,我单独执行了key的运算和hash的运算,我们来看一组数据

public static void main(String args[]) {
        Map<String, Integer> map = new HashMap<String, Integer>();
        map.put("k1", 1);
        map.put("k2", 2);
        map.put("k3", 3);
        map.put("k4", 4);
        map.put("k5", 5);
        map.put("k6", 5);
        map.put("k7", 5);
        map.put("k8", 5);
        map.put("k9", 5);
        map.put("k10", 5); 


         for (Map.Entry<String, Integer> m : map.entrySet()) {
            int hash = hash(m.getKey());
            int i = indexFor(hash, 15);
            System.out.println("m.key=" + m.getKey() + ",hash=" + hash + ",index=" + i);
        }
  } 

  static int hash(Object k) {
        int h = 0;
//        if (0 != h && k instanceof String) {
//            return sun.misc.Hashing.stringHash32((String) k);
//        }

        h ^= k.hashCode();

        // 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);
    }     

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);
    }


//length=16
m.key=k3,hash=3552,index=0
m.key=k10,hash=101761,index=1
m.key=k4,hash=3553,index=1
m.key=k5,hash=3554,index=2
m.key=k6,hash=3555,index=3
m.key=k7,hash=3556,index=4
m.key=k8,hash=3557,index=5
m.key=k9,hash=3558,index=6
m.key=k1,hash=3566,index=14
m.key=k2,hash=3567,index=15

//length=14
m.key=k3,hash=3552,index=0
m.key=k10,hash=101761,index=1
m.key=k4,hash=3553,index=1
m.key=k5,hash=3554,index=0
m.key=k6,hash=3555,index=1
m.key=k7,hash=3556,index=4
m.key=k8,hash=3557,index=5
m.key=k9,hash=3558,index=4
m.key=k1,hash=3566,index=12
m.key=k2,hash=3567,index=13

//length=15
m.key=k3,hash=3552,index=0
m.key=k10,hash=101761,index=0
m.key=k4,hash=3553,index=0
m.key=k5,hash=3554,index=2
m.key=k6,hash=3555,index=2
m.key=k7,hash=3556,index=4
m.key=k8,hash=3557,index=4
m.key=k9,hash=3558,index=6
m.key=k1,hash=3566,index=14
m.key=k2,hash=3567,index=14

可以看到,2的幂减一,运算出来之后分布比较均匀而且空间不浪费;
如果length不是2的次幂,比如length为15,则length-1为14,对应的二进制为1110,在对h进行与操作后,最后一位都为0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!这样就会造成空间的浪费。
假设key的hash为0111
0000 0111
0000 1110
——–
0000 0110
存储到了index=6;由于14的二进制为1110,0为永远不能存储元素了。

第二种说法:2的幂数组长度,在数组扩展的时候,新数组也是一个2的幂次,
在对数组下标计算的时候
假设h=10
老的为:h & (16 - 1);
0000 1010
0000 1111
———
0000 1010

新的为:h & (32 - 1);
0000 1010
0111 1111
————
0000 1010
可以看到高位不管怎么运算都是为0,只有低位在变化,这样老数元素在移动到新数组中的时候,下标不变,但为2的幂不是为了下标不变哦,而是老的移动到新的大数组中时,同样能存储到数组的低位。比如:下面的情况
为2的幂

不为2的幂次,冲突加大
为2的幂次,存储元素是分布比较均匀,并且在数组扩展的时候元素保持不变,这样可以有效的避免浪费扩展的空间。

为什么Hashtable的数组可以不为2的幂呢?


public class HashtableDome {
    static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
    int hashSeed = 0;

    public static void main(String args[]) {
        Map<String, Integer> map = new Hashtable<String, Integer>();
        map.put("k1", 2);
        map.put("k2", 2);
        map.put("k3", 2);
        map.put("k4", 2);
        map.put("k5", 2);
        map.put("k6", 2);
        map.put("k7", 2);
        map.put("k8", 2);

        //23
//        map.put("k9", 2);
//        map.put("k10", 2);
//        map.put("k11", 2);
//        map.put("k12", 2);
//        map.put("k13", 2);
//        map.put("k14", 2);
//        map.put("k15", 2);
//        map.put("k16", 2);
//        map.put("k17", 2);


        map.get("k2");
        map.remove("k1");
        map.entrySet();

        HashtableDome dome = new HashtableDome();

        int length = 11;
        dome.initHashSeedAsNeeded(length);
        for (Map.Entry<String, Integer> m : map.entrySet()) {
            int hash = dome.hash(m.getKey());
            int index = (hash & 0x7FFFFFFF) % length;
            System.out.println("hash=" + hash + ",key=" + m.getKey() + ",index=" + index);
        }


    }

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

    final boolean initHashSeedAsNeeded(int capacity) {
        boolean currentAltHashing = hashSeed != 0;
        boolean useAltHashing = sun.misc.VM.isBooted() &&
                (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        boolean switching = currentAltHashing ^ useAltHashing;
        if (switching) {
            hashSeed = useAltHashing
                    ? sun.misc.Hashing.randomHashSeed(this)
                    : 0;
        }
        return switching;
    }

    private static class Holder {

        /**
         * Table capacity above which to switch to use alternative hashing.
         */
        static final int ALTERNATIVE_HASHING_THRESHOLD;

        static {
            String altThreshold = java.security.AccessController.doPrivileged(
                    new sun.security.action.GetPropertyAction(
                            "jdk.map.althashing.threshold"));

            int threshold;
            try {
                threshold = (null != altThreshold)
                        ? Integer.parseInt(altThreshold)
                        : ALTERNATIVE_HASHING_THRESHOLD_DEFAULT;

                // disable alternative hashing if -1
                if (threshold == -1) {
                    threshold = Integer.MAX_VALUE;
                }

                if (threshold < 0) {
                    throw new IllegalArgumentException("value must be positive integer.");
                }
            } catch (IllegalArgumentException failed) {
                throw new Error("Illegal value for 'jdk.map.althashing.threshold'", failed);
            }

            ALTERNATIVE_HASHING_THRESHOLD = threshold;
        }
    }
}

//结果:
数组长度为11
hash=3373,key=k8,index=7
hash=3372,key=k7,index=6
hash=3371,key=k6,index=5
hash=3370,key=k5,index=4
hash=3369,key=k4,index=3
hash=3368,key=k3,index=2
hash=3367,key=k2,index=1

//数组长度为23(扩大了一倍+1)
hash=3373,key=k8,index=15
hash=3372,key=k7,index=14
hash=3371,key=k6,index=13
hash=3370,key=k5,index=12
hash=3369,key=k4,index=11
hash=3368,key=k3,index=10
hash=3367,key=k2,index=9

解说:从结果来看,数组大小不为2的幂的时候分布也是均匀的,为什么呢?
因为(hash & 0x7FFFFFFF) % length;
0x7FFFFFFF=2的32幂次方。它只是换了一种求index的方式而已,但是它还是和2的32幂次方做了与运算,然后在求余。所以分布也算是均匀的。
这样求index也可以看出来,数组扩大后,元素全部往高位移动了,意味着低位index=0,1,2,3,4.。。为null了。我们遍历数组都是要从0,1,2,3顺序遍历的,元素都存到了后面,这样是不是坑爹了。

HashMap的多线程不安全之put值丢失

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++;
    }

在添加的时候table[bucketIndex]=new Entry<>(hash, key, value, e);
两个线程同时获取到了e,两个线程的下一个节点都指向了e,而table[buchetIndex],两个线程后面的线程会把前面的值覆盖了,导致一个成功一个丢失的情况。

HashMap的多线程不安全之死循环(循环链表)

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);
                //这里会反转链表哦!假如老表节点里面存的是C->B->A->null;C为数组的头节点,存入新的之后变为A->B->C-null
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }    

上面这段代码是数组扩容代码,把老数组里面的值copy到新数组里面。
.

解说:线程1,线程2同时到Entry

Hashtable

Hashtable与HashMap的主体结构是一样的,都是哈希链表。所以这里就不深入讲解了,大同小异,只讲解一下差异

继承关系

Hashtable继承的是Dictionary,HashMap继承的是AbstractMap。Dictionary,AbstractMap他们两个都是抽象类,只不过AbstractMap里面把contains方法去掉了,改成containsvalue和containsKey。

初始化

public Hashtable() {
        this(11, 0.75f);
    }

初始化大小为11,并且Hashtable数组大小并不要求为2的幂(HashMap数组大小必须为2的幂)。

put

public synchronized V put(K key, V value) {
        // Make sure the value is not null
        if (value == null) {
            throw new NullPointerException();
        }

        // Makes sure the key is not already in the hashtable.
        Entry tab[] = table;
        int hash = hash(key);
        int index = (hash & 0x7FFFFFFF) % tab.length;
        for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                V old = e.value;
                e.value = value;
                return old;
            }
        }

        modCount++;
        if (count >= threshold) {
            // Rehash the table if the threshold is exceeded
            rehash();

            tab = table;
            hash = hash(key);
            index = (hash & 0x7FFFFFFF) % tab.length;
        }

        // Creates the new entry.
        Entry<K,V> e = tab[index];
        tab[index] = new Entry<>(hash, key, value, e);
        count++;
        return null;
    }

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

Hashtable
1.计算索引值:int index = (hash & 0x7FFFFFFF) % tab.length;
2.方法上添加了synchronized,意味着它是多线程安全的
3.key和value都不能为null。
4.hash值的运算为:hashSeed ^ k.hashCode();

HashMap
1.index= h & (length-1);
2.方法上没有synchronized,意味着它是多线程不安全的。
3.key和value都可以为null,但是只能有一个null的key值。
4.hash值的运算为:上面又讲,这里就不粘贴了。生成方式和Hashtable不同哦!

扩容

Hashtable:每次为原数组大小*2+1.
int newCapacity = (oldCapacity << 1) + 1;

HashMap:每次扩展为2倍
resize(2 * table.length);

ht和hm的扩容是很有意思的哦,ht默认为11,扩容一倍为22+1,他们都是质数(只能被自己整除);hm也是,默认为16,扩容一倍后为32,但是每次hash值都是和数组长度-1然后在求与运算的,16-1=15,32-1=31.64-1=65,除了15外,其他也都是质数。

HashMap也可以变安全

使用util中的Collections.synchronizedMap(map);可以将map变成安全的。它是怎么实现的呢?

public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {
        return new SynchronizedMap<>(m);
    }

    /**
     * @serial include
     */
    private static class SynchronizedMap<K,V>
        implements Map<K,V>, Serializable {
        private static final long serialVersionUID = 1978198479659022715L;

        private final Map<K,V> m;     // Backing Map
        final Object      mutex;        // Object on which to synchronize

        SynchronizedMap(Map<K,V> m) {
            if (m==null)
                throw new NullPointerException();
            this.m = m;
            mutex = this;
        }

        SynchronizedMap(Map<K,V> m, Object mutex) {
            this.m = m;
            this.mutex = mutex;
        }

        public int size() {
            synchronized (mutex) {return m.size();}
        }
        public boolean isEmpty() {
            synchronized (mutex) {return m.isEmpty();}
        }
        public boolean containsKey(Object key) {
            synchronized (mutex) {return m.containsKey(key);}
        }
        public boolean containsValue(Object value) {
            synchronized (mutex) {return m.containsValue(value);}
        }
        public V get(Object key) {
            synchronized (mutex) {return m.get(key);}
        }

        public V put(K key, V value) {
            synchronized (mutex) {return m.put(key, value);}
        }
        public V remove(Object key) {
            synchronized (mutex) {return m.remove(key);}
        }
        public void putAll(Map<? extends K, ? extends V> map) {
            synchronized (mutex) {m.putAll(map);}
        }
        public void clear() {
            synchronized (mutex) {m.clear();}
        }

        private transient Set<K> keySet = null;
        private transient Set<Map.Entry<K,V>> entrySet = null;
        private transient Collection<V> values = null;

        public Set<K> keySet() {
            synchronized (mutex) {
                if (keySet==null)
                    keySet = new SynchronizedSet<>(m.keySet(), mutex);
                return keySet;
            }
        }

        public Set<Map.Entry<K,V>> entrySet() {
            synchronized (mutex) {
                if (entrySet==null)
                    entrySet = new SynchronizedSet<>(m.entrySet(), mutex);
                return entrySet;
            }
        }

        public Collection<V> values() {
            synchronized (mutex) {
                if (values==null)
                    values = new SynchronizedCollection<>(m.values(), mutex);
                return values;
            }
        }

        public boolean equals(Object o) {
            if (this == o)
                return true;
            synchronized (mutex) {return m.equals(o);}
        }
        public int hashCode() {
            synchronized (mutex) {return m.hashCode();}
        }
        public String toString() {
            synchronized (mutex) {return m.toString();}
        }
        private void writeObject(ObjectOutputStream s) throws IOException {
            synchronized (mutex) {s.defaultWriteObject();}
        }
    }

讲解:SynchronizedMap继承了Map,这样的好处是可以直接返回Map对象,外面不用再次转换了。
内部有一个Map对象,用来承接外面传入的HashMap对象,这样就不用自己在实现一次HashMap了,然后每个方法加入了synchronized。

这里就好比加入了一个壳子,包装了一下HashMap的所有东西。

参考地址

http://blog.csdn.net/zeb_perfect/article/details/52574915 哈希冲突
http://www.cnblogs.com/stevenczp/p/7028071.html 1.8与1.7差异
http://blog.csdn.net/vking_wang/article/details/14166593 HashMap原理
http://blog.jobbole.com/106733/ 哈希算法
http://www.jianshu.com/p/bf1d7eee28d0 哈希算法,hashmap
http://blog.csdn.net/vebasan/article/details/6193916 与或运算符
http://blog.csdn.net/sd_csdn_scy/article/details/57083619 数组长度为2的幂
http://www.cnblogs.com/chengxiao/p/6059914.html 数组长度为2的幂
http://www.cnblogs.com/andy-zhou/p/5402984.html 循环链表(感觉这个讲解的不是很对,但是帮我找到了分析的思路)

猜你喜欢

转载自blog.csdn.net/piaoslowly/article/details/81870697