Set接口
仍然是经典口诀: 无序、不重复、无索引
-
Set接口的常用方法
- 和List一样,Set接口也是Collection的子接口,因此,其常用方法和Collection接口是一样的
-
Set接口的遍历方式
- 使用迭代器进行遍历
- 增强for循环进行遍历
- 不能使用索引的方式来获取
注: 实际上我们在使用Set集合来进行编写的时候,一定要有一个“上帝视角”,一旦放入Set,那么我们就一定能够通过全局来知晓 某个东西究竟在不在集合中。
HashSet
HashSet实际上就是HashMap,底层使用的是HashMap
public static void main(String[] args) {
/**
* 底层源码:
* public HashSet() {
* map = new HashMap<>();
* }
*/
Set<String> set = new HashSet<>();
}
HashSet底层机制说明
HashSet的底层就是HashMap,HashMap的底层是(数组+链表+红黑树)三者组合起来一块儿构成的。
直接先说结论:
HashSet底层是HashMap
private transient HashMap<E,Object> map; private static final Object PRESENT = new Object();
HashSet中维护了一个HashMap对象map,HashSet的重要方法,比如add、remove、iterator、clear、size等等都是围绕map来实现的。
- 通过上面的源码我们注意到,
HashSet中维护的map,被transient修饰,在ArrayList中就已经有提到,被transient修饰,就代表着无法进行序列化。和ArrayList相同,HashSet类中通过定义writeObject()和readObject()方法确定了其序列化和反序列化的机制。- 同时,在
HashSet中还维护了一个静态常量Object,这个实际上就是HashSet用来“偷鸡”的,因为HashSet中任何元素,在底层都是作为HashMap的Key来进行存储,而每个Key所对应的Value,就用这个常来那个PRESENT来顶替了添加一个元素时,先得到hash值,会被转换成->索引值
找到存储数据表table(实际上这个所谓的table就是数组),看这个索引位置是否已经存放的有元素
如果没有,直接加入
如果有,调用
equals方法比较,如果相同,就放弃添加,如果不相同,就添加到最后
- 对
equals方法进行解读,实际上equals是Object类的方法,然后Object类是最顶层的类,其他的任何类都是其子类。Object类中的最原始equals方法,因为Java中是值传递,所以它只会比较值,如果是俩对象,比较的也只是二者的地址值。 像我们使用的很多的类String、Integer等,很多都已经重写了equals()方法,核心代码是比较二者的具体内容值。- 如果是自己自定义的类,实际上也要重写equals()方法和hashCode()方法
在Java8中,如果一条链表的元素个数达到
TREEIFY_THRESHOLD(默认是8),并且table的大小>=MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树)
HashSet底层源码解读(重要重要重要)
自己简单写的代码:
-
重点分析两块儿内容
HashSet的构造方法add方法的源码分析
public class SetDemo2 {
public static void main(String[] args) {
HashSet<String> set = new HashSet<>();
set.add("java");
}
}
HashSet构造方法
源码为:
不用再过多赘述,实际上HashSet底层就是使用的是HashMap,本质上就是给HashMap披上了一层皮而已
public HashSet() {
map = new HashMap<>();
}
HashSet 的add()方法
解读源码的顺序,按照debug调用顺序一步步进入,相关的解读直接敲在源码的注解中:
-
初步进入add()方法
-
public boolean add(E e) { //直接调用的是map的put方法。 //是将值传入到map的键中,PRESENT就是一个静态常量 //private static final Object PRESENT = new Object(); //说白了这个PRESENT不用理会,就是源码用来偷鸡的!!! return map.put(e, PRESENT)==null; }
-
-
进入
put()方法-
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }-
先进入
hash()方法- 实际上就是为了计算当前传入的值,计算它的哈希值。看到下面的源码,
hashCode()具体内容我们不去探究。但是有一个点必须非常明确:▲ 哈希值的计算,并不仅仅是依赖于hashCode()方法,然后与其通过hashCode()方法求得的哈希值向右偏移16位,然后求异或,
- 实际上就是为了计算当前传入的值,计算它的哈希值。看到下面的源码,
-
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
-
-
进入
putVal()方法 。先说结论,如果能够插入,那么putVal()方法返回的实际是null,返回null证明插入成功;插入不成功/无法插入的话,那么实际上putVal()我们可以认为是其执行的是查找功能,返回查找到的元素。-
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { //创建临时变量 Node<K,V>[] tab; Node<K,V> p; int n, i; //检查当前哈希表table是否为空或者长度为0,如果是,则需要进行初始化 if ((tab = table) == null || (n = tab.length) == 0) //如果哈希表需要初始化,那么调用resize()方法来初始化哈希表,并且更新长度 n = (tab = resize()).length; //把键的哈希值通过(n-1)&hash的方式,将其转换为数组索引,检查该位置是否为空 if ((p = tab[i = (n - 1) & hash]) == null) //如果位置为空,那么直接插入即可 tab[i] = newNode(hash, key, value, null); //如果位置不为空,那么就说明有冲突 //那么就看我这个要插入的这个东西究竟会不会插进来,就遍历链表,看我能不能放到链表里面 else { //创建局部变量 Node<K,V> e; K k; //如果两者哈希值相同并且key值的地址也相同(那就是同一个东西)或者是通过equals()方法返回true if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //否则再判断p是不是一棵红黑树 //如果是是一棵红黑树,那么就调用putTreeVal方法,再来进行添加。(这里面又会进行一些新的判断) else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); //如果table对应的索引位置,后面链了个链,就使用for循环来进行比较 else { for (int binCount = 0; ; ++binCount) { //这个if是完成如下功能: //1、把当前node能够转到链表的下一个node //2、判断是否已经指向null(即看是否是最终的节点) if ((e = p.next) == null) { //如果执行到最终的节点,那么就可以进行插入 p.next = newNode(hash, key, value, null); //在把元素添加到链表之后,立刻进行判断,该链表是否已经达到8个结点 //如果是,就调用treeifyBin(),对当前链表进行树化 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } //这儿跟前面一样,判断哈希值是否一样并且是同一对象。 或者是说key不为空,并且通过equals方法判为true if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } //如果最后没有执行插入操作(也就是e==null) if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); //返回值。 (也就是前面说的查询操作) return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); //这是最后插入成功,插入成功就返回null return null; }
-
-
HashSet的底层扩容机制
-
HashSet底层是HashMap,第一次添加时,table数组扩容到16,临界值(threshold) 是16*加载因子(loadFactor)是0.75 = 12 -
如果table数组使用到了临界值12,就会扩容到16*2=32,新的临界值就是 32x0.75 = 24 。以此类推
- 也就是说,每次的临界值都是当前容量的0.75倍,一旦达到了这个临界值,那么就会扩充容量为当前容量的2倍。
-
在Java8中,如果一条链条的元素个数到达
TREEIFY_THRESHOLD(默认是8),并且table的大小>=MIN_TREEIFY_CAPACITY(默认是64),就会进行树化(红黑树),否则仍然采用数组扩容机制
这里直接通过debug来进行调试
代码:
public static void main(String[] args) {
HashSet<Integer> set = new HashSet<>();
for(int i=1;i<100;i++){
set.add(i);
}
}
当容量size超过阈值也就是16*0.75 = 12的时候,这时容量就会倍扩充为原来的2倍
特别注意: 什么时候数组会被扩容? 难道是数组的长度占用到了阈值? 并不是!!
这里回归到前面提到的add()方法putVal()方法的源码的最后几行。
注意到下面的源码,++size 什么概念?就是只要我加一个元素进来,不管这个元素是加在数组上,还是链在链表上,还是挂在红黑树上。都算是所谓的容量增加。所以只要使用容量到了阈值,那么就会执行扩容。
if (++size > threshold)
resize();
韩顺平老师留下的小练习:
定义一个Employee类,该类包括: private成员属性name,sal,birthday(MyData类型),其中birthday为MyDate类型(属性包括:year,month,day) ,要求:
- 创建三个Employee放入到HashSet中
- 当name和birthday的值相同时,认为是相同员工,不能添加到HashSet集合中
代码如下:
package com.nylonmin.setDemo;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
public class SetDemo3 {
public static void main(String[] args) {
Set<Employee> set= new HashSet<>();
Employee e1 = new Employee("zhangsan",20,"2010/10/10");
Employee e2 = new Employee("zhangasdff",20,"2000/1/1");
Employee e3 = new Employee("lisi",20,"2001/10/10");
Employee e4 = new Employee("zhangsan",2045,"2010/10/10");
Employee e5 = new Employee("wangwu",20,"2010/10/10");
set.addAll(Arrays.asList(e1,e2,e3,e4,e5));
for(Employee e : set){
System.out.println(e);
}
}
}
class Employee{
private String name;
private double sal;
private MyData birthday;
private static class MyData{
private int year;
private int month;
private int day;
MyData(int year,int month,int day){
this.year = year;
this.month=month;
this.day=day;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
MyData myData = (MyData) o;
return year == myData.year && month == myData.month && day == myData.day;
}
@Override
public int hashCode() {
return Objects.hash(year, month, day);
}
@Override
public String toString() {
return "MyData{" +
"year=" + year +
", month=" + month +
", day=" + day +
'}';
}
}
//构造函数
public Employee(String name,double sal,String data){
this.name= name;
this.sal=sal;
String[] split = data.split("/");
int year = Integer.parseInt(split[0]);
int month = Integer.parseInt(split[1]);
int day = Integer.parseInt(split[2]);
this.birthday= new MyData(year,month,day);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Employee employee = (Employee) o;
return Objects.equals(name, employee.name) && Objects.equals(birthday, employee.birthday);
}
@Override
public int hashCode() {
return Objects.hash(name, birthday);
}
@Override
public String toString() {
return "Employee{" +
"name='" + name + ''' +
", sal=" + sal +
", birthday=" + birthday +
'}';
}
}
控制台输出如下:
Employee{name='zhangsan', sal=20.0, birthday=MyData{year=2010, month=10, day=10}} Employee{name='zhangasdff', sal=20.0, birthday=MyData{year=2000, month=1, day=1}} Employee{name='wangwu', sal=20.0, birthday=MyData{year=2010, month=10, day=10}} Employee{name='lisi', sal=20.0, birthday=MyData{year=2001, month=10, da