Java HashMap的实现

  • 今天是公司的技术日,晚上一位同事分享了Java中常用的数据结构实现。他说的东西基本上都有些了解,但是并没有认真得看过源码,听完他的分享回家后自己稍微理了一下结合网上的一些资料,记录下自己对Java HashMap的理解。

目录

  • Hash算法
  • Hash表
  • HashMap在Java中的实现

一、 Hash算法

什么是哈希算法呢?在我的理解里,Hash算法就是 一类 算法,它们对任意长度的二进制输入值做换算,最终给出固定长度的二进制输出值。

以更好理解的方式来说,Hash算法 是 摘要算法 :也就是说,从不同的输入中,通过一些计算摘取出来一段输出数据,值可以用以区分输入数据。

举个例子:我们熟悉的MD5,就是一种Hash算法。


二、Hash表

上面我们知道了Hash算法是什么,那么一个事物总该有它的用途是不是。Hash算法的一个重要应用就是Hash表。

那么什么是Hash表呢?要明白什么事Hash表,我们要先来了解下我们是怎样在数据结构中进行查找操作的。

  • 线性表、树

    线性表、树 这些结构中,记录 在结构 中的相对位置是随机的,和记录的关键字之间不存在确定关系,因此,在结构中查找时需要进行一系列和关键字的比较。这一类查找方法建立在“比较”的基础上。在顺序查找时,比较的结果为“=”与“≠”2种可能;在折半查找、二叉排序树查找和B-树查找时,比较的结果为“<”“=”“>”3种可能。查找的效率依赖于查找过程中所进行的比较次数。

  • 哈希表

    理想的情况是希望不经过任何比较,一次存取便能得到所查记录,那就必须在记录的存储位置和它的关键字之间建立一个确定的关系f,使每个关键字和结构中一个唯一的存储位置相对应。因而在查找时,只要根据 这个对应关系f找到给定值K的像f(K) 。若结构中存在关键字和K相等的记录,则必定在f(K)的存储位置上,反之在这个位置上没有记录。由此,不需要比较便可直接取得所查记录。在此,我们称这个 对应关系f为哈希(Hash)函数 ,按这个思想建立的表为 哈希表

上面涉及到了一个叫Hash函数的东西,那么我们再来了解下什么叫Hash函数。

  • Hash函数

    Hash函数是一个映像,因此Hash函数的设定很灵活,只要使得任何关键字由此所得的Hash函数值都落在表长允许的范围之内即可。

    对不同的关键字可能得到同一Hash地址,即key1≠key2,而f(key1)=f(key2) ,这种现象称为冲突(collision)。

    冲突只能尽量地少,而不能完全避免。因为,Hash函数是从关键字集合到地址集合的映像。而通常关键字集合比较大,它的元素包括所有可能的关键字,而地址集合的元素仅为Hash表中的地址值。因此,在实现Hash表这种数据结构的时候不仅要设定一个“好”的Hash函数,而且要设定一种处理冲突的方法。常见的处理冲突的办法有 开放定址法、再哈希法、链地址法和公共溢出区(由于重点HashMap的实现,这部分不再具体表述,不明白的读者可以自行google或者百度下)。


三、HashMap在Java中的实现

先说明下 以下源码都基于JDK 1.7的实现,JDK 1.7和JDK 1.8之间稍微有点不同。

1.一开始先笼统得说下HashMap的组成:

1.HashMap是基于数组来实现哈希表的,数组就好比内存储空间,数组的index就好比内存的地址;

2.HashMap的每个记录就是一个**Entry **对象,数组中存储的就是这些对象;

3.HashMap的哈希函数 = 计算出hashCode + 计算出数组的index;

4.HashMap解决冲突:使用链地址法,每个Entry对象都有一个引用next来指向链表的下 一个Entry;

5.HashMap的装填因子:默认为0.75;

基本上HashMap就像这样:
HashMap

2.new HashMap

/*** 1. (静态/实例)成员变量 ***/
/** 默认的容量,容量必须是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;
/** 默认Entry数组 */
static final Entry<?,?>[] EMPTY_TABLE = {};

/** Entry数组:table */
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

/** table中实际的Entry数量 */
transient int size;

/** 
 * size到达此门槛后,必须扩容table;
 * 值为capacity * load factor,默认为16 * 0.75 也就是12。
 * 意味着默认情况构造情况下,当你存够12个时,table会第一次扩容
 */
int threshold;

/** 装填因子,值从一开构造HashMap时就被确定了,默认为0.75 */
final float loadFactor;

/**
 * 哈希种子,实例化HashMap后在将要使用前设置的随机值,可以使得key的hashCode冲突更难出现
 */
transient int hashSeed = 0;

/**
 * The number of times this HashMap has been structurally modified
 * Structural modifications are those that change the number of mappings in
 * the HashMap or otherwise modify its internal structure (e.g.,
 * rehash).  This field is used to make iterators on Collection-views of
 * the HashMap fail-fast.  (See ConcurrentModificationException).
 */
transient int modCount;

/*** 2. 构造方法:最终使用的是这个构造方法 ***/
// 初始容量initialCapacity为16,装填因子loadFactor为0.75
/**
     * Constructs an empty <tt>HashMap</tt> with the specified initial
     * capacity and load factor.
     *
     * @param  initialCapacity the initial capacity
     * @param  loadFactor      the load factor
     * @throws IllegalArgumentException if the initial capacity is negative
     *         or the load factor is nonpositive
     */
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;
     this.threshold = tableSizeFor(initialCapacity);
    }

/*** 3. Map.Entry<K,V>:数组table中实际存储的类型 ***/
static class Entry<K,V> implements Map.Entry<K,V> {
    final K key;       // "Key-Value对"的Key
    V value;           // "Key-Value对"的Key
    Entry<K,V> next;    
    int hash;

    Entry(int h, K k, V v, Entry<K,V> n) {
        value = v;
        next = n;//链表的下一个Entry
        key = k;
        hash = h;
    }
    public final int hashCode() {
        return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
    }
}

3.存入数值 put

/** 存放 **/
public V put(K key, V value) {
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);//table会被初始化为长度16,且hashSeed会被赋值;
    }
    if (key == null)
        //HashMap允许key为null:在table中找到null key,然后设置Value,同时其hash为0;
        return putForNullKey(value);

    // a). 计算key的hashCode,下面详细说
    int hash = hash(key);

    // b). 根据hashCode计算index
    int i = indexFor(hash, table.length);

    // c). 做覆盖,遍历index位置的Entry链表,*不是解决*冲突
    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))) {
            // hashCode和equals都相等则表明:本次put是覆盖操作,下面return了被覆盖的老value
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }

    modCount++;
    // d). 添加Entry,并解决冲突
    // 如果需要增加table长度(size>threshold)就乘2增加,并重新计算每个元素在新table中的位置和转移
    addEntry(hash, key, value, i);
    return null;//增加成功最后返回null
}


//详细说说上面的a). b). d).

/** a). 为了防止低质量的hash函数,HashMap在这里会重新计算一遍key的hashCode **/
final int hash(Object k) {
    int h = hashSeed;
    if (0 != h && k instanceof String) {//字符串会被特殊处理,返回32bit的整数(就是int)
        return sun.misc.Hashing.stringHash32((String) k);
    }

    h ^= k.hashCode();//将key的hashCode与h按位异或,最后赋值给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);
}

/**
 * b). 计算此hashCode该被放入table的哪个index
 */
static int indexFor(int h, int length) {
    return h & (length-1);//与table的length - 1按位与,就能保证返回结果在0-length-1内
}

/**
 * 解决冲突:链地址法
 * d).  addEntry(hash, key, value, i)最终是调用了此函数
 */
void createEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];// index的Entry拿出来
    // put添加新元素是直接new Entry放在链头,如果有老的(有冲突)则将next设置为老的,如果没有正好设置next为null
    //(JDK 1.8中将新的冲突放在尾部)
    table[bucketIndex] = new Entry<>(hash, key, value, e);// 在构造函数中,e代表next
    size++;
}

4.取 get

//其实看完了最精髓的存,取的话就比较简单,就不放代码在这里了,仅说下思路。

// 1. 根据k使用hash(k)重新计算出hashCode
// 2. 根据indexFor(int h, int length)计算出该k的index
// 3. 如果该index处Entry的key与此k相等,就返回value,否则继续查看该Entry的next

四、其他

最近也有看到面试题,说为什么覆盖equals方法时必须覆盖hashCode()方法,下面来说下为什么这样。
Objectequals()默认是使用==来判断两个Object的引用地址的。
hashCode()native方法,它是直接依赖C来算出的。

如果不遵循上述规则呢?会造成使用Hash函数的数据类型出现错误,你就用不了哈希表这种数据结构了。

我们上面看过了HashMap源码,可以容易知道:

1.导致错误计算index,覆盖操作失效!
原因:
假设你将equals覆盖为name相等即相等(张三等于张三),不过你没覆盖hashCode();

put(key)操作,由于你的新key和HashMap中原有的老key是两个不同的对象,尽管他们equals,不过由于继承自Object的hashCode()方法给出了两个不同的hashCode,在根据hashCode计算出index这一步,它们两个属于不同的index!

这直接导致本该是一次覆盖的操作,却做成了新增了一个值的操作

所以,要避免出现这个问题,就必须在改写equals()的同时改写hashCode(),以保证对象equals则HashCode一致。
你可以看到官方提供的这些API类中,如果它需要覆盖equals()那么在同时也都覆盖了hashCode(),我们写class时也要这样。

猜你喜欢

转载自blog.csdn.net/u012895369/article/details/79735510
今日推荐