hashmap源码学习

1什么是哈希表
哈希表是一种数据结构 它提供了快速的插入和查找操作 其基于数组来实现,实质上是将链表插入到数组中去
2哈希化
(1)直接将关键字作为索引
(2)将单词转换成为索引
3压缩后可能出现的问题
冲突,不能保证每个单词都映射到数组单元
4解决方法
开放地址法
链地址法
4hashcode
把任意长度的输入,通过散列算法,变换成固定长度的输出,该输出就是散列值(hashcode).
这种转换是一种压缩映射,散列值的空间通常远小于输入的空间,
不同的输入可能会散列成相同的输出,而不可能从散列值来唯一的确定输入值.
简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数.
HashMap是基于哈希表的Map接口的非同步实现.此实现提供所有可选的映射操作,并允许使用null值和null键.
此类不保证映射的顺序,特别是它不保证该顺序恒久不变.
5hashmap的数据结构
AVA中最基本的结构:数组+模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的.
Hashmap实际上是一个数组和链表的结合体(链表散列),
6始化数组
HashMap底层就是一个数组结构,数组中的每一项又是一个链表存储[key-value键值对的Entry的数组].
当新建一个hashmap的时候,就会初始化一个数组.
  
 public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
table = new Entry[DEFAULT_INITIAL_CAPACITY]; //初始化数组
init();
}
/**
* The table, resized as necessary. Length MUST Always be a power of two.
*/
  transient Entry[] table;
  static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
   final int hash;
  }

上面的Entry就是数组中的元素[table这个数组存储的并非是单纯的键值对,实际存储的是链表],
它持有一个指向下一个元素的引用[每一个Entry的next将指向该链表的下一个元素],这就构成了链表.

HashMap主要是用数组来存储数据的,它会对key进行哈希运算,哈希运算会有重复的哈希值,对于哈希值的冲突,HashMap采用链表来解决的.next就是为了哈希冲突而存在的.比如通过哈希运算,一个新元素应该在数组的第10个位置,但是第10个位置已经有Entry,那么好吧,将新加的元素也放到第10个位置,将第10个位置的原有Entry赋值给当前新加的 Entry的next属性.
数组存储的是链表,链表是为了解决哈希冲突的.

entry实际上是在实现单向链表结构,每一个entry内部有一个next指向他的后面,所以hashmap的内部数组每个格子存放的是实际上是一个链表的火车头,在最开始的时候table[index]位置上肯定是null,这个就是火车尾了。

所以get的时候查找操作是以null为循环终点判断依据的。在插入的时候把新加的元素替代原来的成为链表火车头,把之前元素的放在新加的火车头后面,这么做是因为不同的key是有可能算出相同的hash值的,这种情况的时候,他们都会存放在数组的同一个index位置上,相同的hash不同的key的值彼此用链表连接起来.
7Hash算法和索引位置
/**
* Initialization hook for subclasses. This method is called
* in all constructors and pseudo-constructors (clone, readObject)
* after HashMap has been initialized but before any entries have
* been inserted. (In the absence of this method, readObject would
* require explicit knowledge of subclasses.)
*/
void init() {
}

/**
* Applies a supplemental hash function to a given hashCode, which
* defends against poor quality hash functions. This is critical
* because HashMap uses power-of-two length hash tables, that
* otherwise encounter collisions for hashCodes that do not differ
* in lower bits. Note: Null keys always map to hash 0, thus index 0.
*/
static int hash(int h) {
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}

hash(int h)方法根据key的hashCode重新计算一次散列. 防止key本身的hashcode实现很差.
[/code]此算法加入了高位计算,防止低位不变,高位变化时,造成的hash冲突
[code="java"] // Returns index for hash code h.
static int indexFor(int h, int length) {
return h & (length-1);
  }

我们可以看到在HashMap中要找到某个元素,需要根据key的hash值来求得对应数组中的位置.如何计算这个位置就是hash算法.
HashMap的数据结构是数组和链表的结合,所以我们希望HashMap里面的元素位置尽量的分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,而不用再去遍历链表,这样就大大优化了查询的效率.
对于任意给定的对象,只要它的hashCode()返回值相同,那么程序调用hash(int h)方法所计算得到的hash码值总是相同的.
首先想到把hash值对数组长度取模运算(hash%length),这样一来元素的分布相对来说是比较均匀.但是”模”运算的消耗比较大.
能不能找一种更快速,消耗更小的方式?在HashMap中是这样做的:调用indexFor(int h, int length)方法来计算该对象应该保存在table数组的哪个索引处.这个方法通过h&(table.length-1)来得到该对象的保存位,而HashMap底层数组的长度总是2的n次方,这是HashMap在速度上的优化.
8存储实现
    //HashMap允许存放null键和null值.
    //当key为null时,调用putForNullKey方法,将value放置在数组第一个位置
    if (key == null)
  return putForNullKey(value);
    //获取key的hash值, 根据key的keyCode重新计算hash值
    int hash = hash(key.hashCode());
    //新元素插入HashMap的位置由其hash值决定.搜索指定hash值在对应table中的索引.
    //通过hash值和table长度算出该key的hash值所对应的table数组索引index:i
    int i = indexFor(hash, table.length);
    //遍历table[i]这个位置的链表. 如果i索引处的Entry不为null,通过循环不断遍历e元素的下一个元素
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//如果hash值与key都相同,表明该链表中已存在该key的entry,则更新该entry的value
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value; //临时变量:oldValue.最后返回oldValue
e.value = value; //新put进来的value作为entry最新的value
e.recordAccess(this);
return oldValue;
}
}
//若程序走到这里,table[i]处尚没有该key对应的entry:
    //i索引处的Entry为null,表明此处还没有Entry,插入entry
modCount++;
//将key、value添加到i索引处
addEntry(hash, key, value, i);
return null;
  }

往HashMap中put元素时,先根据key的hashCode重新计算hash值,根据hash值得到这个元素在数组中的位置(下标),
如果数组该位置上已经存放有其他元素了,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾.如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置上.
9如何正确使用hashmap
正确使用HashMap
1:不要在并发场景中使用HashMapHashMap是线程不安全的,如果被多个线程共享的操作,将会引发不可预知的问题,据sun的说法,在扩容时,会引起链表的闭环,在get元素时,就会无限循环,后果是cpu100%. 
2:如果数据大小是固定的,那么最好给HashMap设定一个合理的容量值
根据上面的分析,HashMap的初始默认容量是16,默认加载因子是0.75,也就是说,如果采用HashMap的默认构造函数,当增加数据时,数据实际容量超过10*0.75=12时,HashMap就扩容,扩容带来一系列的运算,新建一个是原来容量2倍的数组,对原有元素全部重新哈希,如果你的数据有几千几万个,而用默认的HashMap构造函数,那结果是非常悲剧的,因为HashMap不断扩容,不断哈希,在使用HashMap的场景里,不会是多个线程共享一个HashMap,除非对HashMap包装并同步,由此产生的内存开销和cpu开销在某些情况下可能是致命的.

当创建 HashMap 时,有一个默认的负载因子(load factor),其默认值为 0.75,这是时间和空间成本上一种折衷:增大负载因子可以减少 Hash 表(就是那个 Entry 数组)所占用的内存空间,但会增加查询数据的时间开销,而查询是最频繁的的操作(HashMap 的 get() 与 put() 方法都要用到查询);减小负载因子会提高数据查询的性能,但会增加 Hash 表所占用的内存空间.

掌握了上面知识之后,我们可以在创建 HashMap 时根据实际需要适当地调整 load factor 的值;如果程序比较关心空间开销、内存比较紧张,可以适当地增加负载因子;如果程序比较关心时间开销,内存比较宽裕则可以适当的减少负载因子.通常情况下,程序员无需改变负载因子的值.

如果开始就知道 HashMap 会保存多个 key-value 对,可以在创建时就使用较大的初始化容量,如果 HashMap 中 Entry 的数量一直不会超过极限容量(capacity * load factor),HashMap 就无需调用 resize() 方法重新分配 table 数组,从而保证较好的性能.当然,开始就将初始容量设置太高可能会浪费空间(系统需要创建一个长度为 capacity 的 Entry 数组),因此创建 HashMap 时初始化容量设置也需要小心对待.
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值