目录
JDK哈希表原码分析,这是面试常考问题:非常高频的问题
阅读源码的能力:快速成长的捷径。JDK的源码质量非常之高,先去学习和模仿大神的思路和代码。
关于JDK类集源码的阅读。数据结构的基础+Java底层代码的熟悉程度
回到Object,万物之母。
hashCode
1.
2.
Object提供的hashCode可以将任意对象转为int,不同的对象(地址不同)
原则上一定转为不同的int。
注释:原则上自定义的类若需要保存到HashMap哈希表中,不能直接使用Object提供hashCode
需要覆写这个方法。
为什么需要覆写?
因为调用Object的返回值太大了,基本上都是千万,亿级别的。数组要开辟的空间太大了。
举例:自定义一个Student类
hashCode() :用来计算哈希值(索引值)
equals( ): 用来判断哈希表是是否存在了相同的key值
所以这两个方法都需要覆写。
问题:
第一个:必须相同。 第二个:不一定相同
结论:
1. equals相同的两个对象,就认为是同一个对象。哈希表中这个对象有且只能有一个。经过哈希函数运算后,这俩对象保存的索引也应该相同心~
2. hashCode相同,说明此时发生了哈希冲突,不—定就是相同的对象到底是否相同还要取决于equals方法。
3. 在哈希表中,只有equals和hashCode都相同的对象,才是 唯一对象
注释:Object中的equals默认比较的是地址,如果你想比较别的内容,比如说是key值,覆写就可以了。 所以上面说了,equals相同,按默认的equals相当于是地址相同,那肯定是同一个对象了,都同一个对象了,哈希值肯定也是相同。至于计算出相同的哈希值,说明很肯定是发生了哈希冲突,并不一定就是同一个对象。
举例: 把这三个test保存到HashMap中的key中,会保存几个?
2个:test1和test2。test3和test1根据就是同一个对象,只不过是在栈中又开辟了块空间叫test3的来保存了第一个对象的地址。
证明:put了三个对象,打印结果会怎么样?
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public class Test1 {
public static void main(String[] args) {
// //创建两个对象
// Test1 test1 = new Test1();
// Test1 test2 = new Test1();
// //调用hashCode
// System.out.println(test1.hashCode());
// System.out.println(test2.hashCode());
// //一个相同引用对象
// Test1 test3 = test1;//都指向test1对象地址
// System.out.println(test3.hashCode());
Student stu1 = new Student("张三", 18);
Student stu2 = new Student("李四", 18);
Student stu3 = new Student("张三", 18);
Map<Student, String> map = new HashMap<>();
map.put(stu1,"111");
map.put(stu2,"222");
map.put(stu3,"333");
System.out.println(map);
}
}
class Student{
private String name;
private int age;
@Override
public int hashCode() {
return 0;
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object o) {
if(this == o){
//说明此事地址相同,是同一个对象
return true;
}
//如果此时的 o 是个学生类披着Object的外衣
if(o instanceof Student){
//需要强制类型转换
Student stu = (Student) o;
return this.age == stu.age && this.name.equals(stu.name);
}
return false;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
2个对象 :stu1和stu3这两个对象的equals相同,此时在HashMap只会存储一份儿。相同的一个对象。equals相同的对象,就认为是同一个对象。
如果此时把equals方法里面判断直接都注释,无论什么对象比较,都是不同的,false。
再次运行:
现在就存储了三个对象。
我们就是要通过equals方法告诉JDK到底什么样的才算是同一个对象。
自定义对象作为key的唯—性就是通过equals方法保证的。
---------------------------------------------------------------------------------------------------------------------------------
JKD的源码分析
HashMap源码
注释:看源码不像是看书,一定要有目的的去看,带着疑问去看。
比如:
1. HashMap的哈希函数是如何设计的
2. put方法的逻辑,到底是如何存储元素的;
3. 当发生冲突时如何解决的;
4. 哈希表冲突较严重,如何扩容resize;
注释:
查看源码的方法 四种
1. 在IDEA界面双击 shift 键,会弹出一个界面,搜索就行。
⒉直接在使用的类上,ctrl +鼠标左键点进去即可 : 接口方法
3. Ctrl + Alt + 鼠标左键 :选择实现子类的方法,而不是接口方法
4. ctrl + < (方向盘左键) 和 ctrl + > (方向盘右键)
可以左右切换你访问的方法顺序,有点像入栈模式。
注释:有些是 ctrl + Alt + < (方向盘左键) 和 ctrl + Alt + + > (方向盘右键)
注释:有些软件的热键可能会冲突到底按了没反应。
比如看看HashMap的构造方法,是怎么初始化的。
1. JDK8z之后HashMap的结果如下:
数组+链表+红黑树(冲突严重的链表会被"树化",将链表转为红黑树,提高冲突严重的链表的查询效率)
注释:JDK8之前,JDK7以及更老版本,HashMap就是 数组+链表。红黑树是JDK8才加入的。
(这个问题面试大概率会问)
注释:IDEA左下角 Structure,点一下这个,类的所有方法就都显示出来了。
2.关于HashMap源码中属性的解读
![](https://img-blog.csdnimg.cn/34448f3321c640babf5a9948ea8fac45.png)
一个个来解析:
1. DEFAULT_INITIAL_CAPACITY 也叫 哈希桶
注释:哈希桶就是数组的大小
2. 最大数组容量
3. 负载因子
注释:超过计算值 12 就需要扩容
4. 树化阈值
5. 解树化
6.
若某个链表长度 >=8 ,而此时哈希桶的数量不足64则只是简单的哈希表扩容而已。
3.put方法解读
hash()方法:首先先计算一下key的哈希值
再点进行看看。
哈希Map的哈希方法 :hash
空对象放在编号为0的桶中。
不为空:key对象所在类的hashCode方法
若没有覆写,默认调用Object提供的hashCode方法
你会发现上面的hash结果还是很大,1111 的。
注释: 这个值还不是当前数组的索引,只是经过hash运算得到的一个比较均衡的
结论:高低16位都参与哈希函数的运算,尽可能保证不同key映射到比较均衡的状态
看数组索引的最终取值:还是看putVal()方法
这个求出来的值才是索引
当 n 是 2的 n次方 的时候,用n来 取模,或者是 与运算 结果都是一样的。
提问:既然 % 取模运算 和 & 位运算 在这种情况下是一样的,那为什么要 & hash 而不是 %运算
答:位运算的效率是最高的,这是把性能发挥到极致。所以能做 & 位运算的在HashMap中大量的进行着都是 &位运算。
有了上面两步操作,就可以将任意的正整数转为哈希桶数量之内的小整数 ==》 i就是当前key求的哈希桶的编号
可以用Debug进行查看,需要强制进入。Alt + Shift F7
putVal方法分析:
1.
2.
3.
4. 5.
6. 树化处理方法:
7.
总结:put方法
注释:看源码一定要先有大题思路,细节是如果实现的先不用管。
如果想计算自定义类型的哈希值,就可以调用Objects类的hashcode方法。
牵扯到自定义类型的hashCode的覆写,可以调用Objects类的hashCode方法
传入你想要计算的属性,当自定义类的两个对象里面的属性值相同,得到的哈希值就一定相同。
注释:objects是Object类的工具类
就看两个方法 hashCode 和 equals
标准:
若自定义的类型需要保存到Map的Key中则需要同时覆写hashCode和equals
equals相同的俩对象,hashCode必须相同,反之则不一定
因为我们覆写的equals方法里面也有比较成员属性。
所以才说equals相同,haxhCode一定相同。
---------------------------------------------------------------------------------------------------------------------------------
4.关于哈希桶的数量必须为2^n的说明
(n - 1)&hash == hash % n
先看看:HashMap的构造方法
我们在new这个HashMap的时候,并没有产生哈希桶,或者说数组还没设置长度。
内部数组还没有初始化,只有第一次调用put方法时才初始化内部哈希桶数组。(懒加载模式)
第一次使用(添加)时才初始化相应的内存
还有有参构造方法
当你传入的数字表示2^n 大小值,就自动帮你转换。 最接近的。
当传入31时,会自动转换成32。这样才不会让哈系方法失效。
5.
关于Set集合和Map集合的关系
先说结论:Set集合的子类实际上在存储元素时就是放在了Map集合的Key中
这也是为啥Set是不可重复的!!!
HashSet其实就使用HashMap保存的
TreeSet其实就使用TreeMap保存的
注释:value值用个空值来代替
Set就是用的Map的子类来存储元素,Set的不可重复就是因为元素保存在了Map的Key
因此Set保存的元素不可重复!!
HashSet能否保存null : 可以因为HashMap的key可以为null
TreeSet能否保存null : 不可以因为TreeMap的key不能为null