HashMap的原理和源码深度剖析 jdk1.7版本

1.1 hashMap的类注释

从Idea或者Eclipse编译器new 一个HashMap,按住ctrl键将鼠标放在HashMap上,点击进去,就能看到源码了,如果是Eclipse,需要导入源码文件。

我们先从类注释看起,这几段话十分重要。翻译如下:

    1.哈希表是基于实现Map接口的,该实现提供了所有关于Map的操作,并且允许key为null和value为null(HashMap和HashTable几乎一样,区别在于,HashMap不保证线程安全,允许null值)。该实现类(指HashMap)不保证map集合的顺序,更为重要的是,该实现也不能保证随着时间的变化map集合的顺序保持不变(不看注释都不知道还会这样,恐怖)。

       2.该实现为put和get基本操作提供了恒定的时间性能,假设散列函数在桶(指存储的位置)之间正确地分散元素。集合视图上的迭代需要的时间(即遍历的性能)与HashMap实例的“容量”(桶的数量,即HashMap的构造函数传入的一个参数)加上其大小(键值对映射的数量)成比例。所以如果要求迭代性能的话,尽量不要设置过大的初始化容量大小(或者太小的负载因子,负载因子会影响HashMap的自动扩容,可以通过HashMap的构造函数传参设置)。

      3.HashMap的性能受到两个参数的影响:初始容量和负载因子。容量的大小对应哈希表的桶的数量,初始化容量就是哈希表被创建的时候的桶的数量。负载因子决定在实例在自动扩容之前,衡量实例总共能存放多少空间,也就是当存储容量到达负载因子设置的那个【有多满】的度,就会扩容。当哈希表中的   entry数量>负载因子*当前容量的乘积,就是指当前哈希表占用容量除以当前哈希表的容量大于负载因子时,哈希表会重新配置,重建内部数据结构,以确保哈希表的容量是桶的数量的2倍。

      4.基本来说,默认的负载因子0.75,基本做到了时间和空间开销上的均衡。显然,较大的负载因子在空间开销上会更少,但是会增加搜索查询上的开销(反映在大部分操作,包括put和get,这是为何呢?)。所以在设置初始容量的时候,要充分考虑负载因子和entry数量的影响,以减少HashMap重新扩容的次数,提供设备性能。如果初始容量设置大于最大的entry数量除以负载因子的值,那么HashMap 永远都不会重新扩容。

      5.如果HashMap有非常多的键值对映射,那么创建一个足够大的容量的HashMap,可以使这些映射存储的效率高于当容量不够时自动去扩容。需要注意的是,当存储过多的哈希值一样的key,会降低哈希表的性能。为了改善此影响,当key是可比较的,可调用compareTo()方法,可以使用key之间的比较顺序来帮助打破关系。(不太懂。。)

       6.强调一下,该实现是非线程安全的。如果有多线程并发访问,并且至少有一个线程修改了Map结构,那么必须在外部进行同步(删除或者增加映射的操作都会修改Map结构,但仅仅修改key对应的value值不会改变Map结构),通常是将Map封装到一个同步的类中。

       7.如果没有同步的类,可以使用线程同步的包装类,Collections.synchronizedMap,最好在创建的时候完成,防止意外的不同步线程的访问。

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

        8.迭代的时候,返回所有的该集合的视图方法,迭代是<fail-fast>模式:当迭代进行的时候,在任何时候如果Map结构发生改变,除非通过iterator自己的remove方法删除的元素,都会抛出异常:ConcurrentModificationException。因此,在面对并发修改的时候,迭代直接干净利索的执行失败,而不用在未来某个时刻冒着非确定性的风险。

        9.注意:迭代的<fail-fast>模式无法得到保证,因为通常来说,不可能在存在不同步的并发修改的时候做出硬性保证。该模式尽力抛出异常,所以,如果一个程序通过该异常来保证程序的正确性,那么这可能是错误的做法。该异常应该仅应用于检测Bug.

从注释就可以总结到很多知识点,而且是官方的具有权威性,不用担心去看别人的博客把你带上歪路:

    1.HashMap无序 允许null值    2.HashMap可自动扩容  3.HashMap是线程不安全的  4.HashMap 在迭代(遍历)的时候不能增删元素,可以修改已存在的value值,否则抛异常   5.HashMap的性能影响因素


1.2 继承和实现

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable

K:集合维护的key的类型    V:key映射的值的类型

HashMap继承AbstractMap 并且实现了Map,查看AbstractMap发现它是一个抽象类而且也实现了Map,那么HashMap仅仅继承AbstractMap就可以了啊,为什么还要实现Map呢?这么做是不是有些多余呢?吃瓜群众可以参考这篇文章

HashMap实现了Serializable接口,说明其可以被序列化和反序列化,可以作为参数实现RPC远程调用。

实现Cloneable接口,允许HashMap调用clone()方法,复制对象。另外笔者在测试clone()方法的时候发现,HashMap的父类重写了equals方法,若HashMap中的所有的键值对都相等(使用equals方法判断),那么两个集合也是相等的。

1.3 常用成员变量

    //默认容量大小 16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    //最大容量  2的30次方:1073741824 10个亿...
    static final int MAXIMUM_CAPACITY = 1 << 30;
    //默认的负载因子 0.75
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    //存放映射的数组 transient关键字用于不被序列化
    transient HashMap.Entry<K, V>[] table;
    //集合大小
    transient int size;
    //映射的Set集合 元素是entry数组
    private transient Set<java.util.Map.Entry<K, V>> entrySet;

1.4 构造函数 4个

    //最常用构造  16容量 0.75负载因子
    public HashMap() {
        this(16, 0.75F);
    }
    //常用构造  指定初始容量
    public HashMap(int initialCapacity) {
        this(var1, 0.75F);
    }
    //指定初始容量和负载因子的构造函数
    //如果对HashMap不是充分了解,建议还是不要动负载因子
    public HashMap(int initialCapacity, float loadFactor) {....}
    //通过已存在的集合构造
    public HashMap(Map<? extends K, ? extends V> m) {
        this(Math.max((int)((float)m.size() / 0.75F) + 1, 16), 0.75F);
        this.putAllForCreate(var1);
    }

前两个个构造函数都是通过调用第三个构造函数执行的,这里面做了什么呢?

构造函数为table成员变量赋值为指定大小的Entry数组,用于存储键值对

public HashMap(int 容量, float 负载因子) { // jdk1.7的源码  和1.8的代码和这个不一样
        this.hashSeed = Hashing.randomHashSeed(this);//设置哈希种子
        this.entrySet = null;//成员变量 private transient Set<java.util.Map.Entry<K, V>> entrySet;
        if (容量 < 0) {// 1非负判断
            throw new IllegalArgumentException("Illegal initial capacity: " + 容量);
        } else {
            if (容量 > 1073741824) {// 2.最大值判断  超值了取最大值
                容量 = 1073741824;
            }
            if (负载因子 > 0.0F && !Float.isNaN(负载因子)) {//3.负载因子判断
                int var3;
                for(var3 = 1; var3 < 容量; var3 <<= 1) {//此时容量默认等于16  var3用于设置table数组的初始大小
                    ;       //var3 <<= 1 是var3=var<<1,也就是每次乘以2,当2的n次方<容量,n+1次方大于容量时,var 3=2的n次方
                }

                this.loadFactor = 负载因子;// 4.设置负载因子
                this.threshold = (int)Math.min((float)var3 * 负载因子, 1.07374182E9F); // 在1.7中会用到
                this.table = new HashMap.Entry[var3];// 5.新建一个entry数组,初始容量为var3 这里默认等于16  
                this.useAltHashing = VM.isBooted() && var3 >= HashMap.Holder.ALTERNATIVE_HASHING_THRESHOLD;
                this.init();//init()方法没有代码
            } else {
                throw new IllegalArgumentException("Illegal load factor: " + 负载因子);
            }
        }
    }

1.5 进入Entry一探究竟

从成员变量,HashMap.Entry<K, V>[] table,可以看出Entry是HashMap的一个内部类,table变量实际就是Entry[],和int[]没什么区别,就是一个数组,不过加了个括号<K,V>。后续HashMap的put get 操作都是基于table来实现的,都是存储在entry中。我们现在进入该内部类去看看,贴出的是重要的代码;

static class Entry<K, V> implements java.util.Map.Entry<K, V> {//该内部类又实现了Map接口的内部类Entry,有5个方法,getkey getvalue setvalue equals hashcode
	final K key; //key 被定义为final类型,不可改变key的值
        V value;   //映射的value值
        HashMap.Entry<K, V> next; //在Entry内部类中又定义了一个Entry的成员变量,作为指针使用
        int hash;  //哈希值
	Entry(int var1, K var2, V var3, HashMap.Entry<K, V> var4) {//通过构造方法,给成员变量赋值
            this.value = var3;
            this.next = var4;
            this.key = var2;
            this.hash = var1;
        }
	public final K getKey() { return this.key; }

        public final V getValue() { return this.value;  }

        public final V setValue(V var1) {// Map的set方法返回被覆盖前的value值的原理,就是通过下面3句代码
            Object var2 = this.value;
            this.value = var1;
            return var2;
        }
	public final int hashCode() {//重写hashCode方法
            return (this.key == null ? 0 : this.key.hashCode()) ^ (this.value == null ? 0 : this.value.hashCode());
        }
        public final String toString() {//重写toString方法
            return this.getKey() + "=" + this.getValue();
        }
	 public final boolean equals(Object var1) {....}//重写equals方法
}

1.6 put 方法的原理

假设put前集合是空的,那么 put最终是通过addEntry方法 存储键值对的

public V put(K 键值, V 映射值) {
        if (键值 == null) { // 1.允许键值为Null 为null的时候 调用putForNullKey方法
            return this.putForNullKey(映射值);
        } else {
            int 哈希值= this.hash(键值); // 通过HashMap的hash方法计算出key的哈希值。
            int 插入下标位置 = indexFor(哈希值, this.table.length);// 2.通过indexFor方法算出 该哈希值应该在数组的哪个下标位置
	   /**
	    *this.table[插入下标位置],如果该位置是空的,就没有Entry对象
	    *否则,会循环查找Entry和Entry中的next变量的hash值, 判断是否和put进来的key值的哈希值一样,
	    *如果一样,再判断key的equals方法和==方法,还是一样的话,就覆盖之前的数据
	    */
            for(HashMap.Entry var5 = this.table[插入下标位置]; var5 != null; var5 = var5.next) { 
                if (var5.hash == 哈希值) {// 3.当哈希值一样的时候还需要再比较 equals方法
                    Object var6 = var5.key;
                    if (var5.key == 键值 || 键值.equals(var6)) {// 4.这里的equals方法是键值自己的equals方法,比较两个key是否相等
                        Object var7 = var5.value;    // 5.取出当前的key对应的value值
                        var5.value = 映射值;            // 6.覆盖value为最新put进来的映射值
                        var5.recordAccess(this);        // 这个recordAccess方法内没有代码
                        return var7;  // 返回被覆盖的value值  
                    }
                }
            }
            ++this.modCount;//修改次数的增加
            this.addEntry(哈希值, 键值, 映射值, 插入下标位置);//  需要进入addEntry方法一探究竟
            return null;
        }
    }

1.7 真正存储数据的方法  addEntry()

 void addEntry(int var1, K var2, V var3, int var4) {
        if (this.size >= this.threshold && null != this.table[var4]) {  // 刚刚初始化的map的size属性是0 在1.4中 threshold的值在初始化的时候已经指定,必然大于0
            this.resize(2 * this.table.length);    //该判断是为了扩容,所以暂时不考虑
            var1 = null != var2 ? this.hash(var2) : 0;  //扩容后 哈希值和 下标值都要重新判断获取
            var4 = indexFor(var1, this.table.length);
        }

        this.createEntry(var1, var2, var3, var4);// 参数不发生改变
    }

creatEntry()方法,重点在这一块,体现next模拟指针的用法,原理是:创建一个新的Entry对象var,如果该数组位置上已经有Entry对象(var5)了,那么将已存在的var5 通过Entry的构造函数赋值给var 的next成员变量中,然后将该位置让给var 。这样就形成了一个链表,var中的next指向var5,var 5中的next指向var6......如此循环下去,一条常常的链表就有了

void createEntry(int var1, K var2, V var3, int var4) {
        HashMap.Entry var5 = this.table[var4];   //初始化情况下,数组内的所有元素都是Null 。不是null的时候,说明此位置已经有Entry对象了
        this.table[var4] = new HashMap.Entry(var1, var2, var3, var5); // var5作为当前new出来的Entry的next对象(请查看1.5中的Entry构造函数)
        ++this.size;  //集合的大小 自增
    }
如此,我们便总结出了HashMap的存储结构和Put方法的实现原理,画图来看一下。



1.8 get方法原理

 public V get(Object var1) {    //根据key值获取value
        if (var1 == null) {
            return this.getForNullKey();  //如果key是null的时候,从getForNullKey()方法获取
        } else {
            HashMap.Entry var2 = this.getEntry(var1);   //否则 调用getEntry()方法获取value  我们去看一下这个方法是如何写的
            return null == var2 ? null : var2.getValue();
        }
    }

final HashMap.Entry<K, V> getEntry(Object var1) {
        int var2 = var1 == null ? 0 : this.hash(var1); //这里var1不会再等于null了除非发生并发修改  获取哈希值决定数组的下标位置

        for(HashMap.Entry var3 = this.table[indexFor(var2, this.table.length)]; var3 != null; var3 = var3.next) {
            if (var3.hash == var2) {   //逐步遍历比较是否存在该key,然后取出对应的value值
                Object var4 = var3.key;
                if (var3.key == var1 || var1 != null && var1.equals(var4)) {
                    return var3;
                }
            }
        }

        return null;
    }
如果对Put方法看懂了,那么get方法真是太简单了,首先根据key的哈希值定位到数组的下标位置,获取Entry对象,如果该位置是空的,直接返回Null。否则,遍历entry对象,直到找到与查询的key值相等的已存在的key,取出value并返回,否则返回Null

对于不对的地方还请大家指出,共同进步,其他的方法大家自己研究去吧,本案例暂时只介绍这么多哈!




猜你喜欢

转载自blog.csdn.net/YAO_IT/article/details/80584053