HashMap与Hashtable大同小异,这里主要讲一下他们在散列方式上的差异
Hashtable的散列方式是取模
它的put方法有这样一句代码
int index = (hash & 0x7FFFFFFF) % tab.length;
这里的0x7FFFFFFF是一个运算遮罩,防止hash为负数,然后取模
HashMap的散列方式是位运算
它的hash方法将key的hash值的低16位和高16位做一次异或运算(这里的HashMap是jdk1.8以后的HashMap)
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
put方法中调用了putVal方法
其中有一句p = tab[i = (n - 1) & hash])相当于又做了一次与运算,把hash值的高位全部置0
hash方法的目的在于将32位二进制数都充分利用起来,尽量减少hash冲突,假设当前的size是16,不调用hash方法,直接将key.hashCode()与n - 1做与运算,只有最后四位是有效的,其他位全部没有意义,0x00000000和0xABCDEF00,得到的index是一样的,所以这也是HashMap要两倍扩容的原因
而且HashMap的散列方式使用的是简单的与运算和异或运算,相比Hashtable的取模运算,速度会快很多
———————————————————————————————————————
下面是一个自定义的红黑树,没什么实现,没有红黑树,没有用Entry,没有巧妙的散列方式,以后能不能实现还要看老师会不会接着讲(虽然我觉得面试问红黑树的公司如果不是超大厂就是傻逼,但就是有傻逼面试会问红黑树的)
自定义HashMap
- 数据结构:这里用的是数组+链表的形式,链表上的节点用自定义的类Node来表示
- put方法:首先判断当前key是否为null,若key为null,hash为0,否则对hashCode()进行取模运算;其次判断是否已经插入,若已经插入,则更新key对应的value值,若尚未插入,则插入到数组对应index上的链表的最新位置
- get方法:依旧是先判null,得到index值后找到对应链表,逐一比较key值,一样则返回value,未找到则返回null
- rehash方法:当存储的元素个数大于阈值且插入出现hash冲突时,调用rehash方法进行扩容,然后对所有节点进行重新插入,这个步骤会比较费时,此处的加载因子取的是0.75,和HashMap是一样的,之所以取0.75是因为这个值可以在空间和时间的消耗上取得一个较好的平衡(或许你想了解泊松分布吗)
package util;
import java.util.ArrayList;
import java.util.HashMap;
public class MyHashMap {
private Node[] nodes;
private static final int DEFAULT_MAX_CAPACITY = 1 << 30;
private static final int DEFAULT_CAPACITY = 16;
private static final float DEFAULT_LOAD_FACTOR = 0.75f;
int size = DEFAULT_CAPACITY;
int threshold = (int) (size * DEFAULT_LOAD_FACTOR);
int count = 0;
public MyHashMap() {
nodes = new Node[size];
}
public MyHashMap(int size) {
if (size > DEFAULT_CAPACITY) {
this.size = size;
threshold = (int) (size * DEFAULT_LOAD_FACTOR);
}
nodes = new Node[size];
}
public void put(Object key, Object value) {
int hash = key == null? 0: key.hashCode();
int index = hash % size;
Node root = nodes[index];
Node temp = root;
boolean hasSameKey = false;
while(temp != null) {
if(temp.key == key) {
temp.value = value;
hasSameKey = true;
break;
}
temp = temp.next;
}
if(!hasSameKey) {
Node node = new Node(key, value, temp);
nodes[index] = node;
count++;
if (count >= threshold && root != null) {
rehash();
}
}
}
public Object get(Object key) {
int hash = key == null? 0: key.hashCode();
int index = hash % size;
Node temp = nodes[index];
while (temp != null) {
if (temp.key.hashCode() == key.hashCode() && temp.key.equals(key)) {
return temp.value;
}
temp = temp.next;
}
return null;
}
private void rehash() {
if (size >= DEFAULT_MAX_CAPACITY) {
size = DEFAULT_MAX_CAPACITY;
threshold = DEFAULT_MAX_CAPACITY;
} else if (size < DEFAULT_CAPACITY) {
size = DEFAULT_CAPACITY;
threshold = (int) (size * DEFAULT_LOAD_FACTOR);
} else {
size *= 2;
threshold *= 2;
}
Node[] temp = nodes;
nodes = new Node[size];
count = 0;
for (Node node : temp) {
put(node.key, node.value);
}
}
public static void main(String[] args) {
MyHashMap myMap=new MyHashMap();
HashMap map=new HashMap();
long start = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
myMap.put(i, i + "");
}
long end = System.currentTimeMillis();
float time = (end - start) / 1000f;
System.out.println("MyHashMap" + time + "s");
start = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
map.put(i, i + "");
}
end = System.currentTimeMillis();
time = (end - start) / 1000f;
System.out.println("HashMap" + time + "s");
}
private class Node {
Object key;
Object value;
Node next;
public Node(Object key, Object value, Node next) {
this.key = key;
this.value = value;
this.next = next;
}
}
}
运行结果如下:
- 插入10000000个节点
- 插入1000000个节点
- 插入100000个节点
说一下问题,果然很慢吧,原因在于散列方式用的取模,而且没有使用红黑树,当链表上的节点增多之后如果要查找一个节点就要花费很多时间(时间复杂度O(n)),还有比如构造函数的size传入的是一个大于16的非二的指数次幂数,不会做任何处理