1.哈希表的基本结构就是“数组+链表” 此外,JDK8中,当链表长度大于8时,为了保证查询速度,链表就转换为红黑树,小于等于6时候就又会变为链表。
原因:根据泊松分布,链表长度为8时候概率极低,转换成红黑树会占用更多的空间,为了保证均衡设为8(红黑树速度比链表快)
2.Entry[] table (将Entry放入到 table数组中) 就是HashMap的核心数组结构,我们也称之为“位桶数组”。
3.一个 Entry对象存储了:(1).key:键对象 (2).value:值对象 (3).next:下一个节点 (4).hash: 键对象的hash值
4.存储方法set的原理:
- (1)首先调用key对象的hashcode()方法,获得hashcode。
- (2)根据hashcode计算出hash值(要求在[0, 数组长度-1]区间) 数组初长度为16
- (3) 一种简单和常用的算法是(相除取余算法):hash值 = hashcode%数组长度,这种算法可以让hash值均匀的分布在[0,数组长度-1]的区间。 早期的HashTable就是采用这种算法。但是,这种算法由于使用了“除法”,效率低下(jdk1.8之前)。
- (4)JDK1.8后来改进了算法。首先根据key的值计算出hashcode的值,然后根据hashcode计算出hash值(异或其右移十六位),最后通过hash&(length-1)计算得到存储的位置,并且约定数组长度必须为2的整数幂,这样采用位运算既可实现和hashcode%length算出来的值是一样,也能提高效率,(流程图如下)。
- (5).从上图看出h = k.hashCode()) ^ (h >>> 16,为什么要 hashcode 异或其右移十六位的值: JDK1.8 优化了高位运算的算法,通过hashCode()的高16位异或低16位实现:(h = k.hashCode()) ^ (h >>> 16)。这么做可以在数组 table 的 length 比较小的时候,也能保证考虑到高低Bit都参与到Hash的计算中,同时不会有太大的开销。
5.HashMap数组的长度为什么是 2 的幂次方?
如图所示,length为2的幂次方,假设如果 length=16 ,那他的转化为二进制必定是11111……的形式,通过位运算&hash之后得到的数依旧是hash本身,那么可以再length长的数组长均匀分布,效率就高。
在 1、3、5、7、9、11、13、15 等奇数位处没有存放数据。因为hash值在与14(即 1110)进行&运算时,得到的结果最后一位永远都是0,0&一个数之后只能是0,也就是只能存放偶数位,即 0001、0011、0101、0111、1001、1011、1101、1111位置处是不可能存储数据的。
6.寻址方法get原理:
- (1) 获得key的hashcode,通过hash()散列算法(上面的4.(4))定位到数组的位置。
- (2) 在链表上挨个比较key对象。 调用equals()方法,将key对象和链表上所有节点的key对象进行比较,直到碰到返回true的节点对象为止。
- (3) 返回equals()为true的节点对象的value对象。
- (4).注意: Java中规定,两个内容相同(equals()为true)的对象必须具有相等的hashCode。hash值相同不一定hash和key相同因为区余算法。
7.扩容问题 :HashMap的位桶数组,初始大小为16。实际使用时,显然大小是可变的。如果位桶数组中的元素达到(0.75*数组 length), 就重新调整数组大小以2倍扩容
原因:0.75是负载因子,如果是0.5的话,数据存储一半就扩容太浪费空间,如果是1的话,数组满了再扩容会有很多哈希冲突,效率就变低了。
class HashNode2<K,V> {//结点的存储空间
int hash ;
K key;
V values;
HashNode2 next;
}
class SxtHashMap01<K,V>{
HashNode2 [] table;//位桶数组
int size ;
public SxtHashMap01(){
table = new HashNode2[16];
}
public void put(K key, V values) {
//关于扩容数组问题没写 可根据ArrayList的扩容方法写出
HashNode2 <K,V>newNode = new HashNode2<>();
newNode.hash = Myhash(key.hashCode(),table.length);//hashCode()是Object类的方法获得的是一个独特的值(内存空间值)
newNode.key = key;
newNode.values= values;
newNode.next=null;
HashNode2 temp = table[newNode.hash];
HashNode2 last =null;//记录结尾的结点
boolean is = false; //用于记录如果 输入的key相同时候不需要加一个结点
if(temp == null) {//数组 哈希值位置为空直接将新节点放入 ,没有哈希冲突
table[newNode.hash]=newNode;
size++;
}else {//如果不为空就遍历newNode链表
while(temp!=null) {
//如果key值相同,就覆盖
if(temp.key.equals(key)) {
temp.values =values;
is = true;
break;
}else {
//key值不相同就遍历下一个
last=temp;
temp = temp.next;
}
}
if(!is) {//没有重复的key值则加入到链表后面
last.next=newNode;
size++;
}
}
}
public V get(K key) {
//首先计算哈希值:确定存储这个结点 数组位置
int hash = Myhash(key.hashCode(),table.length);
V values=null;
if(table[hash]!=null) {//如果查找到的hash存在那么实例化一个hashNode2的对象
HashNode2 temp = table[hash];
while(temp!=null) {//当结点不为空时候
//然后遍历链表查找相同key的值
if(temp.key.equals(key)) {//查找到之后存储跳出循环,没有相同的key会继续遍历
values=(V)temp.values;
break;
}else {
temp=temp.next;
}
}
}
return values ;
}
public int Myhash(int hashcode,int length) {//获得哈希值
int a = hashcode&(length-1);//取模运算效率低只限定于hashcode为2的整数幂,但是分散性好, 另一种取余运算 hashcode&(length-1)效率高
System.out.println("hash值为:"+a);
return a;
}
@Override
public String toString() {//重写toString 使用StringBuilder存储遍历
StringBuilder sb = new StringBuilder("{");
for(int i = 0;i<table.length;i++) {
HashNode2 temp = table[i];
while(temp!=null) {
sb.append("key="+temp.key+" values="+temp.values+",");
temp=temp.next;
}
}
sb.setCharAt(sb.length()-1, '}');
return sb.toString();
}
}
public class HashMap手动实现底层 {
public static void main(String[] args) {
SxtHashMap01<Integer,String> m = new SxtHashMap01<>();
m.put(11, "aa");
m.put(22, "bb");
m.put(33, "cc");
m.put(53, "gg");
m.put(69, "hh");
m.put(85, "dd");
System.out.println(m);
System.out.println(m.get(53));
System.out.println(m.get(69));
System.out.println(m.get(85));
}
}