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
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对象后面。
整体结构(借用下大神的图):
三:几个关键参数以及构造方法。
//默认的初始容量
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
一个最原始的办法,就是拿这个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
return null == entry ? null : entry.getValue();
}
final Entry
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
for (Entry
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,这就是数学上的问题了。就到这里。如有不对,多谢指正。