HashMap源码分析

来源:互联网 发布:葛优演技 知乎 编辑:程序博客网 时间:2024/06/10 11:39

由于最近出JavaSE的教程视频,录制到集合这里了,所以就写个博客吧,也顺便理清下思路。

好的,其实HashMap用一张图,稍微加点注释就能解释完的,但是,你要是这么说,别人肯定说你没水准~就像很多培训机构一样,讲解的知识点要录制一个多小时,中间还要讲个故事鼓励大家~发火

还需要两个方法,equals、hashcode,那么如果要是put,先根据hashcode找到数组中可以安放元素的位置,然后根据equals到链表里面判断有没有相同的值,如果

没有,那么将此元素放到第一位,如果有,将链表此节点的数据替换,并且返回被换了的值,哦,对了,如果数组这个位置上面没有的话,此元素就放到数组上面啦~

那么如果你要是面试的话,HashMap的优缺点说下数组、链表的优缺点就行了~

额,到此好像讲解完了~好吧,这样对不起观众~分析下源码吧,这样估计就能讲解个把小时了~大笑


咱们先看下JDK api对HashMap的解释:

基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

此实现假定哈希函数将元素适当地分布在各桶之间,可为基本操作(get  put)提供稳定的性能。迭代 collection 视图所需的时间与HashMap 实例的“容量”(桶的数量)及其大小(键-值映射关系数)成比例。所以,如果迭代性能很重要,则不要将初始容量设置得太高(或将加载因子设置得太低)。

HashMap 的实例有两个参数影响其性能:初始容量 加载因子容量 是哈希表中桶的数量,初始容量只是哈希表在创建时的容量。加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。

通常,默认加载因子 (.75) 在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数HashMap 类的操作中,包括 get  put 操作,都反映了这一点)。在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少 rehash 操作次数。如果初始容量大于最大条目数除以加载因子,则不会发生 rehash 操作。

如果很多映射关系要存储在 HashMap 实例中,则相对于按需执行自动的 rehash 操作以增大表的容量来说,使用足够大的初始容量创建它将使得映射关系能更有效地存储。

注意,此实现不是同步的。如果多个线程同时访问一个哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须 保持外部同步。(结构上的修改是指添加或删除一个或多个映射关系的任何操作;仅改变与实例已经包含的键关联的值不是结构上的修改。)这一般通过对自然封装该映射的对象进行同步操作来完成。如果不存在这样的对象,则应该使用 Collections.synchronizedMap 方法来“包装”该映射。最好在创建时完成这一操作,以防止对映射进行意外的非同步访问,如下所示:

   Map m = Collections.synchronizedMap(new HashMap(...));

由所有此类的“collection 视图方法”所返回的迭代器都是快速失败 的:在迭代器创建之后,如果从结构上对映射进行修改,除非通过迭代器本身的 remove 方法,其他任何时间任何方式的修改,迭代器都将抛出 ConcurrentModificationException。因此,面对并发的修改,迭代器很快就会完全失败,而不冒在将来不确定的时间发生任意不确定行为的风险。

注意,迭代器的快速失败行为不能得到保证,一般来说,存在非同步的并发修改时,不可能作出任何坚决的保证。快速失败迭代器尽最大努力抛出 ConcurrentModificationException。因此,编写依赖于此异常的程序的做法是错误的,正确做法是:迭代器的快速失败行为应该仅用于检测程序错误。



首先我们看看equals、hashcode方法:

//这里equals被设置成了final,就是为了防止别人重写~public final boolean equals(Object o) {           //判断是不是Entry,如果是Entry可以继续比较            if (!(o instanceof Map.Entry))//这里的Entry是个链表结构,是单向链表(单向就是里面只有next引用,没有pre引用)                return false;            Map.Entry e = (Map.Entry)o;            //分别取得两者的key值            Object k1 = getKey();            Object k2 = e.getKey();            //判断两个key的引用或者内容相等             if (k1 == k2 || (k1 != null && k1.equals(k2))) {                Object v1 = getValue();                Object v2 = e.getValue();                //判断两个key的值是否想的                if (v1 == v2 || (v1 != null && v1.equals(v2)))                    return true;            }            return false;        }

下面看下JDK api对equals的解释:就看中文吧,不装逼~

equalspublic boolean equals(Object obj)指示其他某个对象是否与此对象“相等”。equals 方法在非空对象引用上实现相等关系:自反性:对于任何非空引用值 x,x.equals(x) 都应返回 true。对称性:对于任何非空引用值 x 和 y,当且仅当 y.equals(x) 返回 true 时,x.equals(y) 才应返回 true。传递性:对于任何非空引用值 x、y 和 z,如果 x.equals(y) 返回 true,并且 y.equals(z) 返回 true,那么 x.equals(z) 应返回 true。一致性:对于任何非空引用值 x 和 y,多次调用 x.equals(y) 始终返回 true 或始终返回 false,前提是对象上 equals 比较中所用的信息没有被修改。对于任何非空引用值 x,x.equals(null) 都应返回 false。Object 类的 equals 方法实现对象上差别可能性最大的相等关系;即,对于任何非空引用值 x 和 y,当且仅当 x 和 y 引用同一个对象时,此方法才返回 true(x == y 具有值 true)。注意:当此方法被重写时,通常有必要重写 hashCode 方法,以维护 hashCode 方法的常规协定,该协定声明相等对象必须具有相等的哈希码。参数:obj - 要与之比较的引用对象。返回:如果此对象与 obj 参数相同,则返回 true;否则返回 false。




下面看看hashcode:


        public final int hashCode() {            return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());//通过key、value一起来位运算来获得hashCode值        }

下面是JDK api对hashcode的解释:

hashCodepublic int hashCode()返回该对象的哈希码值。支持此方法是为了提高哈希表(例如 java.util.Hashtable 提供的哈希表)的性能。hashCode 的常规协定是:在 Java 应用程序执行期间,在对同一对象多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是将对象进行 equals 比较时所用的信息没有被修改。从某一应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。如果根据 equals(Object) 方法,两个对象是相等的,那么对这两个对象中的每个对象调用 hashCode 方法都必须生成相同的整数结果。如果根据 equals(java.lang.Object) 方法,两个对象不相等,那么对这两个对象中的任一对象上调用 hashCode 方法不 要求一定生成不同的整数结果。但是,程序员应该意识到,为不相等的对象生成不同整数结果可以提高哈希表的性能。//全放数组里面肯定快啊,另外你删除也是很影响性能的~因为是数组啊实际上,由 Object 类定义的 hashCode 方法确实会针对不同的对象返回不同的整数。(这一般是通过将该对象的内部地址转换成一个整数来实现的,但是 JavaTM 编程语言不需要这种实现技巧。)返回:此对象的一个哈希码值。


至此,我们把概念都了解完了,下面开始了解了解具体的底层代码:

1、创建HashMap对象

2、put

3、get

4、rehash

先看看HashMap中的几个参数:

    /**     * 默认初始化容量,必须是2的倍数,默认是16个长度:1 << 4是位运算,可以查资料~     */    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16    /**     * 表示此HashMap最大容量是1<<30=1,073,741,824 这么长     */    static final int MAXIMUM_CAPACITY = 1 << 30;    /**     * 加载因子,当容量不够,就乘以这个倍数去扩容     */    static final float DEFAULT_LOAD_FACTOR = 0.75f;    /**     *  空数组     */    static final Entry<?,?>[] EMPTY_TABLE = {};    /**     * 是个链表类型的数组,初始化为空     */    transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;


1、看下HashMap的构造器:其他几个都调此构造器

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

至此,大家可以看到,如果你即使在新建一个HashMap对象制定一个容量长度,那么他/它给你的还是一个{}!!!


2、put

    public V put(K key, V value) {        //如果是空数组,那么新建一个数组        if (table == EMPTY_TABLE) {            inflateTable(threshold);        }        if (key == null)//如果key是null,那么执行存储一个key为null的Entry            return putForNullKey(value);        int hash = hash(key);//获取key的hash码        int i = indexFor(hash, table.length);//根据key的hash码来,此算法能保证key放在桶(即上面的数组)中的合适位置        for (Entry<K,V> e = table[i]; e != null; e = e.next) {//这里其实还有判断功能,如果key的hash获得的数组i位置不为空,那么在到链表里面找具体的key            Object k;            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {//如果找到的key相等,那么将此key的value替换掉,并且返回旧的value                V oldValue = e.value;                e.value = value;                e.recordAccess(this);                return oldValue;            }        }         modCount++;        //添加数据        addEntry(hash, key, value, i);        return null;    }

下面是添加数据的方法:addEntry

    void addEntry(int hash, K key, V value, int bucketIndex) {//bucketIndex:桶索引,也就是数组的索引        if ((size >= threshold) && (null != table[bucketIndex])) {//这段是又一次判断到底这个数组索引处是否有数据,如果已经有了从新计算bucketIndex桶索引            resize(2 * table.length);            hash = (null != key) ? hash(key) : 0;            bucketIndex = indexFor(hash, table.length);        }        //具体的添加数据方法        createEntry(hash, key, value, bucketIndex);    }

下面是createEntry方法:

    void createEntry(int hash, K key, V value, int bucketIndex) {        Entry<K,V> e = table[bucketIndex];//取出桶索引的当前元素,也就是数组的当前元素        table[bucketIndex] = new Entry<>(hash, key, value, e);//新建一个Entry放到当前位置,也就是将心得key:value生成的Entry放到链表的头位置        size++;    }
HashMap.Entry的构造器:
        Entry(int h, K k, V v, Entry<K,V> n) {            value = v;            next = n;//这里可以看到,将新的Entry放到了头链表头位置            key = k;            hash = h;        }

至此,put方法分析完成!看不明白的建议看源码!


3、get

    public V get(Object key) {        if (key == null)//如果是null,返回null对应的值            return getForNullKey();        Entry<K,V> entry = getEntry(key);        return null == entry ? null : entry.getValue();    }

    final Entry<K,V> getEntry(Object key) {        if (size == 0) {            return null;        }        //根据key获得hash,在根据hash获取桶的位置也就是数组的索引,找到那个元素,在此元素即链表上查找具体的key,然后执行判断        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;    }


至此get方法分析完。


4、rehash:也就是数组扩容了

    void resize(int newCapacity) {//容量是原来的2倍        Entry[] oldTable = table;        int oldCapacity = oldTable.length;        if (oldCapacity == MAXIMUM_CAPACITY) {            threshold = Integer.MAX_VALUE;            return;        }        Entry[] newTable = new Entry[newCapacity];//扩容是new了个新数组        transfer(newTable, initHashSeedAsNeeded(newCapacity));//这里执行拷贝操作        table = newTable;        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);    }


我们到现在可以知道,HashMap中最主要的是桶(数组)的概念,还有每个数组上面的链表

hash(key)来获取key的hash,indexFor(hash, table.length)获取hash的数组索引。


未完待续

需要了解的知识:(如果这些不了解以后研究JDK底层源码还是有点看不明白的)

1、二进制的负数、源码、补码、反码

2、位移<<、>>、>>>运算

3、Java中的与(&)、或(|)、  非(~ )  、 异或(^)      

4、二进制、八进制、十进制、十六进制之间互相转换


0 0
原创粉丝点击