HashMap的源码理解

HashMap是常用的集合。采用键值对方式存储.   此博客是基于jdk1.8分析的。

  一:先看看HashMap的继承关系:

public class HashMap extends AbstractMap
    implements Map, Cloneable, Serializable {

   1.继承AbstractMap,然后实现了Map.其实AbstractMap也是实现了大部分的Map方法,其他Map的实现类只需要继承       AbstractMap然后实现少量的方法即可。 

   2.实现Cloneable可以被克隆,实现Serializable接口可以被序列化。

二:说下HashMap的大致框架。

     HashMap的主干是一个Map.Entry数组(jdk1.8和jdk1.7形式上有些不同,jdk1.8采用Node)。

transient Node[] table;

Map.Entry对象:

static class Node implements Map.Entry {
        final int hash;//hash值
        final K key;//节点的key值
        V value;//节点的value值
        Node next;//此节点连接的下一个节点(解决hash冲突)
    //省略部分代码,下面的代码是 get,set方法,equals以及toString方法
}

然后根据hash算法来确定每  个Map.Entry对象存放的位置。如果不同的对象的hash值一样,这就造成了hash冲突(不能将多个对象放置在同一个数组位置),此时可以采用链表结构,即相同的hash值的Map.Entry对象接在该Map.Entry对象后面。

整体结构(借用下大神的图):

HashMap的源码理解_第1张图片

三:几个关键参数以及构造方法。

//默认的初始容量 
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大容量
 static final int MAXIMUM_CAPACITY = 1 << 30;//1073741824
//默认的填充因子
 static final float DEFAULT_LOAD_FACTOR = 0.75f;

transient Node[] table;//HashMap中的'主干数组'

transient int size;//HashMap中实际存储了多少个对象

transient int modCount;//HashMap对象结构修改的次数,包括增,删。用于fail-fast机制。和ArrayList 
                       //LinkedList类似。

 int threshold;//最开始是用作初始化HashMap的数组,后面用来判断是否扩容。
               //threshold=capacity * loadfactor  即容量*填充因子

final float loadFactor;//实际的填充因子参数

构造方法:

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);//返回一个2的幂次方数,便于hash算法
    }

public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

 public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
//由于构造方法没有给‘主干数组’赋值,即此时该HashMap还没有完全初始化。真正给map的数组赋值是在
//put方法中.下面给予介绍

 

四:介绍常用的方法,put(K k,V v)

  put(K k,V v).先说下大概流程吧:首先hash(k)来获取hash值。然后通过(n-1)&hash值得到Map数组的下标。将该值放置在该地方。如果已经存在值了,就接在该值后面。

代码:

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

然后看看 hash方法:

static final int hash(Object key) {
        int h;
      //key 为null的hash值为0 
      //调用key 类型的hashCode方法 然后和该(hashCode值无符号右移16位后)进行或运算
      //二进制运算了解:https://blog.csdn.net/echohuangshihuxue/article/details/86182908
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

再看看核心逻辑 putVal:其中求数组下标:hash&(n-1)。本质上和hash%n 算法是一样的。

   1.hash%(n-1)很好理解。即得到的数据为(0 到n-1之间)。即数组的下标。不会越界。

   2.hash&(n-1)的效率比hash%n的效率高。hash%n最终还是要转换为二进制进行计算。

   3.为啥hash&(n-1)和 hash%n一样呢。二进制可以参考:https://blog.csdn.net/echohuangshihuxue/article/details/86182908

    首先的理解&运算。即都为1才为1.而n是2的幂次方。二进制数形式为: 0000....1..0000.

                                                                                  n-1的二进制形式为: 0000....0..1111.

   即hash&(n-1)的结果为                                          0000.....0..1111   

                                                                                         &

                                                                                 0101.....1..0101

其结果必定小于等于n-1.如果(n-1)和hash相等,那么&的结果就是n-1.其他情况都小于n-1.并且n-1的二进制有效位全是1.所以

和hash进行&运算。前面的全是0.后面的结果,hash的结果是啥 &的结果就是啥。因为0或者1与1进行&运算。结果有前面的数决定。所以n-1与hash值进行&运算得的结果为  hash-(Math.floor(hash/n))  *n.其中带红色的运算是取 hash/n的最接近的整数。比如hash/n 为2.1. 那么整个值为2. 其实上面的表达式就是   hash%n了。

 

可能我表达的不太好。别人还没有理解。最开始我就是自己做了很多测试。最终发现确实是这样的      

/**
	 * 前提是s为2的幂次方。
	 * 
	 * 测试 one%s  和 one &(s-1) 是等值的
	 * 经测试,确实是等值的
	 */
	@Test
	public void testOne(){
		int one=187832123;
		int s=2<<4;//将2的幂次方 进行左移或者右移 得到的依然是2的幂次方    
		           //二进制可以参考:https://blog.csdn.net/echohuangshihuxue/article/details/86182908
		int res_one=one%s;
		
		int res_two=one&(s-1);
		System.out.println(res_one);//27
		System.out.println(res_two);//27
	}
	

putVal方法:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node[] tab; Node p; int n, i;//没有对下面的table进行操作,基本都是用tab进 
                                               //行替代操作。
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;//如果数组为空,resize()即给Map的数组赋值。真正完成 
                                         //初始化。
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);//如果没有发生hash冲突,直接插入
        else {
            Node e; K k;                       //如果发生了hash冲突,即接在后面,修改节 
                                                    //点的next
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)    //关于TreeNode现在暂时没有深入了解
                e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // .如果已存在了key.那么覆盖,返 
                             //回旧的值
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;               //增加了修改次数,用友 fail-fast机制
        if (++size > threshold)
            resize();             //进行扩容操作
        afterNodeInsertion(evict);//属于节点为TreeNode类型(需要修改结构)。现在不考虑
        return null;
    }

五:常用方法,get(Object key)

 public V get(Object key) {
        Node e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

hash方法之前看过了。主要逻辑就在getNode里面。

 final Node getNode(int hash, Object key) {
        Node[] tab; Node first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            //用hash值 和(n-1)进行&运算  的值作为Map的数组下标值
            (first = tab[(n - 1) & hash]) != null) { //如果存在。
            if (first.hash == hash &&  //首先比较第一个 通过equals方法
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {//如果第一个比较失败,然后比较下一个
                if (first instanceof TreeNode)//Node对象为TreeNode类型暂不考虑
                    return ((TreeNode)first).getTreeNode(hash, key);
                do { //遍历这个数组下标下的节点。找到返回,没有返回null
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

 

总结: 1.HashMap的get,即查找方法。是首先根据hash()方法计算hash值,然后用(n-1)&hash值得到HashMap主数组的下                    标,  这里的n为数组的长度。 然后在该数组对应的地方进行equals方法比对链表值(Node).如果equals方法相等,则证               明找到。

           所以我们重写HashMap的hash方法是,一定得重写equals方法。并且保证:key的hash值相等时,equals的值必须为                   true;

         2.HashMap的key不能重复。因为在put方法中,首先根据key的hash方法得到hash值,再根据(n-1)&hash来查找数组的                 下标。如果key值一样,那么其hash值一样,并且equals方法为true.最终会进行覆盖。所以不会存在相同的key值。

         3.如何理解hashMap的效率高:

           比如集合中存了1000个对象。LInkedList的集合的get(int one)方法,需要用equals方法一个个去比对。可能比较500次(可以看LinkedList源码)。  而HashMap不同,hashMap首先会计算hash值。找到对应的数组下标。如果该下标下有50个对象。则只需用equals方法比较50次。数组根据下标求值消耗忽略不算。  如果hash算法以及填充因子(默认0.75)设置的好。效率会更高。

        4.后续再看看为hash()方法。

 

 

 

 

 

 

 

 

 

 

 

 

下面的部分杂谈,哈哈。

一:可能你用hashMap的时候,就是Map map=new HashMap();这样创建一个Map对象,然后使用map.put(),然后,map.get()。其实我们我可以进一步想想,map.put(one,two)方法是如何判断one的唯一性的(即one不能重复)。

一个最原始的办法,就是拿这个one去和map的key一个个去比较,一般是通过equals比较。如果是数量很多的话,可能你put一个元素都要很大的开销。同样的道理,你get一个元素也要花很长的时间。所以就需要算法来优化了。

二:那HashMap是怎么优化的呢。这里我用自己的话尽量说通俗点吧。

    首先HashMap在内存分为m个部分,用int(散列码)数组标识arr。比如arr[x]代表a部分。每个部分存储着若干个key.

   现在,当我们put一个元素时,此时key对象会调用自己的hashcode方法,生成一个int数字,该int数字即匹配到上面所说的数组的下标,然后就找到对应的内存区域(如果匹配不上,即hashcode生成的int数字对应的数组中内存没有对象,就把该key,value put进去),此时key就和该部分的对象进行equals比对。如果返回true,说明key对象存在,不能put进去。否则就Put进去。

  这里说简单说下,原来需要用key对象和hashMap中所有的对象比较,现在只需比较一部分。比如HashMap中有10000个对象,原来需要比较10000,现在就只需比较50次,中间的数组底层会有些消耗,但这样还是会有很大的性能提升。

三:说到这里,可能有点迷糊。打个不恰当的比方:现在这个书房有10000本书,你需要知道这个书房中是否有笑傲江湖这本小说。如果有,就不把这本书放进去,如果没有,就将这本书放进去。如果书是毫无头绪放着,那么你就只能一本一本去看找了。现在如果你对书进行了整理分类,都贴了标签。你就可以直接找到小说类。然后一本本比较就得了。是不是快多了。。。。。。

   其实标签就好比HashMap中的数组。现在重点来了,你怎么知道这本书是小说类?这就提到 了hash算法了。

四:其实hashCode和equals方法是Object就有的,说明任何对象都可以重写hashCode和equals方法。说明设计者早就想好了,伙计们,当你需要 将对象(我指的是key值)put到HashMap中时, 就要将该对象重写hashCode和equals方法。不然HashMap的key唯一性就会出错。现在大伙可能更加迷糊了。我明明没有重写什么hashcode和equals方法,怎么在使用HashMap的时候,什么问题都没有呢,哈哈,其实我们一一般是将String或者Integer对象作为key.而这两个类已经重写了hashCode和equals方法了。所以当然没问题了。说了这么多,好像跑题了。

五:回归到正题,如何在大量数据里判断是否有对象one,HashMap的做法是首先根据hashCode方法判断(靠hashCode方法生成散      列码,和底层的数字查询该内存是否有对象。如果存在,就再采取equals方法比较),这里还涉及到一个逻辑问题。就是一个对象和HashMap对象的equals方法为true时,hashcode得到的散列码一定在底层数组中有对象。反过来不一定对(通俗点说就是这个房间有一本书叫笑傲江湖,那么这个书房一定会有个地方是放小说的)。

六.来看下HashMap的get方法。

 public V get(Object key) {
        if (key == null)
            return getForNullKey();
        Entry entry = getEntry(key);

        return null == entry ? null : entry.getValue();
    }

 final Entry getEntry(Object key) {
        if (size == 0) {
            return null;
        }

        int hash = (key == null) ? 0 : hash(key);
        for (Entry 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(实际上是调用key对象的hashCode方法)。比较hash值。当hash值不相等,就不用比较equasl方法了,当hash值相等且equals为true,才会得到value。大概意思就是这样。如果要深究,可以读读Thinking in java--容器深入研究(21章)。感兴趣的还可以看看String的hashCode方法。

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

至于为什么h=31*h+val[i]这里是31,这就是数学上的问题了。就到这里。如有不对,多谢指正。

 

 

 

 

 

 

你可能感兴趣的:(集合)