Java你可能不知道的事(3)HashMap

概述

HashMap对于作Java的小伙伴来讲太熟悉了。估计大家天天都在使用它。它为何叫作HashMap?它的内部是怎么实现的呢?为何咱们使用的时候不少状况都是用String做为它的key呢?带着这些疑问让咱们来了解HashMap!java

HashMap介绍

一、介绍

HashMap是一个用”KEY”-“VALUE”来实现数据存储的类。你能够用一个”key”去存储数据。当你想得到数据的时候,你能够经过”key”去获得数据。因此你能够把HashMap看成一个字典。 那么HashMap的名字从何而来呢?其实HashMap的由来是基于Hasing技术(Hasing),Hasing就是将很大的字符串或者任何对象转换成一个用来表明它们的很小的值,这些更短的值就能够很方便的用来方便索引、加快搜索。数组

在讲解HashMap的存储过程以前还须要提到一个知识点
咱们都知道在Java中每一个对象都有一个hashcode()方法用来返回该对象的 hash值。HashMap中将会用到对象的hashcode方法来获取对象的hash值。缓存

二、关系

图1展现了HashMap的类结构关系。ide

图1

HashMap继承了AbstractMap,而且支持序列化和反序列化。因为实现了Clonable接口,也就支持clone()方法来复制一个对象。今天主要说HashMap的内部实现,这里就不对序列化和clone作讲解了。函数

三、内部介绍

HashMap内部实现原理图

上面的图很清晰的说明了HashMap内部的实现原理。就比如一个篮子,篮子里装了不少苹果,苹果里包含了本身的信息和另一个苹果的引用this

一、和上图显示的同样,HashMap内部包含了一个Entry类型的数组table, table里的每个数据都是一个Entry对象。.net

二、再来看table里面存储的Entry类型,Entry类里包含了hashcode变量,key,value 和另一个Entry对象。为何要有一个Entry对象呢?其实若是你看过linkedList的源码,你可能会知道这就是一个链表结构。经过我找到你,你再找到他。不过这里的Entry并非LinkedList,它是单独为HashMap服务的一个内部单链表结构的类。code

三、那么Entry是一个单链表结构的意义又是什么呢?在咱们了解了HashMap的存储过程以后,你就会很清楚了,接着让咱们来看HashMap怎么工做的。对象

HashMap的存储过程

下面分析一段代码的HashMap存储过程。(这里只是做为演示的例子,并无真实的去取到了Hash值,若是你有须要能够经过Debug来获得key的Hash值)blog

   HashMap hashMap = new HashMap();//line1
   hashMap.put("one","hello1");//line2
   hashMap.put("two","hello2");//line3
   hashMap.put("three","hello3");//line4
   hashMap.put("four","hello4");//line5
   hashMap.put("five","hello5");//line6h
   ashMap.put("six","hello6");//line7
   hashMap.put("seven","hello7");//line8

put操做的伪代码能够表示以下:

public V put(K key, V value){
    int hash = hash(key);
    int i = indexFor(hash, table.length);
    //在table[i]的地方添加一个包含hash,key,value信息的Entry类。
    
   }

下面咱们来看上面代码的过程
一、line1建立了一个HashMap,因此咱们来看构造函数

/** * Constructs an empty <tt>HashMap</tt> with the default initial capacity 
* (16) and the default load factor (0.75).*/
 public HashMap() {this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    
 }

空构造函数调用了它本身的另外一个构造函数,注释说明了构建了一个初始容量的空HashMap,那咱们就来看它另一个构造函数。

public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;i
            f (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        threshold = initialCapacity;
        init();
    }void init() {
    }

上面的代码只是简单的给loadFactor(实际上是数组不够用来扩容的)和threshold(内部数组的初始化容量),init()是一个空方法。因此如今数组table仍是一个空数组。

 /** * An empty table instance to share when the table is not inflated. */
 
     static final Entry<?,?>[] EMPTY_TABLE = {};
     
 /** * The table, resized as necessary. Length MUST Always be a power of two. */
     transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

二、接下来到了line2的地方, hashMap.put(“one”,”hello1”);在这里先提一下put方法源码:

public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);//若是是空的,加载
            }
        if (key == null)
        return putForNullKey(value);
        
        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;//遍历已存在的Entry,若是要存入的key和hash值都同样就覆盖。
                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;
    }

源码很简单,先判断table若是是空的,就初始化数组table,接着若是key是null就单独处理。不然的话就获得key的hash值再生成索引,这里用了indexFor()方法生成索引是由于:hash值通常都很大,是不适合咱们的数组的。来看indexFor方法

/** * Returns index for hash code h. */
    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);
    }

就是一个&操做,这样返回的值比较小适合咱们的数组。

继续 line2put操做,由于开始table是空数组,因此会进入 inflateTable(threshold)方法,其实这个方法就是出实话数组容量,初始化长度是16,这个长度是在开始的构造方法赋值的。
因此,如今空数组变成了长度16的数组了,就像下图同样。
这里写图片描述

接着因为咱们的key不为null,到了获取hash值和索引,这里假设int hash = hash(key)和int i = indexFor(hash, table.length)生成的索引i为hash=2306996,i = 4;那么就会在table索引为4的位置新建一个Entry,对应的代码是addEntry(hash, key, value, i);到此结果以下图:
这里写图片描述

新建的Entry内部的变量分别是,hash,key,value,和指向下一节点的next Entry。

三、继续来看line3,line3和line2同样,并且数组不为空直接hash(key)和index。因此直接看图了
这里写图片描述

四、到了line4,这里line4状况有点特殊,咱们假设line4里key生成的hashcode产生的index也为4,好比hash(“three”) 的值 63281940
hash&(15)产生的index为4。这种状况因为以前的位置已经有Entry了,因此遍历Entry若是key和hashcode都相同,就直接替换,不然新添加一个Entry,来看一下对应源码

public V put(K key, V value) {
        ...//一些代码
        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))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);return oldValue;
            }
        }//for循环里判断若是hash和key都同样直接替换。
        modCount++;
        addEntry(hash, key, value, i);//没有重复的话就addEntry
        return null;
    }

上面代码先判断是否须要替换,不须要就调用了addEntry方法。来看addEntry

void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[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);
        size++;//获取当前节点,而后新建一个含有当前hash,key,value信息的一个节点,而且该节点的Entry指向了前一个Entry并赋值给table[index],成为了最新的节点Entry,同时将size加1。}

到这里相信你们很清楚了。来看看图:
这里写图片描述

五、到这里以后的代码都在上面的分析状况当中。我就不一一画图了,直接给出程序执行到最后的图
line5到line8

代码 hashcode index key value next
hashMap.put(“four”,”hello4”); 54378290 9 four hello4 null
hashMap.put(“five”,”hello5”); 39821723 8 five hello5 null
hashMap.put(“six”,”hello6”); 86726537 4 six hello6 line4产生的Entry
hashMap.put(“seven”,”hello7”); 28789082 2 seven hello7 line3产生的Entry

结果图以下:
这里写图片描述

到此put 操做就结束了,再来看看取

HashMap的取值过程

咱们经过hashMap.get(K key) 来获取存入的值,key的取值很简单了。咱们经过数组的index直接找到Entry,而后再遍历Entry,当hashcode和key都同样就是咱们当初存入的值啦。看源码:

 public V get(Object key) {
        if (key == null)
        return getForNullKey();
        Entry<K,V> entry = getEntry(key);
        return null == entry ? null : entry.getValue();
    }

调用getEntry(key)拿到entry ,而后返回entry的value,来看getEntry(key)方法

final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }
        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;
    }

按什么规则存的就按什么规则取,获取到hash,再获取index,而后拿到Entry遍历,hash相等的状况下,若是key相等就知道了咱们想要的值。

再get方法中有null的判断,null取hash值老是0,再getNullKey(K key)方法中,也是按照遍历方法来查找的。

到这你确定明白了为何HashMap能够用null作key。

了解的存储取值过程和内部实现,其它的方法本身看看源码很好理解,在此就不一一解释了。

几个问题

问题一、HashMap是基于key的hashcode的存储的,若是两个不一样的key产生的hashcode同样取值怎么办?
看了上面的分析,你确定知道,再数组里面有链表结构的Entry来实现,经过遍历全部的Entry,比较key来肯定究竟是哪个value;

问题二、HashMap是基于key的hashcode的存储的,若是两个key同样产生的hashcode同样怎么办?
在put操做的时候会遍历全部Entry,若是有key相等的则替换。因此get的时候只会有一个

问题三、咱们老是习惯用一个String做为HashMap的key,这是为何呢?其它的类能够作为HashMap的key吗?
这里由于String是不能够变的,而且java为它实现了hashcode的缓存技术。咱们在put和get中都须要获取key的hashcode,这些方法的效率很大程度上取决于获取hashcode的,因此用String的缘由:一、它是不可变的。二、它实现了hashcode的缓存,效率更高。若是你对String不了解能够看:Java你可能不知道的事-String

问题四、可变的对象能做为HashMap的key吗?
可变的对象是能够当作HashMap的key的,只是你要确保你可变变量的改变不会改变hashcode。好比如下代码

public class TestMemory {public static void main(String[] args) {
        HashMap hashMap = new HashMap();
        TestKey testKey = new TestKey();
        testKey.setAddress("sdfdsf");//line3hashMap.put(testKey,"hello");
        testKey.setAddress("sdfsdffds");//line5System.out.println(hashMap.get(testKey));
    }
}

public class TestKey {
    String name;
    String address;public String getName() {return name;
    }
    
    public void setName(String name) {this.name = name;
    }
    
    public String getAddress() {return address;
    }
    
    public void setAddress(String address) {this.address = address;
    }

    @Override
    public int hashCode() {
    if (name==null){return 0;
    }
       return name.hashCode();
    }
}

上面的代码line3到line5对象里的address作了改变,可是因为hashCode是基于name来生成的,name没变,因此依然可以正常找到value。可是若是把setAdress换成name,get就会返回null。这就是为何咱们选择String的缘由。

到这里相信你对HashMap内部已经很是清楚了,若是本篇文章对你有帮助记得点赞和评论,或者关注我,我会继续更新文章,感谢支持!

相关文章
相关标签/搜索