一、HashMap的原理
所谓Map,就是关联数组,存的是键值对——key&value。
实现一个简单的Map,你也许会直接用两个LIst,一个存key,一个存value。然后做查询或者get的时候,就遍历key的list,然后返回相应的value。
这样时间复杂度显然就是线性的,但这在map中已经是效率最低的get的方法了。而Hash主要提高效率的,也就是在这个位置——key的定位和查询这。
在数据结构中,我们学了hash这一技术,也就是散列表的技术。我们把整个表格看作是许多许多的空桶,然后散列函数也就是hash函数(拿质数来取模是一个很经典的hash的方法)会把你传入的参数处理后,散列到这些桶中。一个完美的哈希函数呢,是可以将你的输入无冲突的散列到表格中,也就是你传入的参数一人进一个桶,互相之间不冲突。但这是不可能的,然后数据结构中学到了很多处理冲突的方法,有链表处理法——就是在桶中冲突的元素用链表将他们存起来;还有线性探测法等,这些大概就是碰到冲突,然后根据一些算法来换位置,再冲突就再换。
Java的HashMap就是用散列表这一技术来存key和value。在HashMap中,我们hash函数的对象是Key instance,用一个Node[][]的二维数组来做table。这个Node是结点,一个Node代表着一个key和一个value,可以理解成HashMap中存储的一个对象。这个table可以看作是桶群,每个node都是一个桶。
当然传进hash函数的不可能直接是这个Key的实例对象,而是它的hashCode()方法产生的一个hash code。这个hashCode()方法是基类Object的一个方法,如果你不对这个方法进行重写的话,hashCode()方法返回的是根据这个对象的地址生成的一个int的散列码。
jdk对这个key instance的hashcode进行一个处理后,然后将它散列出一个值,作为桶的index,然后将这个key代表的node放进桶里面。对hashCode的处理会在下面的源码分析中介绍到。
jdk1.8后的hashMap还对数据结构做了一个处理,当一个桶冲突的链表太长了的时候,会把链表改成红黑树,然后当冲突小了的话,又会变回链表。
补充:看了源码后发现,只是对那个桶的链表进行转换。
tips:如果你的类直接当作key在hashMap中使用的话,equals和hashCode这两个方法用的都是Object默认的,也就是说主要比较的是对象的地址。如果你想根据你的类的实例的内容来进行散列的话,请重写hashCode;如果你想通过你的类的实例的内容来进行key的查找的话,请重写equals方法。
而且你重写的hashCode不一定要是unique的,但你重写的equals方法一定要严格区分不同的对象!!
可以参考String这个例子,这个类就可以很好地在hashMap中当key使用,它重写了hashCode()和equals(),都是根据String的内容来生成的。
二、源码重要部分解析
类的声明部分:
继承了AbstractMap,这是个Map接口的简单实现类。然后实现了Map、Cloneable接口(之前的博客有讲过,这里是为了实现浅复制)还有序列化的Serializable接口。
常量部分:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
首先这个capacity指的是HashMap中的桶表格——table (下面会说到)所分配的内存。所以这个常量的意思是table的初始length也就是初始化的长度,为16。
static final int MAXIMUM_CAPACITY = 1 << 30;
table的capacity的最大值,如果用户在构造器中间接指明的的参数大于等于这个最大值的时候,table的capacity就会取这个。这个值是2的30次方。
这里我们会看到,而且源码中的备注中也有写到,这里的capacity一定要是2的整数次幂,因为这样才能很方便地用&、^等位操作符来进行一些运算,速度更快,下面的hash算法还有很多算法中会看到。
static final float DEFAULT_LOAD_FACTOR = 0.75f;
这个参数是加载因子,load_facotr = size/capacity,也就是map中存储的entry(键值对实体)除以table数组容量的值。这个值很重要,当这个facotr到达了这个数值,map就会进行扩容操作。如果在Map的构造器中没有特别指定load_factor,用的就是0.75.
static final int TREEIFY_THRESHOLD = 8;
这个值是说,table中,如果有桶(bucket)的链表的长度大于8,就有可能把所有的链表变成红黑树,是转变成树的一个阈值。
static final int UNTREEIFY_THRESHOLD = 6;
这个值是退化回链表的一个阈值,在扩容操作的时候,如果桶中的node数目小于6就变回链表喔。
static final int MIN_TREEIFY_CAPACITY = 64;
这个值是在转变成树之前,还会有一次判断,只有键值对数量大于 64 才会发生转换。这是为了避免在哈希表建立初期,多个键值对恰好被放入了同一个链表中而导致不必要的转化。
嵌套类Node的声明
/** * Basic hash bin node, used for most entries. (See below for * TreeNode subclass, and in LinkedHashMap for its Entry subclass.) */ static class Node<K,V> implements Map.Entry<K,V> {//这应该是普通节点的定义 final int hash; final K key; V value; Node<K,V> next; Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } public final K getKey() { return key; } public final V getValue() { return value; } public final String toString() { return key + "=" + value; } public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value);//大概是想让node的hash值和key和value都有关系吧 } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } public final boolean equals(Object o) { if (o == this) return true; if (o instanceof Map.Entry) { Map.Entry<?,?> e = (Map.Entry<?,?>)o; if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue())) return true; } return false; } }