一、HashSet 源码
1、作者前言
从作者前言来看,我们就可以归纳HashSet的要点了:
- 实现Set接⼝
- 不保证迭代顺序
- 允许元素为null
- 底层实际上是⼀个HashMap实例
- 非同步
- 初始容量⾮常影响迭代性能
2、基本属性
3、构造函数
4、添加元素
value是⼀个Object,所有的value都是它。
所以可以直接总结出:HashSet实际上就是封装了HashMap,操作HashSet元素实际上就是操作HashMap。
下面我们来看看Map的put方法
看看如何计算key的hash
从而发现hash并不是直接使用key的hashCode()函数返回的int值,还需要进行位异或运算
补充:位异或运算(^)
运算规则是:两个数转为二进制,然后从高位开始比较,如果相同则为0,不相同则为1。
比如:8^11
8转为二进制是1000,11转为二进制是1011.从高位开始比较得到的是:0011.然后二进制转为十进制,就Integer.parseInt("0011",2)=3;
- HashSet底层是HashMap
- 添加一个元素时,先得到hash值,然后再与size-1进行与运算,得出索引值
- 找到存储数据表table,看这个索引位置是否已经存放的有元素
- 如果没有,就直接加入
- 如果有,说明索引值一样,判断hash值是否一样,再调用==和equals比较,如果相同就放弃添加,如果不相同,则添加到链表的尾部
- 在Java8中,如果一条链表的元素个数到达 TREEIFY_THRESHOLD(默认是8),并且table的大小>=MIN TREEIFY CAPACITY(默认64),就会进行树化(红黑树)
下面我们来看看HashMap是如何扩容的,主要分为2部分:
1、数组扩容
2、将旧表上的数据复制到新表上
看看是然后转换为红黑树的
总结:触发扩容的时机:
- 数组中数据的size达到阙值,指的并不是每个数组的索引位置,而是添加元素的个数。
- 当链表的长度达到TREEIFY_THRESHOLD = 8会触发树化,但如果数组的长度小于MIN_TREEIFY_CAPACITY = 64并不会树化,而是会进行扩容。
二、LinkedHashSet 源码
1、作者前言
从作者前言来看,我们就可以归纳LinkedHashSet的要点了:
- 迭代是有序的
- 允许为null
- 底层实际上是⼀个LinkedHashMap实例(其实就是数组+双向链表)
- 非同步
- 性能比HashSet差⼀丢丢,因为要维护⼀个双向链表
- 初始容量与迭代速度无关,LinkedHashSet迭代的是双向链表
2、构造函数
父类构造函数
3、添加元素
LinkedHashSet没有实现add()方法,所以调用父类HashSet的add()方法,而HashSet是调用底层数据结构map的put()方法。
public boolean add(E e) {
return map.put(e, PRESENT)==null; // 这里的map是构造函数创建的LinkedHashMap
}
因为底层创建的是LinkedHashMap,但LinkedHashMap并没有实现put()方法,所以调用的还是父类HashMap的put方法进行添加元素。只不过底层保存数据的类型不同,HashMap使用的是Node结点,而LinkedHashMap使用的是Entry(继承Node类增加了before和after属性用来实现双向链表)
添加方法:
LinkedHashMap重写了newNode()方法,从而实现在插入数据的时候实现前后指针
下面我们来看看这个双向链表是如何实现的~
可以看到这个双向链表的实现非常简单,就只是创建一个新Entry结点的时候直接链接在尾部,从而达到读取顺序一致。除此之外,LinkedHashSet 跟 HashSet 几乎一样。
三、TreeSet 源码
1、成员属性
2、构造函数
3、常用方法
1、添加元素,调用的是TreeMap的put方法
public boolean add(E e) {
return m.put(e, PRESENT)==null;
}
2、删除元素,调用的是TreeMap的remove方法
public boolean remove(Object o) {
return m.remove(o)==PRESENT;
}
下面不再过多阐述,具体细节查看TreeMap