jdk1.8以前为 HashMap由hash函数+数组+单链表实现(本次实现)。
jdk 1.8版本之后的java中HashMap是数组+链表+红黑树实现的。在链表长度<8的时候都是一样的,当链表长度到达8时,该链表会转换成红黑树来存储节点。
结构图:
(图片来自网络)
如图:0-6表示数组长度,纵向为单链表,每一列的 hash(key) 得到的下标相同。
实现代码:
- 定义接口
public interface BaseMap<K,V> {
public V put(K k, V v);
public V get(K k);
}
/**
* 节点的基类接口
*/
public interface BaseEntry<K,V> {
public K getKey();//获取键
public V getValue(); //获取值
}
2.单链表实现
/**
* 单链表的体现!
*
*/
public class Entry<K,V> implements BaseEntry<K,V> {
private K k;
V v;
Entry<K,V> next;//存在next指向下一个节点,形成链表
public Entry(K k, V v,Entry<K,V> next) {
this.k = k;
this.v = v;
this.next=next;
}
@Override
public K getKey() {
return k;
}
@Override
public V getValue() {
return v;
}
}
3.实现Map
public class MyHashMap<K,V> implements BaseMap<K,V> {
//默认初始化长度
private static final int DEFAULT_INITIAL_CAPACITY= 1<<4;
//阈值比例(超过这个阈值长度*2)
private static final float DEFAULT_LOAD_FACTOR= 0.75F;
//属性
private int defaultLength ;//长度
private double defaultAddFactor;//负载因子
private int useSize;//使用数组位置的数量
private int entryUseSize;//map中 entry的数量
private Entry<K, V>[] table;//数组
//构造方法 门面模式”。这里的2个构造方法其实指向的是同一个,但是对外却暴露了2个“门面”
// 默认使用基本长度和负载因子
public MyHashMap() {
this(DEFAULT_INITIAL_CAPACITY,DEFAULT_LOAD_FACTOR);
}
// 自定义长度和负载因子。
public MyHashMap(int defaultLength, double defaultAddFactor) {
if (defaultLength < 0) {
throw new IllegalArgumentException("数组异常");
}
if (defaultAddFactor <= 0 || Double.isNaN(defaultAddFactor)) {
throw new IllegalArgumentException("因子异常");
}
this.defaultLength = defaultLength;
this.defaultAddFactor = defaultAddFactor;
table = new Entry[defaultLength];
}
/**
* 使用每个object的hashCode计算hashCode
*通过向左位移,和与或运算
* @param hashCode
* @return hashCode
*/
private int hash(int hashCode) {
hashCode = hashCode ^ ((hashCode >>> 20) ^ (hashCode >>> 12));
return hashCode ^ ((hashCode >>> 7) ^ hashCode >>> 4);
}
/**
* 获取保存位置的数组下标
*
* @param k
* @param length
* @return
*/
private int getIndex(K k, int length) {
int m = length - 1;
int index = hash(k.hashCode()) & m;//安位与
return index >= 0 ? index : -index;
}
/**
* 扩容i
* 对于HashMap而言,如果频繁进行resize/rehash操作,是会影响性能的。
*resize/rehash的过程,就是数组变大,原来数组中的entry元素一个个的put到新数组的过程
* @param i
*/
private void resize(int i) {
Entry<K, V>[] newTable = new Entry[i];
//改变数组大小
defaultLength=i;
entryUseSize=0;
useSize=0;
rehash(newTable);
}
//对新数组设置值
private void rehash(Entry<K, V>[] newTable){
//得到原来老map中的entry集合
List<Entry<K, V>> entryList = new ArrayList<>();
for (int i = 0; i < table.length; i++) {//遍历数组
if (table[i] == null)
continue;
//遍历链表 添加到list
Entry<K, V> entry = table[i];
while (entry != null) {//遍历单链表
entryList.add(entry);
entry = entry.next;
}
}
//添加到新的map中
if (entryList.size() > 0) {
table = newTable;//覆盖旧的引用
for (Entry<K, V> entry : entryList) {
//分离所有的entry
if (entry.next != null) {
entry.next = null;
}
put(entry.getKey(), entry.getValue());//entryUseSize/useSize 重新计算
}
}
}
/**
* 第一,要考虑是否扩容?
*HashMap中的Entry的数量(数组以及单链表中的所有Entry)是否达到阀值?
*第二,如果扩容,意味着新生成一个Entry[],不仅如此还得重新散列。
*第三,要根据Key计算出在Entry[]中的位置,定位后,如果Entry[]中的元素为null,那么可以放入其中,如果不为空,那么得遍历单链表,要么更新value,要么形成一个新的Entry“挤压”单链表!
* @param k
* @param v
* @return
*/
@Override
public V put(K k, V v) {
V oldValue=null;
if(useSize > defaultAddFactor * defaultLength){
//扩容
resize(2*defaultLength);
}
//计算出下标(数组中的位置)
int index =getIndex(k,table.length);
Entry<K, V> entry = table[index];
Entry<K, V> newEntry = new Entry<>(k, v, null);
if (entry == null) {
table[index] = newEntry;//新值(新的位置)
useSize++;//占用table中的一个位置
++entryUseSize;
}else{//该位置已经被占用
Entry<K, V> t = entry;
//改值
while (t != null) {//遍历单链表(查看是否有相同的key)
if (t.getKey() == k || (t.getKey() != null && t.getKey().equals(k))) {//相同key 对应修改当前value 直接退出
oldValue=t.v;
t.v = v;//赋值
return oldValue;
}
t = t.next;
}
//添加值 不同key 添加到当前数组位置的链表
table[index]= new Entry<>(k,v,entry);//同一位置添加值在尾端
++entryUseSize;
}
return oldValue;
}
@Override
public V get(K k ) {
//获取数组下标
int index = getIndex(k, table.length);
//获取当前链表
Entry<K, V> entry = table[index];
if (entry == null) {
return null;
}
//循环单链表取值
do{
if (k == entry.getKey() || k.equals(entry.getKey())) {
return entry.v;
} else {
entry = entry.next;
}
}while (entry != null);
return null;
}
}
4.测试
public static void main(String[] args) {
BaseMap<String,String> map = new MyHashMap<>();
map.put("小明","10");
System.out.println("old="+map.put("小明","20"));
System.out.println("new="+map.get("小明"));
/*for (int i = 0; i < 500; i++) {
map.put("key"+i,"value"+i);
}
for (int i = 0; i < 500; i++ ) {
System.out.println("key"+i+",value is :" +map.get("key"+i));
}*/
}
5.扩容影响性能,如果已知最大长度可以设置长度创建
数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry的next指向null),那么对于查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。