github上有个开源项目对于理解hashMap很有帮助
github地址:
https://github.com/dn-jack/dn-jack-hashMap
参考文献
Hashmap实现原理
HashMap的数据结构
数组的特点是:寻址容易,插入和删除困难;而链表的特点是:寻址困难,插入和删除容易。那么我们能不能综合两者的特性,做出一种寻址容易,插入删除也容易的数据结构?答案是肯定的,这就是我们要提起的哈希表,哈希表有多种不同的实现方法,我接下来解释的是最常用的一种方法—— 拉链法,我们可以理解为“链表的数组” ,如图:
从上图我们可以发现哈希表是由数组+链表组成的,一个长度为16的数组中,每个元素存储的是一个链表的头结点。那么这些元素是按照什么样的规则存储到数组中呢。一般情况是通过hash(key)%len获得,也就是元素的key的哈希值对数组长度取模得到。比如上述哈希表中,12%16=12,28%16=12,108%16=12,140%16=12。所以12、28、108以及140都存储在数组下标为12的位置。
HashMap其实也是一个线性的数组实现的,所以可以理解为其存储数据的容器就是一个线性数组。这可能让我们很不解,一个线性的数组怎么实现按键值对来存取数据呢?这里HashMap有做一些处理。
1.首先HashMap里面实现一个静态内部类Entry,其重要的属性有 key , value, next,从属性key,value我们就能很明显的看出来Entry就是HashMap键值对实现的一个基础bean,我们上面说到HashMap的基础就是一个线性数组,这个数组就是Entry[],Map里面的内容都保存在Entry[]里面。
HashMap的存取实现
既然是线性数组,为什么能随机存取?这里HashMap用了一个小算法,大致是这样实现:
//存储时: int hash = key.hashCode();// 这个hashCode方法这里不详述,只要理解每个key的hash是一个固定的int值 int index = hash % Entry[].length; Entry[index] = value; //取值时: int hash = key.hashCode(); int index = hash % Entry[].length; return Entry[index];
到这里我们轻松的理解了HashMap通过键值对实现存取的基本原理
3.疑问:如果两个key通过hash%Entry[].length得到的index相同,会不会有覆盖的危险?
这里HashMap里面用到链式数据结构的一个概念。上面我们提到过Entry类里面有一个next属性,作用是指向下一个Entry。打个比方, 第一个键值对A进来,通过计算其key的hash得到的index=0,记做:Entry[0] = A。一会后又进来一个键值对B,通过计算其index也等于0,现在怎么办?HashMap会这样做:B.next = A,Entry[0] = B,如果又进来C,index也等于0,那么C.next = B,Entry[0] = C;这样我们发现index=0的地方其实存取了A,B,C三个键值对,他们通过next这个属性链接在一起。所以疑问不用担心。也就是说数组中存储的是最后插入的元素。到这里为止,HashMap的大致实现,我们应该已经清楚了。
当然HashMap里面也包含一些优化方面的实现,这里也说一下。比如:Entry[]的长度一定后,随着map里面数据的越来越长,这样同一个index的链就会很长,会不会影响性能?HashMap里面设置一个因素(也称为因子),随着map的size越来越大,Entry[]会以一定的规则加长长度。
3.解决hash冲突的办法
- 开放定址法(线性探测再散列,二次探测再散列,伪随机探测再散列)
- 再哈希法
- 链地址法
- 建立一个公共溢出区
Java中hashmap的解决办法就是采用的链地址法。
github中dn-jack-hashMap这个解决办法是再哈希
查看源码步骤:
1、先看其接口定义DNMap.java
2、再看其对接口的实现DNHashMap.java(类似实现java jdk中的hashmap)
3、最后看其测试代码Test.java
//=========下面先把代码复制上了,有空时补上对其原理的理解
===DNMap.java
package com.dongnao.jack;
public interface DNMap<K, V> {
public V put(K k, V v);
public V get(K k);
public int size();
public interface Entry<K, V> {
public K getKey();
public V getValue();
}
}
===DNHashMap.java(类似实现java jdk中的hashmap)
import java.util.ArrayList;
import java.util.List;
public class DNHashMap<K, V> implements DNMap<K, V> {
private static int defaultLength = 16; //默认数组长度
private static double defaultLoader = 0.75; //扩张因子,也就是当组数被使用超过0.75%时,该类会自动去扩充数组长度*2
private Entry<K, V>[] table = null; //存储的hash表数组<主键,值>
private int size = 0; //已经存放到数组的对象个数
public DNHashMap(int length, double loader) {
defaultLength = length;
defaultLoader = loader;
table = new Entry[defaultLength];
}
public DNHashMap() {
this(defaultLength, defaultLoader);
}
public V put(K k, V v) {
//在这里要判断一下,size是否达到了一个扩容的一个标准
if (size >= defaultLength * defaultLoader) {
up2size(); //调用宽容申请*2的空间
}
//1、 创建一个hash函数,根据key和hash函数算出数组下标
int index = getIndex(k);
Entry<K, V> entry = table[index];
if (entry == null) {
//如果entry为null,说明table的index位置上没有元素
table[index] = newEntry(k, v, null);
size++;
}
else {
//如果index位置不为空,说明index位置有元素,那么就要进行一个替换,然后next指针指向老数据
table[index] = newEntry(k, v, entry);
}
return table[index].getValue();
}
private void up2size() {
Entry<K, V>[] newTable = new Entry[2 * defaultLength];
//新创建数组以后,以前老数组里面的元素要对新数组进行再散列
againHash(newTable);
}
//新创建数组以后,以前老数组里面的元素要对新数组进行再散列
private void againHash(Entry<K, V>[] newTable) {
List<Entry<K, V>> list = new ArrayList<Entry<K, V>>();
for (int i = 0; i < table.length; i++) {
if (table[i] == null) {
continue;
}
findEntryByNext(table[i], list);
}
if (list.size() > 0) {
//要进行一个新数组的再散列
size = 0;
defaultLength = defaultLength * 2;
table = newTable;
for (Entry<K, V> entry : list) {
if (entry.next != null) {
entry.next = null;
}
put(entry.getKey(), entry.getValue());
}
}
}
private void findEntryByNext(Entry<K, V> entry, List<Entry<K, V>> list) {
if (entry != null && entry.next != null) {
list.add(entry);
findEntryByNext(entry.next, list);
}
else {
list.add(entry);
}
}
private Entry<K, V> newEntry(K k, V v, Entry<K, V> next) {
return new Entry(k, v, next);
}
private int getIndex(K k) {
int m = defaultLength;
int index = k.hashCode() % m;
return index >= 0 ? index : -index;
}
public V get(K k) {
//1、 创建一个hash函数,根据key和hash函数算出数组下标
int index = getIndex(k);
if (table[index] == null) {
return null;
}
return findValueByEqualKey(k, table[index]);
}
public V findValueByEqualKey(K k, Entry<K, V> entry) {
if (k == entry.getKey() || k.equals(entry.getKey())) {
return entry.getValue();
}
else {
if (entry.next != null) {
return findValueByEqualKey(k, entry.next);
}
}
return null;
}
public int size() {
return size;
}
class Entry<K, V> implements DNMap.Entry<K, V> {
K k;
V v;
Entry<K, V> next;
public Entry(K k, V v, Entry<K, V> next) {
this.k = k;
this.v = v;
this.next = next;
}
public K getKey() {
return k;
}
public V getValue() {
return v;
}
}
}
===Test.java
package com.dongnao.jack;public class Test {
public static void main(String[] args) {
DNMap<String, String> dnmap = new DNHashMap<String, String>();
Long t1 = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
dnmap.put("key" + i, "value" + i);
}
for (int i = 0; i < 10000; i++) {
System.out.println("key: " + "key" + i + " value:"
+ dnmap.get("key" + i));
}
Long t2 = System.currentTimeMillis();
// System.out.println("jack写的dnhashMap耗时:" + (t2 - t1));
// System.out.println("-----------------------HashMap--------------------------");
//
// Map<String, String> map = new HashMap<String, String>();
// Long t3 = System.currentTimeMillis();
// for (int i = 0; i < 1000; i++) {
// map.put("key" + i, "value" + i);
// }
//
// for (int i = 0; i < 1000; i++) {
// System.out.println("key: " + "key" + i + " value:"
// + map.get("key" + i));
// }
// Long t4 = System.currentTimeMillis();
// System.out.println("jdk的hashMap耗时:" + (t4 - t3));
}
}